promptlineapp 1.3.11
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/README.md +138 -0
- package/bin/cli.js +1911 -0
- package/package.json +41 -0
package/bin/cli.js
ADDED
|
@@ -0,0 +1,1911 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* create-promptline-app CLI
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* npx create-promptline-app my-app
|
|
8
|
+
* npx create-promptline-app my-app --preset saas
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
const readline = require('readline');
|
|
14
|
+
|
|
15
|
+
// ANSI colors
|
|
16
|
+
const colors = {
|
|
17
|
+
reset: '\x1b[0m',
|
|
18
|
+
bold: '\x1b[1m',
|
|
19
|
+
dim: '\x1b[2m',
|
|
20
|
+
red: '\x1b[31m',
|
|
21
|
+
green: '\x1b[32m',
|
|
22
|
+
yellow: '\x1b[33m',
|
|
23
|
+
blue: '\x1b[34m',
|
|
24
|
+
magenta: '\x1b[35m',
|
|
25
|
+
cyan: '\x1b[36m',
|
|
26
|
+
white: '\x1b[37m',
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const c = colors;
|
|
30
|
+
|
|
31
|
+
// Utility functions
|
|
32
|
+
function success(message) {
|
|
33
|
+
console.log(`${c.green}✓${c.reset} ${message}`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function error(message) {
|
|
37
|
+
console.log(`${c.red}✗${c.reset} ${message}`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function printLogo() {
|
|
41
|
+
console.log(`
|
|
42
|
+
${c.cyan}╔═══════════════════════════════════════════════════════════╗
|
|
43
|
+
║ ║
|
|
44
|
+
║ ${c.bold}create-promptline-app${c.reset}${c.cyan} ║
|
|
45
|
+
║ Build AI-powered applications with PromptLine ║
|
|
46
|
+
║ ║
|
|
47
|
+
╚═══════════════════════════════════════════════════════════════╝${c.reset}
|
|
48
|
+
`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function slugify(text) {
|
|
52
|
+
return text
|
|
53
|
+
.toLowerCase()
|
|
54
|
+
.trim()
|
|
55
|
+
.replace(/[^\w\s-]/g, '')
|
|
56
|
+
.replace(/[-\s]+/g, '-');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ===========================================
|
|
60
|
+
// Input Validation & Sanitization Functions
|
|
61
|
+
// ===========================================
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Escape special YAML characters to prevent injection
|
|
65
|
+
* @param {string} str - The string to escape
|
|
66
|
+
* @returns {string} - YAML-safe escaped string
|
|
67
|
+
*/
|
|
68
|
+
function escapeYaml(str) {
|
|
69
|
+
if (typeof str !== 'string') return str;
|
|
70
|
+
|
|
71
|
+
// YAML bare words that would be interpreted as special types
|
|
72
|
+
const yamlKeywords = ['true', 'false', 'yes', 'no', 'on', 'off', 'null', '~', 'y', 'n'];
|
|
73
|
+
if (yamlKeywords.includes(str.toLowerCase())) {
|
|
74
|
+
return `"${str}"`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Numbers that might be parsed as floats/ints
|
|
78
|
+
if (/^[+-]?(\d+\.?\d*|\.\d+)([eE][+-]?\d+)?$/.test(str) && str !== '') {
|
|
79
|
+
return `"${str}"`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// If string contains special YAML characters, wrap in quotes and escape internal quotes
|
|
83
|
+
const needsQuoting = /[:\{\}\[\],&*#?|\-<>=!%@`\n\r\t]/.test(str) ||
|
|
84
|
+
str.startsWith(' ') ||
|
|
85
|
+
str.endsWith(' ') ||
|
|
86
|
+
str.includes('"') ||
|
|
87
|
+
str.includes("'");
|
|
88
|
+
|
|
89
|
+
if (needsQuoting) {
|
|
90
|
+
// Escape backslashes and double quotes, then wrap in double quotes
|
|
91
|
+
return '"' + str.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n').replace(/\r/g, '\\r').replace(/\t/g, '\\t') + '"';
|
|
92
|
+
}
|
|
93
|
+
return str;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Escape characters for JavaScript template literals
|
|
98
|
+
* Prevents code injection when embedding user input in JS templates
|
|
99
|
+
* @param {string} str - The string to escape
|
|
100
|
+
* @returns {string} - JS template literal safe string
|
|
101
|
+
*/
|
|
102
|
+
function escapeJsTemplate(str) {
|
|
103
|
+
if (typeof str !== 'string') return str;
|
|
104
|
+
return str
|
|
105
|
+
.replace(/\\/g, '\\\\') // Escape backslashes first
|
|
106
|
+
.replace(/`/g, '\\`') // Escape backticks
|
|
107
|
+
.replace(/\$/g, '\\$'); // Escape $ to prevent ${} interpolation
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Validate email format (RFC 5322 simplified)
|
|
112
|
+
* @param {string} email - Email to validate
|
|
113
|
+
* @returns {{valid: boolean, error?: string}}
|
|
114
|
+
*/
|
|
115
|
+
function validateEmail(email) {
|
|
116
|
+
if (!email || email.length === 0) {
|
|
117
|
+
return { valid: false, error: 'Email cannot be empty' };
|
|
118
|
+
}
|
|
119
|
+
if (email.length > 254) {
|
|
120
|
+
return { valid: false, error: 'Email is too long (max 254 characters)' };
|
|
121
|
+
}
|
|
122
|
+
// RFC 5322 simplified regex
|
|
123
|
+
const emailRegex = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
|
|
124
|
+
if (!emailRegex.test(email)) {
|
|
125
|
+
return { valid: false, error: 'Invalid email format' };
|
|
126
|
+
}
|
|
127
|
+
return { valid: true };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Validate hex color format
|
|
132
|
+
* @param {string} color - Color to validate (with or without #)
|
|
133
|
+
* @returns {{valid: boolean, error?: string, normalized?: string}}
|
|
134
|
+
*/
|
|
135
|
+
function validateHexColor(color) {
|
|
136
|
+
if (!color || color.length === 0) {
|
|
137
|
+
return { valid: false, error: 'Color cannot be empty' };
|
|
138
|
+
}
|
|
139
|
+
// Remove # if present
|
|
140
|
+
const hex = color.startsWith('#') ? color.slice(1) : color;
|
|
141
|
+
// Check for valid hex format (3 or 6 characters)
|
|
142
|
+
if (!/^[0-9A-Fa-f]{3}$|^[0-9A-Fa-f]{6}$/.test(hex)) {
|
|
143
|
+
return { valid: false, error: 'Invalid hex color format. Use #RGB or #RRGGBB (e.g., #6366f1)' };
|
|
144
|
+
}
|
|
145
|
+
// Normalize to 6-character format with #
|
|
146
|
+
const normalized = hex.length === 3
|
|
147
|
+
? '#' + hex.split('').map(c => c + c).join('')
|
|
148
|
+
: '#' + hex;
|
|
149
|
+
return { valid: true, normalized: normalized.toLowerCase() };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Validate and sanitize display name / description
|
|
154
|
+
* @param {string} str - String to validate
|
|
155
|
+
* @param {string} fieldName - Field name for error messages
|
|
156
|
+
* @param {number} maxLength - Maximum allowed length
|
|
157
|
+
* @returns {{valid: boolean, error?: string, sanitized?: string}}
|
|
158
|
+
*/
|
|
159
|
+
function validateString(str, fieldName, maxLength = 200) {
|
|
160
|
+
if (!str || str.trim().length === 0) {
|
|
161
|
+
return { valid: false, error: `${fieldName} cannot be empty` };
|
|
162
|
+
}
|
|
163
|
+
if (str.length > maxLength) {
|
|
164
|
+
return { valid: false, error: `${fieldName} is too long (max ${maxLength} characters)` };
|
|
165
|
+
}
|
|
166
|
+
// Remove control characters except newlines/tabs in descriptions
|
|
167
|
+
const sanitized = str.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '').trim();
|
|
168
|
+
if (sanitized.length === 0) {
|
|
169
|
+
return { valid: false, error: `${fieldName} contains only invalid characters` };
|
|
170
|
+
}
|
|
171
|
+
return { valid: true, sanitized };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function validatePackageName(slug) {
|
|
175
|
+
// Security: prevent path traversal and invalid names
|
|
176
|
+
if (!slug || slug.length === 0) {
|
|
177
|
+
return { valid: false, error: 'Package name cannot be empty' };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Comprehensive path traversal prevention
|
|
181
|
+
// Check for various path traversal patterns
|
|
182
|
+
const dangerousPatterns = [
|
|
183
|
+
'..', // Parent directory
|
|
184
|
+
'/', // Unix path separator
|
|
185
|
+
'\\', // Windows path separator
|
|
186
|
+
'%2e', // URL encoded .
|
|
187
|
+
'%2f', // URL encoded /
|
|
188
|
+
'%5c', // URL encoded \
|
|
189
|
+
'\x00', // Null byte
|
|
190
|
+
'~', // Home directory shortcut
|
|
191
|
+
];
|
|
192
|
+
|
|
193
|
+
const lowerSlug = slug.toLowerCase();
|
|
194
|
+
for (const pattern of dangerousPatterns) {
|
|
195
|
+
if (lowerSlug.includes(pattern.toLowerCase())) {
|
|
196
|
+
return { valid: false, error: 'Package name contains invalid characters' };
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Must start with alphanumeric
|
|
201
|
+
if (!/^[a-z0-9]/.test(slug)) {
|
|
202
|
+
return { valid: false, error: 'Package name must start with a letter or number' };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Must end with alphanumeric
|
|
206
|
+
if (!/[a-z0-9]$/.test(slug)) {
|
|
207
|
+
return { valid: false, error: 'Package name must end with a letter or number' };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Only allow alphanumeric, hyphens, and underscores
|
|
211
|
+
if (!/^[a-z0-9][a-z0-9_-]*[a-z0-9]$|^[a-z0-9]$/.test(slug)) {
|
|
212
|
+
return { valid: false, error: 'Package name can only contain letters, numbers, hyphens, and underscores' };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (slug.length > 100) {
|
|
216
|
+
return { valid: false, error: 'Package name is too long (max 100 characters)' };
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Reserved names (system directories and common conflicts)
|
|
220
|
+
const reserved = [
|
|
221
|
+
'node_modules', 'package', 'src', 'dist', 'build', 'test', 'tests',
|
|
222
|
+
'lib', 'bin', 'tmp', 'temp', 'cache', 'logs', 'config', 'public',
|
|
223
|
+
'private', 'assets', 'static', 'vendor', 'packages', 'modules',
|
|
224
|
+
'con', 'prn', 'aux', 'nul', // Windows reserved names
|
|
225
|
+
'com1', 'com2', 'com3', 'com4', 'lpt1', 'lpt2', 'lpt3', 'lpt4'
|
|
226
|
+
];
|
|
227
|
+
if (reserved.includes(slug)) {
|
|
228
|
+
return { valid: false, error: `'${slug}' is a reserved name` };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return { valid: true };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Interactive prompts using readline
|
|
235
|
+
function createPrompt() {
|
|
236
|
+
const rl = readline.createInterface({
|
|
237
|
+
input: process.stdin,
|
|
238
|
+
output: process.stdout,
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
let closed = false;
|
|
242
|
+
|
|
243
|
+
// Handle Ctrl+C gracefully
|
|
244
|
+
rl.on('close', () => {
|
|
245
|
+
if (!closed) {
|
|
246
|
+
closed = true;
|
|
247
|
+
console.log('\n\nAborted.');
|
|
248
|
+
process.exit(0);
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
// Also handle SIGINT
|
|
253
|
+
process.on('SIGINT', () => {
|
|
254
|
+
if (!closed) {
|
|
255
|
+
closed = true;
|
|
256
|
+
rl.close();
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
return {
|
|
261
|
+
question: (query, defaultValue) => {
|
|
262
|
+
return new Promise((resolve, reject) => {
|
|
263
|
+
if (closed) {
|
|
264
|
+
reject(new Error('Prompt closed'));
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const prompt = defaultValue
|
|
269
|
+
? `${c.cyan}?${c.reset} ${query} ${c.dim}[${defaultValue}]${c.reset}: `
|
|
270
|
+
: `${c.cyan}?${c.reset} ${query}: `;
|
|
271
|
+
|
|
272
|
+
rl.question(prompt, (answer) => {
|
|
273
|
+
resolve(answer.trim() || defaultValue || '');
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
},
|
|
277
|
+
select: (query, choices, defaultIndex = 0) => {
|
|
278
|
+
return new Promise((resolve, reject) => {
|
|
279
|
+
if (closed) {
|
|
280
|
+
reject(new Error('Prompt closed'));
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
console.log(`\n${c.cyan}?${c.reset} ${query}`);
|
|
285
|
+
choices.forEach((choice, i) => {
|
|
286
|
+
const marker = i === defaultIndex ? `${c.green}❯${c.reset}` : ' ';
|
|
287
|
+
console.log(` ${marker} ${i + 1}. ${c.bold}${choice.name}${c.reset} - ${c.yellow}${choice.description}${c.reset}`);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
rl.question(`\n Enter choice [1-${choices.length}] ${c.dim}(default: ${defaultIndex + 1})${c.reset}: `, (answer) => {
|
|
291
|
+
const idx = answer.trim() ? parseInt(answer) - 1 : defaultIndex;
|
|
292
|
+
if (idx >= 0 && idx < choices.length) {
|
|
293
|
+
resolve(choices[idx].value);
|
|
294
|
+
} else {
|
|
295
|
+
resolve(choices[defaultIndex].value);
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
},
|
|
300
|
+
close: () => {
|
|
301
|
+
closed = true;
|
|
302
|
+
rl.close();
|
|
303
|
+
},
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Template content generators
|
|
308
|
+
const templates = {
|
|
309
|
+
// promptline.yaml template
|
|
310
|
+
config: (config) => `# PromptLine Package Configuration
|
|
311
|
+
# Created with: npx create-promptline-app ${escapeYaml(config.slug)}
|
|
312
|
+
# Docs: https://docs.promptlineops.com/packages
|
|
313
|
+
|
|
314
|
+
name: ${escapeYaml(config.displayName)}
|
|
315
|
+
version: "1.0.0"
|
|
316
|
+
description: ${escapeYaml(config.description)}
|
|
317
|
+
|
|
318
|
+
# AI Sources - Connect your prompts and agents
|
|
319
|
+
# Configure these in PromptLine Creator Studio after upload
|
|
320
|
+
ai_sources:
|
|
321
|
+
main:
|
|
322
|
+
type: prompt
|
|
323
|
+
source_id: null # Set in Creator Studio
|
|
324
|
+
role: main
|
|
325
|
+
description: "Main AI prompt for this application"
|
|
326
|
+
|
|
327
|
+
# Data Collections - Your app's data models
|
|
328
|
+
collections:
|
|
329
|
+
submissions:
|
|
330
|
+
display_name: "Submissions"
|
|
331
|
+
description: "Form submissions and user data"
|
|
332
|
+
fields:
|
|
333
|
+
- name: name
|
|
334
|
+
type: string
|
|
335
|
+
label: "Name"
|
|
336
|
+
required: true
|
|
337
|
+
- name: email
|
|
338
|
+
type: email
|
|
339
|
+
label: "Email"
|
|
340
|
+
required: true
|
|
341
|
+
- name: message
|
|
342
|
+
type: text
|
|
343
|
+
label: "Message"
|
|
344
|
+
required: false
|
|
345
|
+
- name: created_at
|
|
346
|
+
type: date
|
|
347
|
+
label: "Created"
|
|
348
|
+
auto: true
|
|
349
|
+
settings:
|
|
350
|
+
public_read: false
|
|
351
|
+
public_write: true
|
|
352
|
+
|
|
353
|
+
# Instance Variables - Configurable per deployment
|
|
354
|
+
variables:
|
|
355
|
+
app_name:
|
|
356
|
+
type: string
|
|
357
|
+
label: "Application Name"
|
|
358
|
+
required: true
|
|
359
|
+
default: ${escapeYaml(config.displayName)}
|
|
360
|
+
|
|
361
|
+
primary_color:
|
|
362
|
+
type: color
|
|
363
|
+
label: "Brand Color"
|
|
364
|
+
default: ${escapeYaml(config.primaryColor)}
|
|
365
|
+
|
|
366
|
+
contact_email:
|
|
367
|
+
type: email
|
|
368
|
+
label: "Contact Email"
|
|
369
|
+
required: true
|
|
370
|
+
default: ${escapeYaml(config.contactEmail)}
|
|
371
|
+
|
|
372
|
+
# Branding defaults
|
|
373
|
+
branding:
|
|
374
|
+
colors:
|
|
375
|
+
primary: "{{variables.primary_color}}"
|
|
376
|
+
secondary: "#10b981"
|
|
377
|
+
typography:
|
|
378
|
+
font: "Inter"
|
|
379
|
+
|
|
380
|
+
# Automated actions
|
|
381
|
+
actions:
|
|
382
|
+
notify_on_submit:
|
|
383
|
+
type: email
|
|
384
|
+
trigger:
|
|
385
|
+
event: on_data_create
|
|
386
|
+
collection: submissions
|
|
387
|
+
config:
|
|
388
|
+
to: "{{variables.contact_email}}"
|
|
389
|
+
subject: "New submission - {{variables.app_name}}"
|
|
390
|
+
body: |
|
|
391
|
+
New submission received:
|
|
392
|
+
Name: {{data.name}}
|
|
393
|
+
Email: {{data.email}}
|
|
394
|
+
Message: {{data.message}}
|
|
395
|
+
`,
|
|
396
|
+
|
|
397
|
+
// Public index page
|
|
398
|
+
publicIndex: (config) => `/**
|
|
399
|
+
* ${escapeJsTemplate(config.displayName)} - Landing Page
|
|
400
|
+
*
|
|
401
|
+
* Public page accessible without authentication.
|
|
402
|
+
* Edit this file to customize your landing page.
|
|
403
|
+
*/
|
|
404
|
+
import React, { useState } from 'react'
|
|
405
|
+
import { usePromptLine } from '@promptline/sdk'
|
|
406
|
+
|
|
407
|
+
export default function LandingPage() {
|
|
408
|
+
const { config, submitForm, isLoading } = usePromptLine()
|
|
409
|
+
|
|
410
|
+
const [formData, setFormData] = useState({
|
|
411
|
+
name: '',
|
|
412
|
+
email: '',
|
|
413
|
+
message: ''
|
|
414
|
+
})
|
|
415
|
+
const [submitted, setSubmitted] = useState(false)
|
|
416
|
+
const [error, setError] = useState(null)
|
|
417
|
+
|
|
418
|
+
const handleSubmit = async (e) => {
|
|
419
|
+
e.preventDefault()
|
|
420
|
+
setError(null)
|
|
421
|
+
|
|
422
|
+
try {
|
|
423
|
+
await submitForm('submissions', formData)
|
|
424
|
+
setSubmitted(true)
|
|
425
|
+
setFormData({ name: '', email: '', message: '' })
|
|
426
|
+
} catch (err) {
|
|
427
|
+
setError(err.message || 'Something went wrong')
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const handleChange = (e) => {
|
|
432
|
+
setFormData(prev => ({
|
|
433
|
+
...prev,
|
|
434
|
+
[e.target.name]: e.target.value
|
|
435
|
+
}))
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
return (
|
|
439
|
+
<div className="min-h-screen bg-gradient-to-b from-gray-50 to-white">
|
|
440
|
+
{/* Header */}
|
|
441
|
+
<header className="border-b bg-white/80 backdrop-blur-sm sticky top-0 z-10">
|
|
442
|
+
<div className="max-w-5xl mx-auto px-4 py-4 flex items-center justify-between">
|
|
443
|
+
<h1
|
|
444
|
+
className="text-2xl font-bold"
|
|
445
|
+
style={{ color: config.primary_color }}
|
|
446
|
+
>
|
|
447
|
+
{config.app_name}
|
|
448
|
+
</h1>
|
|
449
|
+
<nav className="flex gap-6">
|
|
450
|
+
<a href="#features" className="text-gray-600 hover:text-gray-900">Features</a>
|
|
451
|
+
<a href="#contact" className="text-gray-600 hover:text-gray-900">Contact</a>
|
|
452
|
+
</nav>
|
|
453
|
+
</div>
|
|
454
|
+
</header>
|
|
455
|
+
|
|
456
|
+
{/* Hero Section */}
|
|
457
|
+
<section className="max-w-5xl mx-auto px-4 py-20 text-center">
|
|
458
|
+
<h2 className="text-5xl font-bold text-gray-900 mb-6">
|
|
459
|
+
Welcome to {config.app_name}
|
|
460
|
+
</h2>
|
|
461
|
+
<p className="text-xl text-gray-600 max-w-2xl mx-auto mb-8">
|
|
462
|
+
${escapeJsTemplate(config.description) || 'Your AI-powered application built with PromptLine.'}
|
|
463
|
+
</p>
|
|
464
|
+
<a
|
|
465
|
+
href="#contact"
|
|
466
|
+
className="inline-block px-8 py-3 text-white font-medium rounded-lg transition-transform hover:scale-105"
|
|
467
|
+
style={{ backgroundColor: config.primary_color }}
|
|
468
|
+
>
|
|
469
|
+
Get Started
|
|
470
|
+
</a>
|
|
471
|
+
</section>
|
|
472
|
+
|
|
473
|
+
{/* Features Section */}
|
|
474
|
+
<section id="features" className="max-w-5xl mx-auto px-4 py-16">
|
|
475
|
+
<h3 className="text-3xl font-bold text-center text-gray-900 mb-12">Features</h3>
|
|
476
|
+
<div className="grid md:grid-cols-3 gap-8">
|
|
477
|
+
{[
|
|
478
|
+
{ title: 'AI-Powered', desc: 'Leverage cutting-edge AI for intelligent responses' },
|
|
479
|
+
{ title: 'Easy to Use', desc: 'Simple interface designed for everyone' },
|
|
480
|
+
{ title: 'Customizable', desc: 'Adapt to your specific needs and branding' }
|
|
481
|
+
].map((feature, i) => (
|
|
482
|
+
<div key={i} className="bg-white p-6 rounded-xl shadow-sm border">
|
|
483
|
+
<div
|
|
484
|
+
className="w-12 h-12 rounded-lg mb-4 flex items-center justify-center text-white text-xl font-bold"
|
|
485
|
+
style={{ backgroundColor: config.primary_color }}
|
|
486
|
+
>
|
|
487
|
+
{i + 1}
|
|
488
|
+
</div>
|
|
489
|
+
<h4 className="text-lg font-semibold text-gray-900 mb-2">{feature.title}</h4>
|
|
490
|
+
<p className="text-gray-600">{feature.desc}</p>
|
|
491
|
+
</div>
|
|
492
|
+
))}
|
|
493
|
+
</div>
|
|
494
|
+
</section>
|
|
495
|
+
|
|
496
|
+
{/* Contact Form */}
|
|
497
|
+
<section id="contact" className="max-w-lg mx-auto px-4 py-16">
|
|
498
|
+
<div className="bg-white rounded-2xl shadow-lg p-8">
|
|
499
|
+
<h3 className="text-2xl font-bold text-gray-900 mb-6 text-center">
|
|
500
|
+
Get in Touch
|
|
501
|
+
</h3>
|
|
502
|
+
|
|
503
|
+
{submitted ? (
|
|
504
|
+
<div className="text-center py-8">
|
|
505
|
+
<div
|
|
506
|
+
className="w-16 h-16 rounded-full mx-auto mb-4 flex items-center justify-center"
|
|
507
|
+
style={{ backgroundColor: config.primary_color + '20' }}
|
|
508
|
+
>
|
|
509
|
+
<svg className="w-8 h-8" style={{ color: config.primary_color }} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
510
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
|
511
|
+
</svg>
|
|
512
|
+
</div>
|
|
513
|
+
<h4 className="text-xl font-medium text-gray-900 mb-2">Thank you!</h4>
|
|
514
|
+
<p className="text-gray-600 mb-4">We'll get back to you soon.</p>
|
|
515
|
+
<button
|
|
516
|
+
onClick={() => setSubmitted(false)}
|
|
517
|
+
className="text-sm underline"
|
|
518
|
+
style={{ color: config.primary_color }}
|
|
519
|
+
>
|
|
520
|
+
Send another message
|
|
521
|
+
</button>
|
|
522
|
+
</div>
|
|
523
|
+
) : (
|
|
524
|
+
<form onSubmit={handleSubmit} className="space-y-5">
|
|
525
|
+
{error && (
|
|
526
|
+
<div className="p-4 bg-red-50 text-red-700 rounded-lg text-sm">
|
|
527
|
+
{error}
|
|
528
|
+
</div>
|
|
529
|
+
)}
|
|
530
|
+
|
|
531
|
+
<div>
|
|
532
|
+
<label htmlFor="name" className="block text-sm font-medium text-gray-700 mb-1">
|
|
533
|
+
Name *
|
|
534
|
+
</label>
|
|
535
|
+
<input
|
|
536
|
+
type="text"
|
|
537
|
+
id="name"
|
|
538
|
+
name="name"
|
|
539
|
+
required
|
|
540
|
+
value={formData.name}
|
|
541
|
+
onChange={handleChange}
|
|
542
|
+
className="w-full px-4 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-offset-0 focus:border-transparent"
|
|
543
|
+
style={{ '--tw-ring-color': config.primary_color }}
|
|
544
|
+
placeholder="Your name"
|
|
545
|
+
/>
|
|
546
|
+
</div>
|
|
547
|
+
|
|
548
|
+
<div>
|
|
549
|
+
<label htmlFor="email" className="block text-sm font-medium text-gray-700 mb-1">
|
|
550
|
+
Email *
|
|
551
|
+
</label>
|
|
552
|
+
<input
|
|
553
|
+
type="email"
|
|
554
|
+
id="email"
|
|
555
|
+
name="email"
|
|
556
|
+
required
|
|
557
|
+
value={formData.email}
|
|
558
|
+
onChange={handleChange}
|
|
559
|
+
className="w-full px-4 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-offset-0 focus:border-transparent"
|
|
560
|
+
placeholder="you@example.com"
|
|
561
|
+
/>
|
|
562
|
+
</div>
|
|
563
|
+
|
|
564
|
+
<div>
|
|
565
|
+
<label htmlFor="message" className="block text-sm font-medium text-gray-700 mb-1">
|
|
566
|
+
Message
|
|
567
|
+
</label>
|
|
568
|
+
<textarea
|
|
569
|
+
id="message"
|
|
570
|
+
name="message"
|
|
571
|
+
rows={4}
|
|
572
|
+
value={formData.message}
|
|
573
|
+
onChange={handleChange}
|
|
574
|
+
className="w-full px-4 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-offset-0 focus:border-transparent resize-none"
|
|
575
|
+
placeholder="How can we help?"
|
|
576
|
+
/>
|
|
577
|
+
</div>
|
|
578
|
+
|
|
579
|
+
<button
|
|
580
|
+
type="submit"
|
|
581
|
+
disabled={isLoading}
|
|
582
|
+
className="w-full py-3 px-4 text-white font-medium rounded-lg transition-all disabled:opacity-50 hover:opacity-90"
|
|
583
|
+
style={{ backgroundColor: config.primary_color }}
|
|
584
|
+
>
|
|
585
|
+
{isLoading ? 'Sending...' : 'Send Message'}
|
|
586
|
+
</button>
|
|
587
|
+
</form>
|
|
588
|
+
)}
|
|
589
|
+
</div>
|
|
590
|
+
</section>
|
|
591
|
+
|
|
592
|
+
{/* Footer */}
|
|
593
|
+
<footer className="border-t bg-gray-50">
|
|
594
|
+
<div className="max-w-5xl mx-auto px-4 py-8 text-center text-sm text-gray-500">
|
|
595
|
+
© {new Date().getFullYear()} {config.app_name}. Powered by PromptLine.
|
|
596
|
+
</div>
|
|
597
|
+
</footer>
|
|
598
|
+
</div>
|
|
599
|
+
)
|
|
600
|
+
}
|
|
601
|
+
`,
|
|
602
|
+
|
|
603
|
+
// Private dashboard page
|
|
604
|
+
privateDashboard: (config) => `/**
|
|
605
|
+
* ${escapeJsTemplate(config.displayName)} - Dashboard
|
|
606
|
+
*
|
|
607
|
+
* Private page requiring authentication.
|
|
608
|
+
* Users must be logged in to access this page.
|
|
609
|
+
*/
|
|
610
|
+
import React, { useEffect, useState } from 'react'
|
|
611
|
+
import { usePromptLine, Link } from '@promptline/sdk'
|
|
612
|
+
|
|
613
|
+
export default function DashboardPage() {
|
|
614
|
+
const { config, fetchCollection, user, logout } = usePromptLine()
|
|
615
|
+
const [submissions, setSubmissions] = useState([])
|
|
616
|
+
const [stats, setStats] = useState({ total: 0, today: 0, week: 0 })
|
|
617
|
+
const [loading, setLoading] = useState(true)
|
|
618
|
+
|
|
619
|
+
useEffect(() => {
|
|
620
|
+
loadData()
|
|
621
|
+
}, [])
|
|
622
|
+
|
|
623
|
+
const loadData = async () => {
|
|
624
|
+
try {
|
|
625
|
+
const data = await fetchCollection('submissions', {
|
|
626
|
+
orderBy: 'created_at',
|
|
627
|
+
order: 'desc',
|
|
628
|
+
limit: 10
|
|
629
|
+
})
|
|
630
|
+
setSubmissions(data.items)
|
|
631
|
+
|
|
632
|
+
// Calculate stats
|
|
633
|
+
const now = new Date()
|
|
634
|
+
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate())
|
|
635
|
+
const weekAgo = new Date(today.getTime() - 7 * 24 * 60 * 60 * 1000)
|
|
636
|
+
|
|
637
|
+
setStats({
|
|
638
|
+
total: data.total,
|
|
639
|
+
today: data.items.filter(s => new Date(s.created_at) >= today).length,
|
|
640
|
+
week: data.items.filter(s => new Date(s.created_at) >= weekAgo).length
|
|
641
|
+
})
|
|
642
|
+
} catch (err) {
|
|
643
|
+
console.error('Failed to load data:', err)
|
|
644
|
+
} finally {
|
|
645
|
+
setLoading(false)
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
return (
|
|
650
|
+
<div className="min-h-screen bg-gray-100">
|
|
651
|
+
{/* Header */}
|
|
652
|
+
<header className="bg-white shadow-sm">
|
|
653
|
+
<div className="max-w-7xl mx-auto px-4 py-4 flex items-center justify-between">
|
|
654
|
+
<div className="flex items-center gap-8">
|
|
655
|
+
<h1 className="text-xl font-bold" style={{ color: config.primary_color }}>
|
|
656
|
+
{config.app_name}
|
|
657
|
+
</h1>
|
|
658
|
+
<nav className="flex gap-4">
|
|
659
|
+
<Link to="/dashboard" className="font-medium px-3 py-2 rounded-lg" style={{ backgroundColor: config.primary_color + '10', color: config.primary_color }}>
|
|
660
|
+
Dashboard
|
|
661
|
+
</Link>
|
|
662
|
+
<Link to="/settings" className="text-gray-600 hover:text-gray-900 px-3 py-2">
|
|
663
|
+
Settings
|
|
664
|
+
</Link>
|
|
665
|
+
</nav>
|
|
666
|
+
</div>
|
|
667
|
+
<div className="flex items-center gap-4">
|
|
668
|
+
<span className="text-sm text-gray-600">{user?.email}</span>
|
|
669
|
+
<button onClick={logout} className="text-sm text-gray-500 hover:text-gray-700">
|
|
670
|
+
Logout
|
|
671
|
+
</button>
|
|
672
|
+
</div>
|
|
673
|
+
</div>
|
|
674
|
+
</header>
|
|
675
|
+
|
|
676
|
+
{/* Main Content */}
|
|
677
|
+
<main className="max-w-7xl mx-auto px-4 py-8">
|
|
678
|
+
<h2 className="text-2xl font-bold text-gray-900 mb-6">Dashboard</h2>
|
|
679
|
+
|
|
680
|
+
{/* Stats Cards */}
|
|
681
|
+
<div className="grid md:grid-cols-3 gap-6 mb-8">
|
|
682
|
+
<div className="bg-white rounded-xl p-6 shadow-sm">
|
|
683
|
+
<p className="text-sm text-gray-500 uppercase tracking-wide">Total Submissions</p>
|
|
684
|
+
<p className="text-3xl font-bold mt-2" style={{ color: config.primary_color }}>
|
|
685
|
+
{loading ? '...' : stats.total}
|
|
686
|
+
</p>
|
|
687
|
+
</div>
|
|
688
|
+
<div className="bg-white rounded-xl p-6 shadow-sm">
|
|
689
|
+
<p className="text-sm text-gray-500 uppercase tracking-wide">Today</p>
|
|
690
|
+
<p className="text-3xl font-bold mt-2 text-green-600">
|
|
691
|
+
{loading ? '...' : stats.today}
|
|
692
|
+
</p>
|
|
693
|
+
</div>
|
|
694
|
+
<div className="bg-white rounded-xl p-6 shadow-sm">
|
|
695
|
+
<p className="text-sm text-gray-500 uppercase tracking-wide">This Week</p>
|
|
696
|
+
<p className="text-3xl font-bold mt-2 text-blue-600">
|
|
697
|
+
{loading ? '...' : stats.week}
|
|
698
|
+
</p>
|
|
699
|
+
</div>
|
|
700
|
+
</div>
|
|
701
|
+
|
|
702
|
+
{/* Recent Submissions */}
|
|
703
|
+
<div className="bg-white rounded-xl shadow-sm overflow-hidden">
|
|
704
|
+
<div className="px-6 py-4 border-b">
|
|
705
|
+
<h3 className="font-semibold text-gray-900">Recent Submissions</h3>
|
|
706
|
+
</div>
|
|
707
|
+
|
|
708
|
+
{loading ? (
|
|
709
|
+
<div className="p-8 text-center text-gray-500">Loading...</div>
|
|
710
|
+
) : submissions.length === 0 ? (
|
|
711
|
+
<div className="p-8 text-center text-gray-500">
|
|
712
|
+
<p>No submissions yet</p>
|
|
713
|
+
<p className="text-sm mt-1">New submissions will appear here</p>
|
|
714
|
+
</div>
|
|
715
|
+
) : (
|
|
716
|
+
<table className="w-full">
|
|
717
|
+
<thead className="bg-gray-50">
|
|
718
|
+
<tr>
|
|
719
|
+
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Name</th>
|
|
720
|
+
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Email</th>
|
|
721
|
+
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Message</th>
|
|
722
|
+
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Date</th>
|
|
723
|
+
</tr>
|
|
724
|
+
</thead>
|
|
725
|
+
<tbody className="divide-y divide-gray-200">
|
|
726
|
+
{submissions.map((sub) => (
|
|
727
|
+
<tr key={sub.id} className="hover:bg-gray-50">
|
|
728
|
+
<td className="px-6 py-4 text-sm font-medium text-gray-900">{sub.name}</td>
|
|
729
|
+
<td className="px-6 py-4 text-sm text-gray-600">{sub.email}</td>
|
|
730
|
+
<td className="px-6 py-4 text-sm text-gray-600 max-w-xs truncate">{sub.message || '-'}</td>
|
|
731
|
+
<td className="px-6 py-4 text-sm text-gray-500">
|
|
732
|
+
{new Date(sub.created_at).toLocaleDateString()}
|
|
733
|
+
</td>
|
|
734
|
+
</tr>
|
|
735
|
+
))}
|
|
736
|
+
</tbody>
|
|
737
|
+
</table>
|
|
738
|
+
)}
|
|
739
|
+
</div>
|
|
740
|
+
</main>
|
|
741
|
+
</div>
|
|
742
|
+
)
|
|
743
|
+
}
|
|
744
|
+
`,
|
|
745
|
+
|
|
746
|
+
// Blank template - minimal
|
|
747
|
+
blankIndex: (config) => `/**
|
|
748
|
+
* ${escapeJsTemplate(config.displayName)}
|
|
749
|
+
*
|
|
750
|
+
* Your PromptLine app starts here.
|
|
751
|
+
* Edit this file to build your application.
|
|
752
|
+
*/
|
|
753
|
+
import React from 'react'
|
|
754
|
+
import { usePromptLine } from '@promptline/sdk'
|
|
755
|
+
|
|
756
|
+
export default function HomePage() {
|
|
757
|
+
const { config, callAI, isLoading } = usePromptLine()
|
|
758
|
+
|
|
759
|
+
return (
|
|
760
|
+
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-gray-50 to-gray-100">
|
|
761
|
+
<div className="text-center">
|
|
762
|
+
<h1
|
|
763
|
+
className="text-5xl font-bold mb-4"
|
|
764
|
+
style={{ color: config.primary_color }}
|
|
765
|
+
>
|
|
766
|
+
{config.app_name}
|
|
767
|
+
</h1>
|
|
768
|
+
<p className="text-xl text-gray-600 mb-8">
|
|
769
|
+
Your PromptLine app is ready!
|
|
770
|
+
</p>
|
|
771
|
+
<div className="space-x-4">
|
|
772
|
+
<a
|
|
773
|
+
href="https://docs.promptlineops.com"
|
|
774
|
+
target="_blank"
|
|
775
|
+
rel="noopener noreferrer"
|
|
776
|
+
className="inline-block px-6 py-2 border-2 rounded-lg font-medium transition-colors hover:bg-gray-50"
|
|
777
|
+
style={{ borderColor: config.primary_color, color: config.primary_color }}
|
|
778
|
+
>
|
|
779
|
+
Read the Docs
|
|
780
|
+
</a>
|
|
781
|
+
</div>
|
|
782
|
+
</div>
|
|
783
|
+
</div>
|
|
784
|
+
)
|
|
785
|
+
}
|
|
786
|
+
`,
|
|
787
|
+
|
|
788
|
+
// Backend API template
|
|
789
|
+
backendApi: (config) => `"""
|
|
790
|
+
${escapeJsTemplate(config.displayName)} - Custom API Endpoints
|
|
791
|
+
|
|
792
|
+
Add your custom FastAPI routes here.
|
|
793
|
+
These will be available at /api/custom/<endpoint>
|
|
794
|
+
|
|
795
|
+
Documentation: https://docs.promptlineops.com/packages/backend
|
|
796
|
+
"""
|
|
797
|
+
|
|
798
|
+
from fastapi import APIRouter, Depends, HTTPException
|
|
799
|
+
from pydantic import BaseModel
|
|
800
|
+
from typing import Optional
|
|
801
|
+
from promptline import (
|
|
802
|
+
get_instance_config,
|
|
803
|
+
get_collection,
|
|
804
|
+
call_ai_source,
|
|
805
|
+
get_current_user,
|
|
806
|
+
require_auth
|
|
807
|
+
)
|
|
808
|
+
|
|
809
|
+
router = APIRouter()
|
|
810
|
+
|
|
811
|
+
|
|
812
|
+
class AnalyzeRequest(BaseModel):
|
|
813
|
+
text: str
|
|
814
|
+
options: Optional[dict] = None
|
|
815
|
+
|
|
816
|
+
|
|
817
|
+
@router.get("/health")
|
|
818
|
+
async def health_check():
|
|
819
|
+
"""Health check endpoint"""
|
|
820
|
+
config = await get_instance_config()
|
|
821
|
+
return {
|
|
822
|
+
"status": "healthy",
|
|
823
|
+
"app": config.get("app_name", "Unknown")
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
|
|
827
|
+
@router.post("/analyze")
|
|
828
|
+
async def analyze_text(request: AnalyzeRequest):
|
|
829
|
+
"""
|
|
830
|
+
Analyze text using the connected AI source.
|
|
831
|
+
|
|
832
|
+
This is a public endpoint - no authentication required.
|
|
833
|
+
"""
|
|
834
|
+
if not request.text.strip():
|
|
835
|
+
raise HTTPException(status_code=400, detail="Text is required")
|
|
836
|
+
|
|
837
|
+
try:
|
|
838
|
+
result = await call_ai_source("main", {
|
|
839
|
+
"text": request.text,
|
|
840
|
+
"options": request.options or {}
|
|
841
|
+
})
|
|
842
|
+
return {"success": True, "result": result}
|
|
843
|
+
except Exception as e:
|
|
844
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
845
|
+
|
|
846
|
+
|
|
847
|
+
@router.get("/stats", dependencies=[Depends(require_auth)])
|
|
848
|
+
async def get_stats():
|
|
849
|
+
"""
|
|
850
|
+
Get submission statistics.
|
|
851
|
+
|
|
852
|
+
Requires authentication.
|
|
853
|
+
"""
|
|
854
|
+
collection = await get_collection("submissions")
|
|
855
|
+
items = await collection.find({})
|
|
856
|
+
|
|
857
|
+
return {
|
|
858
|
+
"total": len(items),
|
|
859
|
+
"latest": items[0] if items else None
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
|
|
863
|
+
@router.get("/me", dependencies=[Depends(require_auth)])
|
|
864
|
+
async def get_current_user_info():
|
|
865
|
+
"""Get current authenticated user info"""
|
|
866
|
+
user = await get_current_user()
|
|
867
|
+
return {
|
|
868
|
+
"id": user.id,
|
|
869
|
+
"email": user.email,
|
|
870
|
+
"name": user.name
|
|
871
|
+
}
|
|
872
|
+
`,
|
|
873
|
+
|
|
874
|
+
// Settings page
|
|
875
|
+
privateSettings: (config) => `/**
|
|
876
|
+
* ${escapeJsTemplate(config.displayName)} - Settings
|
|
877
|
+
*
|
|
878
|
+
* User settings and preferences.
|
|
879
|
+
*/
|
|
880
|
+
import React from 'react'
|
|
881
|
+
import { usePromptLine, Link } from '@promptline/sdk'
|
|
882
|
+
|
|
883
|
+
export default function SettingsPage() {
|
|
884
|
+
const { config, user } = usePromptLine()
|
|
885
|
+
|
|
886
|
+
return (
|
|
887
|
+
<div className="min-h-screen bg-gray-100">
|
|
888
|
+
<header className="bg-white shadow-sm">
|
|
889
|
+
<div className="max-w-7xl mx-auto px-4 py-4 flex items-center gap-8">
|
|
890
|
+
<h1 className="text-xl font-bold" style={{ color: config.primary_color }}>
|
|
891
|
+
{config.app_name}
|
|
892
|
+
</h1>
|
|
893
|
+
<nav className="flex gap-4">
|
|
894
|
+
<Link to="/dashboard" className="text-gray-600 hover:text-gray-900 px-3 py-2">
|
|
895
|
+
Dashboard
|
|
896
|
+
</Link>
|
|
897
|
+
<Link to="/settings" className="font-medium px-3 py-2 rounded-lg" style={{ backgroundColor: config.primary_color + '10', color: config.primary_color }}>
|
|
898
|
+
Settings
|
|
899
|
+
</Link>
|
|
900
|
+
</nav>
|
|
901
|
+
</div>
|
|
902
|
+
</header>
|
|
903
|
+
|
|
904
|
+
<main className="max-w-3xl mx-auto px-4 py-8">
|
|
905
|
+
<h2 className="text-2xl font-bold text-gray-900 mb-6">Settings</h2>
|
|
906
|
+
|
|
907
|
+
<div className="bg-white rounded-xl shadow-sm divide-y">
|
|
908
|
+
<div className="p-6">
|
|
909
|
+
<h3 className="font-medium text-gray-900 mb-4">Profile</h3>
|
|
910
|
+
<div className="space-y-4">
|
|
911
|
+
<div>
|
|
912
|
+
<label className="block text-sm text-gray-500 mb-1">Email</label>
|
|
913
|
+
<p className="text-gray-900">{user?.email}</p>
|
|
914
|
+
</div>
|
|
915
|
+
<div>
|
|
916
|
+
<label className="block text-sm text-gray-500 mb-1">Name</label>
|
|
917
|
+
<p className="text-gray-900">{user?.name || 'Not set'}</p>
|
|
918
|
+
</div>
|
|
919
|
+
</div>
|
|
920
|
+
</div>
|
|
921
|
+
|
|
922
|
+
<div className="p-6">
|
|
923
|
+
<h3 className="font-medium text-gray-900 mb-4">Preferences</h3>
|
|
924
|
+
<p className="text-sm text-gray-500">
|
|
925
|
+
Additional settings coming soon.
|
|
926
|
+
</p>
|
|
927
|
+
</div>
|
|
928
|
+
</div>
|
|
929
|
+
</main>
|
|
930
|
+
</div>
|
|
931
|
+
)
|
|
932
|
+
}
|
|
933
|
+
`,
|
|
934
|
+
|
|
935
|
+
// README
|
|
936
|
+
readme: (config) => `# ${config.displayName}
|
|
937
|
+
|
|
938
|
+
${config.description}
|
|
939
|
+
|
|
940
|
+
## Getting Started
|
|
941
|
+
|
|
942
|
+
This is a PromptLine package created with \`create-promptline-app\`.
|
|
943
|
+
|
|
944
|
+
### Project Structure
|
|
945
|
+
|
|
946
|
+
\`\`\`
|
|
947
|
+
${config.slug}/
|
|
948
|
+
├── public/ # Public pages (no auth required)
|
|
949
|
+
│ └── index.tsx # Landing page
|
|
950
|
+
├── private/ # Protected pages (auth required)
|
|
951
|
+
│ ├── dashboard.tsx # Main dashboard
|
|
952
|
+
│ └── settings.tsx # User settings
|
|
953
|
+
├── backend/ # Custom API endpoints
|
|
954
|
+
│ └── api.py # FastAPI routes
|
|
955
|
+
├── assets/ # Static files
|
|
956
|
+
├── promptline.yaml # Package configuration
|
|
957
|
+
└── README.md
|
|
958
|
+
\`\`\`
|
|
959
|
+
|
|
960
|
+
### Development
|
|
961
|
+
|
|
962
|
+
1. Edit your pages in \`public/\` and \`private/\`
|
|
963
|
+
2. Add custom API endpoints in \`backend/api.py\`
|
|
964
|
+
3. Configure AI sources in \`promptline.yaml\`
|
|
965
|
+
|
|
966
|
+
### Deployment
|
|
967
|
+
|
|
968
|
+
1. Create a ZIP of this directory
|
|
969
|
+
2. Upload to [PromptLine Creator Studio](https://app.promptlineops.com/apps/creator)
|
|
970
|
+
3. Connect your AI sources
|
|
971
|
+
4. Configure instance variables
|
|
972
|
+
5. Publish!
|
|
973
|
+
|
|
974
|
+
### SDK Usage
|
|
975
|
+
|
|
976
|
+
\`\`\`tsx
|
|
977
|
+
import { usePromptLine } from '@promptline/sdk'
|
|
978
|
+
|
|
979
|
+
function MyComponent() {
|
|
980
|
+
const {
|
|
981
|
+
config, // Instance variables (app_name, primary_color, etc.)
|
|
982
|
+
submitForm, // Submit data to a collection
|
|
983
|
+
fetchCollection, // Fetch collection data
|
|
984
|
+
callAI, // Call connected AI source
|
|
985
|
+
user, // Current user (private pages only)
|
|
986
|
+
isLoading
|
|
987
|
+
} = usePromptLine()
|
|
988
|
+
|
|
989
|
+
// Example: Call AI
|
|
990
|
+
const result = await callAI('main', { text: 'Hello!' })
|
|
991
|
+
|
|
992
|
+
// Example: Submit form
|
|
993
|
+
await submitForm('submissions', { name: 'John', email: 'john@example.com' })
|
|
994
|
+
}
|
|
995
|
+
\`\`\`
|
|
996
|
+
|
|
997
|
+
## Documentation
|
|
998
|
+
|
|
999
|
+
- [PromptLine Docs](https://docs.promptlineops.com)
|
|
1000
|
+
- [Package Development](https://docs.promptlineops.com/packages)
|
|
1001
|
+
- [SDK Reference](https://docs.promptlineops.com/sdk)
|
|
1002
|
+
|
|
1003
|
+
---
|
|
1004
|
+
|
|
1005
|
+
Created with [create-promptline-app](https://npmjs.com/package/create-promptline-app)
|
|
1006
|
+
`,
|
|
1007
|
+
|
|
1008
|
+
// .gitignore
|
|
1009
|
+
gitignore: () => `# Dependencies
|
|
1010
|
+
node_modules/
|
|
1011
|
+
|
|
1012
|
+
# Dev server
|
|
1013
|
+
.promptline-dev/
|
|
1014
|
+
|
|
1015
|
+
# Build
|
|
1016
|
+
dist/
|
|
1017
|
+
build/
|
|
1018
|
+
.next/
|
|
1019
|
+
|
|
1020
|
+
# Environment
|
|
1021
|
+
.env
|
|
1022
|
+
.env.local
|
|
1023
|
+
.env.*
|
|
1024
|
+
|
|
1025
|
+
# IDE
|
|
1026
|
+
.idea/
|
|
1027
|
+
.vscode/
|
|
1028
|
+
*.swp
|
|
1029
|
+
|
|
1030
|
+
# OS
|
|
1031
|
+
.DS_Store
|
|
1032
|
+
Thumbs.db
|
|
1033
|
+
|
|
1034
|
+
# Logs
|
|
1035
|
+
*.log
|
|
1036
|
+
|
|
1037
|
+
# Package archives
|
|
1038
|
+
*.zip
|
|
1039
|
+
*.tar.gz
|
|
1040
|
+
`,
|
|
1041
|
+
|
|
1042
|
+
// package.json for local dev
|
|
1043
|
+
packageJson: (config) => JSON.stringify({
|
|
1044
|
+
"name": config.slug,
|
|
1045
|
+
"version": "1.0.0",
|
|
1046
|
+
"private": true,
|
|
1047
|
+
"type": "module",
|
|
1048
|
+
"scripts": {
|
|
1049
|
+
"dev": "vite",
|
|
1050
|
+
"build": "vite build",
|
|
1051
|
+
"preview": "vite preview"
|
|
1052
|
+
},
|
|
1053
|
+
"dependencies": {
|
|
1054
|
+
"react": "^18.2.0",
|
|
1055
|
+
"react-dom": "^18.2.0",
|
|
1056
|
+
"react-router-dom": "^6.20.0"
|
|
1057
|
+
},
|
|
1058
|
+
"devDependencies": {
|
|
1059
|
+
"vite": "^5.0.0",
|
|
1060
|
+
"@vitejs/plugin-react": "^4.2.0"
|
|
1061
|
+
}
|
|
1062
|
+
}, null, 2),
|
|
1063
|
+
|
|
1064
|
+
// vite.config.js
|
|
1065
|
+
viteConfig: () => `import { defineConfig } from 'vite'
|
|
1066
|
+
import react from '@vitejs/plugin-react'
|
|
1067
|
+
import path from 'path'
|
|
1068
|
+
|
|
1069
|
+
export default defineConfig({
|
|
1070
|
+
plugins: [react()],
|
|
1071
|
+
resolve: {
|
|
1072
|
+
alias: {
|
|
1073
|
+
'@promptline/sdk': path.resolve(__dirname, 'dev/sdk-mock.jsx')
|
|
1074
|
+
}
|
|
1075
|
+
},
|
|
1076
|
+
server: {
|
|
1077
|
+
port: 5173,
|
|
1078
|
+
host: 'localhost',
|
|
1079
|
+
open: true,
|
|
1080
|
+
strictPort: false,
|
|
1081
|
+
proxy: {
|
|
1082
|
+
// Proxy API calls to avoid CORS issues in development
|
|
1083
|
+
'/api/promptline-local': {
|
|
1084
|
+
target: 'https://app.local.promptlineops.com',
|
|
1085
|
+
changeOrigin: true,
|
|
1086
|
+
rewrite: (path) => path.replace(/^\\/api\\/promptline-local/, '/api/v1/live'),
|
|
1087
|
+
secure: true
|
|
1088
|
+
},
|
|
1089
|
+
'/api/promptline': {
|
|
1090
|
+
target: 'https://app.promptlineops.com',
|
|
1091
|
+
changeOrigin: true,
|
|
1092
|
+
rewrite: (path) => path.replace(/^\\/api\\/promptline/, '/api/v1/live'),
|
|
1093
|
+
secure: true
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
})
|
|
1098
|
+
`,
|
|
1099
|
+
|
|
1100
|
+
// SDK mock for local development with real API support
|
|
1101
|
+
sdkMock: (config) => {
|
|
1102
|
+
const mockConfig = {
|
|
1103
|
+
app_name: config.displayName,
|
|
1104
|
+
primary_color: config.primaryColor,
|
|
1105
|
+
contact_email: config.contactEmail
|
|
1106
|
+
};
|
|
1107
|
+
return `/**
|
|
1108
|
+
* PromptLine SDK Mock for Local Development
|
|
1109
|
+
* Supports real API calls when configured via /_dev page
|
|
1110
|
+
*/
|
|
1111
|
+
import React, { createContext, useContext, useState } from 'react'
|
|
1112
|
+
import { Link as RouterLink } from 'react-router-dom'
|
|
1113
|
+
|
|
1114
|
+
const mockConfig = ${JSON.stringify(mockConfig, null, 2)}
|
|
1115
|
+
|
|
1116
|
+
const mockUser = {
|
|
1117
|
+
id: 'dev-user-123',
|
|
1118
|
+
email: 'dev@example.com',
|
|
1119
|
+
name: 'Dev User'
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
// Get PromptLine API config from localStorage
|
|
1123
|
+
function getAPIConfig() {
|
|
1124
|
+
try {
|
|
1125
|
+
return JSON.parse(localStorage.getItem('promptline_dev_config') || 'null')
|
|
1126
|
+
} catch { return null }
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
// Call PromptLine API (sync mode)
|
|
1130
|
+
async function callPromptLineAPI(config, input) {
|
|
1131
|
+
const { endpoint, apiKey } = config
|
|
1132
|
+
|
|
1133
|
+
// Add ?mode=sync for synchronous response
|
|
1134
|
+
const url = endpoint.includes('?') ? endpoint + '&mode=sync' : endpoint + '?mode=sync'
|
|
1135
|
+
|
|
1136
|
+
const res = await fetch(url, {
|
|
1137
|
+
method: 'POST',
|
|
1138
|
+
headers: {
|
|
1139
|
+
'Content-Type': 'application/json',
|
|
1140
|
+
...(apiKey ? { 'X-API-Key': apiKey } : {})
|
|
1141
|
+
},
|
|
1142
|
+
body: JSON.stringify({
|
|
1143
|
+
input: typeof input === 'string' ? { text: input } : input,
|
|
1144
|
+
variables: {}
|
|
1145
|
+
})
|
|
1146
|
+
})
|
|
1147
|
+
const data = await res.json()
|
|
1148
|
+
if (!res.ok || data.error) throw new Error(data.detail || data.error?.message || data.error || res.statusText)
|
|
1149
|
+
// Sync mode returns data.data.output or data.data.response
|
|
1150
|
+
const output = data.data?.output || data.data?.response || data.output || data.response
|
|
1151
|
+
return { response: typeof output === 'string' ? output : JSON.stringify(output) }
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
const PromptLineContext = createContext(null)
|
|
1155
|
+
|
|
1156
|
+
export function PromptLineProvider({ children }) {
|
|
1157
|
+
const [isLoading, setIsLoading] = useState(false)
|
|
1158
|
+
|
|
1159
|
+
const value = {
|
|
1160
|
+
config: mockConfig,
|
|
1161
|
+
user: mockUser,
|
|
1162
|
+
isLoading,
|
|
1163
|
+
submitForm: async (collection, data) => {
|
|
1164
|
+
console.log('[PromptLine SDK] submitForm:', collection, data)
|
|
1165
|
+
alert('Form submitted to "' + collection + '"\\n\\nData: ' + JSON.stringify(data, null, 2))
|
|
1166
|
+
return { success: true, id: 'mock-' + Date.now() }
|
|
1167
|
+
},
|
|
1168
|
+
fetchCollection: async (collection, query) => {
|
|
1169
|
+
console.log('[PromptLine SDK] fetchCollection:', collection, query)
|
|
1170
|
+
return { items: [], total: 0 }
|
|
1171
|
+
},
|
|
1172
|
+
callAI: async (sourceId, input) => {
|
|
1173
|
+
console.log('[PromptLine SDK] callAI:', sourceId, input)
|
|
1174
|
+
const config = getAPIConfig()
|
|
1175
|
+
|
|
1176
|
+
if (config && config.endpoint) {
|
|
1177
|
+
console.log('[PromptLine SDK] Using PromptLine API:', config.endpoint)
|
|
1178
|
+
try {
|
|
1179
|
+
return await callPromptLineAPI(config, input)
|
|
1180
|
+
} catch (err) {
|
|
1181
|
+
console.error('[PromptLine SDK] API Error:', err)
|
|
1182
|
+
return { response: '[API Error] ' + err.message, error: true }
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
console.log('[PromptLine SDK] No config found, using mock. Configure at /_dev')
|
|
1187
|
+
return { response: '[Mock] Configure your PromptLine endpoint at /_dev' }
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
return (
|
|
1192
|
+
<PromptLineContext.Provider value={value}>
|
|
1193
|
+
{children}
|
|
1194
|
+
</PromptLineContext.Provider>
|
|
1195
|
+
)
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
export function usePromptLine() {
|
|
1199
|
+
const context = useContext(PromptLineContext)
|
|
1200
|
+
if (!context) {
|
|
1201
|
+
return {
|
|
1202
|
+
config: mockConfig,
|
|
1203
|
+
user: mockUser,
|
|
1204
|
+
isLoading: false,
|
|
1205
|
+
submitForm: async (c, d) => { console.log('[PromptLine SDK] submitForm:', c, d); return { success: true } },
|
|
1206
|
+
fetchCollection: async (c, q) => { console.log('[PromptLine SDK] fetchCollection:', c, q); return { items: [], total: 0 } },
|
|
1207
|
+
callAI: async (s, i) => { console.log('[PromptLine SDK] callAI:', s, i); return { response: '[Mock] Configure at /_dev' } }
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
return context
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
export const Link = ({ to, children, ...props }) => (
|
|
1214
|
+
<RouterLink to={to} {...props}>{children}</RouterLink>
|
|
1215
|
+
)
|
|
1216
|
+
|
|
1217
|
+
export default { usePromptLine, PromptLineProvider, Link }
|
|
1218
|
+
`},
|
|
1219
|
+
|
|
1220
|
+
// Dev admin page for configuring PromptLine endpoint
|
|
1221
|
+
devAdmin: () => `/**
|
|
1222
|
+
* PromptLine Dev Admin - Endpoint Configuration
|
|
1223
|
+
*
|
|
1224
|
+
* This page is ONLY for local development.
|
|
1225
|
+
* Configure your PromptLine endpoint here to test with real API.
|
|
1226
|
+
*
|
|
1227
|
+
* NEVER commit API keys to git!
|
|
1228
|
+
*/
|
|
1229
|
+
import React, { useState, useEffect } from 'react'
|
|
1230
|
+
import { Link } from 'react-router-dom'
|
|
1231
|
+
|
|
1232
|
+
export default function DevAdminPage() {
|
|
1233
|
+
const [endpoint, setEndpoint] = useState('')
|
|
1234
|
+
const [apiKey, setApiKey] = useState('')
|
|
1235
|
+
const [saved, setSaved] = useState(false)
|
|
1236
|
+
const [testInput, setTestInput] = useState('Hello! What is 2+2?')
|
|
1237
|
+
const [testResult, setTestResult] = useState('')
|
|
1238
|
+
const [testing, setTesting] = useState(false)
|
|
1239
|
+
const [status, setStatus] = useState('') // 'connected' | 'error' | 'testing' | ''
|
|
1240
|
+
const [logs, setLogs] = useState([])
|
|
1241
|
+
|
|
1242
|
+
const addLog = (type, message, details = null) => {
|
|
1243
|
+
const timestamp = new Date().toLocaleTimeString()
|
|
1244
|
+
setLogs(prev => [...prev.slice(-50), { timestamp, type, message, details }])
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
useEffect(() => {
|
|
1248
|
+
const config = JSON.parse(localStorage.getItem('promptline_dev_config') || 'null')
|
|
1249
|
+
if (config) {
|
|
1250
|
+
setEndpoint(config.endpoint || '')
|
|
1251
|
+
setApiKey(config.apiKey || '')
|
|
1252
|
+
setSaved(true)
|
|
1253
|
+
}
|
|
1254
|
+
}, [])
|
|
1255
|
+
|
|
1256
|
+
const saveConfig = () => {
|
|
1257
|
+
if (!endpoint) return
|
|
1258
|
+
const config = { endpoint, apiKey }
|
|
1259
|
+
localStorage.setItem('promptline_dev_config', JSON.stringify(config))
|
|
1260
|
+
setSaved(true)
|
|
1261
|
+
addLog('info', 'Configuration saved')
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
const clearConfig = () => {
|
|
1265
|
+
localStorage.removeItem('promptline_dev_config')
|
|
1266
|
+
setEndpoint('')
|
|
1267
|
+
setApiKey('')
|
|
1268
|
+
setSaved(false)
|
|
1269
|
+
setStatus('')
|
|
1270
|
+
addLog('info', 'Configuration cleared')
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
// Convert PromptLine URLs to use local proxy (avoids CORS in development)
|
|
1274
|
+
const toProxyUrl = (url) => {
|
|
1275
|
+
// Match app.local.promptlineops.com (local dev environment)
|
|
1276
|
+
const localMatch = url.match(/https?:\\/\\/app\\.local\\.promptlineops\\.com\\/api\\/v1\\/live\\/([^?]+)/)
|
|
1277
|
+
if (localMatch) {
|
|
1278
|
+
return '/api/promptline-local/' + localMatch[1]
|
|
1279
|
+
}
|
|
1280
|
+
// Match app.promptlineops.com (production environment)
|
|
1281
|
+
const prodMatch = url.match(/https?:\\/\\/app\\.promptlineops\\.com\\/api\\/v1\\/live\\/([^?]+)/)
|
|
1282
|
+
if (prodMatch) {
|
|
1283
|
+
return '/api/promptline/' + prodMatch[1]
|
|
1284
|
+
}
|
|
1285
|
+
return url // Non-PromptLine URLs pass through unchanged
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
const testConnection = async () => {
|
|
1289
|
+
if (!endpoint) return
|
|
1290
|
+
setStatus('testing')
|
|
1291
|
+
const proxyUrl = toProxyUrl(endpoint)
|
|
1292
|
+
const url = proxyUrl.includes('?') ? proxyUrl + '&mode=sync' : proxyUrl + '?mode=sync'
|
|
1293
|
+
addLog('info', 'Testing connection...', { endpoint: url, original: endpoint })
|
|
1294
|
+
|
|
1295
|
+
const startTime = Date.now()
|
|
1296
|
+
try {
|
|
1297
|
+
const res = await fetch(url, {
|
|
1298
|
+
method: 'POST',
|
|
1299
|
+
headers: {
|
|
1300
|
+
'Content-Type': 'application/json',
|
|
1301
|
+
...(apiKey ? { 'X-API-Key': apiKey } : {})
|
|
1302
|
+
},
|
|
1303
|
+
body: JSON.stringify({ input: { text: 'test' }, variables: {} })
|
|
1304
|
+
})
|
|
1305
|
+
const duration = Date.now() - startTime
|
|
1306
|
+
|
|
1307
|
+
if (res.ok) {
|
|
1308
|
+
setStatus('connected')
|
|
1309
|
+
addLog('success', 'Connected (' + duration + 'ms)', { status: res.status })
|
|
1310
|
+
} else {
|
|
1311
|
+
const data = await res.json().catch(() => ({}))
|
|
1312
|
+
setStatus('error')
|
|
1313
|
+
addLog('error', res.status + ' ' + res.statusText + ' (' + duration + 'ms)', data)
|
|
1314
|
+
}
|
|
1315
|
+
} catch (err) {
|
|
1316
|
+
const duration = Date.now() - startTime
|
|
1317
|
+
setStatus('error')
|
|
1318
|
+
addLog('error', 'Network error (' + duration + 'ms)', {
|
|
1319
|
+
message: err.message,
|
|
1320
|
+
hint: err.message.includes('Failed to fetch') ? 'CORS issue - server needs Access-Control-Allow-Origin header' : null
|
|
1321
|
+
})
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
const testAI = async () => {
|
|
1326
|
+
if (!endpoint) return
|
|
1327
|
+
setTesting(true)
|
|
1328
|
+
setTestResult('')
|
|
1329
|
+
const proxyUrl = toProxyUrl(endpoint)
|
|
1330
|
+
const url = proxyUrl.includes('?') ? proxyUrl + '&mode=sync' : proxyUrl + '?mode=sync'
|
|
1331
|
+
addLog('request', 'POST ' + url, { input: testInput })
|
|
1332
|
+
|
|
1333
|
+
const startTime = Date.now()
|
|
1334
|
+
try {
|
|
1335
|
+
const res = await fetch(url, {
|
|
1336
|
+
method: 'POST',
|
|
1337
|
+
headers: {
|
|
1338
|
+
'Content-Type': 'application/json',
|
|
1339
|
+
...(apiKey ? { 'X-API-Key': apiKey } : {})
|
|
1340
|
+
},
|
|
1341
|
+
body: JSON.stringify({ input: { text: testInput }, variables: {} })
|
|
1342
|
+
})
|
|
1343
|
+
const data = await res.json()
|
|
1344
|
+
const duration = Date.now() - startTime
|
|
1345
|
+
|
|
1346
|
+
if (!res.ok || data.error) {
|
|
1347
|
+
const errMsg = data.detail || data.error?.message || data.error || res.statusText
|
|
1348
|
+
setTestResult('Error: ' + errMsg)
|
|
1349
|
+
addLog('error', res.status + ' (' + duration + 'ms)', data)
|
|
1350
|
+
} else {
|
|
1351
|
+
// Sync mode returns data.data.output or data.data.response
|
|
1352
|
+
const output = data.data?.output || data.data?.response || data.output || data.response || JSON.stringify(data, null, 2)
|
|
1353
|
+
const outputStr = typeof output === 'string' ? output : JSON.stringify(output, null, 2)
|
|
1354
|
+
setTestResult(outputStr)
|
|
1355
|
+
addLog('success', '200 OK (' + duration + 'ms)', { output: outputStr.substring(0, 200) })
|
|
1356
|
+
}
|
|
1357
|
+
} catch (err) {
|
|
1358
|
+
setTestResult('Error: ' + err.message)
|
|
1359
|
+
addLog('error', 'Network error', { message: err.message })
|
|
1360
|
+
}
|
|
1361
|
+
setTesting(false)
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
return (
|
|
1365
|
+
<div className="min-h-screen bg-gray-900 text-white p-8">
|
|
1366
|
+
<div className="max-w-2xl mx-auto">
|
|
1367
|
+
<div className="flex items-center justify-between mb-8">
|
|
1368
|
+
<div>
|
|
1369
|
+
<h1 className="text-3xl font-bold text-yellow-400">/_dev</h1>
|
|
1370
|
+
<p className="text-gray-400 mt-1">PromptLine Endpoint (Local Only)</p>
|
|
1371
|
+
</div>
|
|
1372
|
+
<Link to="/" className="px-4 py-2 bg-gray-700 rounded hover:bg-gray-600">
|
|
1373
|
+
Back to App
|
|
1374
|
+
</Link>
|
|
1375
|
+
</div>
|
|
1376
|
+
|
|
1377
|
+
<div className="bg-yellow-900/30 border border-yellow-600 rounded-lg p-4 mb-8">
|
|
1378
|
+
<p className="text-yellow-300 text-sm">
|
|
1379
|
+
Development Only - API keys stored in localStorage. Never commit to git.
|
|
1380
|
+
</p>
|
|
1381
|
+
</div>
|
|
1382
|
+
|
|
1383
|
+
{/* Configuration */}
|
|
1384
|
+
<div className="bg-gray-800 rounded-lg p-6 mb-8">
|
|
1385
|
+
<div className="flex items-center justify-between mb-4">
|
|
1386
|
+
<h2 className="text-xl font-semibold">Configuration</h2>
|
|
1387
|
+
{saved && (
|
|
1388
|
+
<span className="px-2 py-1 bg-green-600/30 text-green-400 text-xs rounded">Saved</span>
|
|
1389
|
+
)}
|
|
1390
|
+
</div>
|
|
1391
|
+
|
|
1392
|
+
<div className="space-y-4">
|
|
1393
|
+
<div>
|
|
1394
|
+
<label className="block text-sm text-gray-400 mb-1">Endpoint URL</label>
|
|
1395
|
+
<input
|
|
1396
|
+
value={endpoint}
|
|
1397
|
+
onChange={(e) => { setEndpoint(e.target.value); setSaved(false) }}
|
|
1398
|
+
className="w-full bg-gray-700 p-3 rounded text-white font-mono text-sm"
|
|
1399
|
+
placeholder="https://app.promptlineops.com/api/v1/live/..."
|
|
1400
|
+
/>
|
|
1401
|
+
<p className="text-xs text-gray-500 mt-1">Copy the Live API URL from Creator Studio</p>
|
|
1402
|
+
</div>
|
|
1403
|
+
|
|
1404
|
+
<div>
|
|
1405
|
+
<label className="block text-sm text-gray-400 mb-1">API Key</label>
|
|
1406
|
+
<input
|
|
1407
|
+
type="password"
|
|
1408
|
+
value={apiKey}
|
|
1409
|
+
onChange={(e) => { setApiKey(e.target.value); setSaved(false) }}
|
|
1410
|
+
className="w-full bg-gray-700 p-3 rounded text-white font-mono text-sm"
|
|
1411
|
+
placeholder="Your API key..."
|
|
1412
|
+
/>
|
|
1413
|
+
<p className="text-xs text-gray-500 mt-1">Get your API key from /api-keys</p>
|
|
1414
|
+
</div>
|
|
1415
|
+
|
|
1416
|
+
<div className="flex gap-3">
|
|
1417
|
+
<button
|
|
1418
|
+
onClick={saveConfig}
|
|
1419
|
+
disabled={!endpoint}
|
|
1420
|
+
className="px-6 py-2 bg-indigo-600 rounded font-medium hover:bg-indigo-500 disabled:opacity-50"
|
|
1421
|
+
>
|
|
1422
|
+
Save
|
|
1423
|
+
</button>
|
|
1424
|
+
<button
|
|
1425
|
+
onClick={testConnection}
|
|
1426
|
+
disabled={!endpoint || status === 'testing'}
|
|
1427
|
+
className="px-6 py-2 bg-gray-600 rounded font-medium hover:bg-gray-500 disabled:opacity-50 flex items-center gap-2"
|
|
1428
|
+
>
|
|
1429
|
+
{status === 'testing' ? 'Testing...' : 'Test Connection'}
|
|
1430
|
+
{status === 'connected' && <span className="text-green-400">●</span>}
|
|
1431
|
+
{status === 'error' && <span className="text-red-400">●</span>}
|
|
1432
|
+
</button>
|
|
1433
|
+
{saved && (
|
|
1434
|
+
<button
|
|
1435
|
+
onClick={clearConfig}
|
|
1436
|
+
className="px-4 py-2 bg-red-600/30 text-red-400 rounded hover:bg-red-600/50"
|
|
1437
|
+
>
|
|
1438
|
+
Clear
|
|
1439
|
+
</button>
|
|
1440
|
+
)}
|
|
1441
|
+
</div>
|
|
1442
|
+
</div>
|
|
1443
|
+
</div>
|
|
1444
|
+
|
|
1445
|
+
{/* Test AI */}
|
|
1446
|
+
{saved && (
|
|
1447
|
+
<div className="bg-gray-800 rounded-lg p-6 mb-8">
|
|
1448
|
+
<h2 className="text-xl font-semibold mb-4">Test AI</h2>
|
|
1449
|
+
<textarea
|
|
1450
|
+
value={testInput}
|
|
1451
|
+
onChange={(e) => setTestInput(e.target.value)}
|
|
1452
|
+
className="w-full bg-gray-700 p-3 rounded mb-4 text-white"
|
|
1453
|
+
rows={3}
|
|
1454
|
+
placeholder="Enter test input..."
|
|
1455
|
+
/>
|
|
1456
|
+
<button
|
|
1457
|
+
onClick={testAI}
|
|
1458
|
+
disabled={testing}
|
|
1459
|
+
className="px-6 py-2 bg-green-600 rounded font-medium hover:bg-green-500 disabled:opacity-50"
|
|
1460
|
+
>
|
|
1461
|
+
{testing ? 'Sending...' : 'Send Request'}
|
|
1462
|
+
</button>
|
|
1463
|
+
{testResult && (
|
|
1464
|
+
<div className="mt-4 p-4 bg-gray-700 rounded">
|
|
1465
|
+
<p className="text-sm text-gray-400 mb-2">Response:</p>
|
|
1466
|
+
<pre className="whitespace-pre-wrap text-green-300">{testResult}</pre>
|
|
1467
|
+
</div>
|
|
1468
|
+
)}
|
|
1469
|
+
</div>
|
|
1470
|
+
)}
|
|
1471
|
+
|
|
1472
|
+
{/* Logs */}
|
|
1473
|
+
<div className="bg-gray-800 rounded-lg p-6">
|
|
1474
|
+
<div className="flex items-center justify-between mb-4">
|
|
1475
|
+
<h2 className="text-xl font-semibold">Logs</h2>
|
|
1476
|
+
<button onClick={() => setLogs([])} className="px-3 py-1 bg-gray-700 rounded text-sm hover:bg-gray-600">
|
|
1477
|
+
Clear
|
|
1478
|
+
</button>
|
|
1479
|
+
</div>
|
|
1480
|
+
<div className="bg-gray-900 rounded p-4 h-48 overflow-y-auto font-mono text-sm">
|
|
1481
|
+
{logs.length === 0 ? (
|
|
1482
|
+
<p className="text-gray-500">No logs yet</p>
|
|
1483
|
+
) : (
|
|
1484
|
+
logs.map((log, i) => (
|
|
1485
|
+
<div key={i} className="mb-2 border-b border-gray-800 pb-2">
|
|
1486
|
+
<div className="flex items-center gap-2">
|
|
1487
|
+
<span className="text-gray-500">{log.timestamp}</span>
|
|
1488
|
+
<span className={
|
|
1489
|
+
log.type === 'success' ? 'text-green-400' :
|
|
1490
|
+
log.type === 'error' ? 'text-red-400' :
|
|
1491
|
+
log.type === 'request' ? 'text-blue-400' :
|
|
1492
|
+
'text-yellow-400'
|
|
1493
|
+
}>[{log.type.toUpperCase()}]</span>
|
|
1494
|
+
<span className="text-gray-200">{log.message}</span>
|
|
1495
|
+
</div>
|
|
1496
|
+
{log.details && (
|
|
1497
|
+
<pre className="text-xs text-gray-400 mt-1 ml-4 overflow-x-auto">
|
|
1498
|
+
{JSON.stringify(log.details, null, 2)}
|
|
1499
|
+
</pre>
|
|
1500
|
+
)}
|
|
1501
|
+
</div>
|
|
1502
|
+
))
|
|
1503
|
+
)}
|
|
1504
|
+
</div>
|
|
1505
|
+
</div>
|
|
1506
|
+
|
|
1507
|
+
{/* Usage */}
|
|
1508
|
+
<div className="mt-8 p-6 bg-gray-800 rounded-lg">
|
|
1509
|
+
<h2 className="text-xl font-semibold mb-4">Usage in Your Code</h2>
|
|
1510
|
+
<pre className="bg-gray-900 p-4 rounded text-sm overflow-x-auto">
|
|
1511
|
+
{$BACKTICK}const { callAI } = usePromptLine()
|
|
1512
|
+
|
|
1513
|
+
const result = await callAI('main', { text: 'Hello!' })
|
|
1514
|
+
console.log(result.response)$BACKTICK}
|
|
1515
|
+
</pre>
|
|
1516
|
+
</div>
|
|
1517
|
+
</div>
|
|
1518
|
+
</div>
|
|
1519
|
+
)
|
|
1520
|
+
}
|
|
1521
|
+
`.replace(/\$BACKTICK/g, '`'),
|
|
1522
|
+
|
|
1523
|
+
// index.html for Vite
|
|
1524
|
+
indexHtml: (config) => `<!DOCTYPE html>
|
|
1525
|
+
<html lang="en">
|
|
1526
|
+
<head>
|
|
1527
|
+
<meta charset="UTF-8" />
|
|
1528
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
1529
|
+
<title>${escapeJsTemplate(config.displayName)}</title>
|
|
1530
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
1531
|
+
</head>
|
|
1532
|
+
<body>
|
|
1533
|
+
<div id="root"></div>
|
|
1534
|
+
<script type="module" src="/dev/main.jsx"></script>
|
|
1535
|
+
</body>
|
|
1536
|
+
</html>
|
|
1537
|
+
`,
|
|
1538
|
+
|
|
1539
|
+
// main.jsx entry point
|
|
1540
|
+
mainJsx: (config, pages) => {
|
|
1541
|
+
const imports = [];
|
|
1542
|
+
const routes = [];
|
|
1543
|
+
|
|
1544
|
+
pages.public.forEach(name => {
|
|
1545
|
+
const componentName = 'Public' + name.charAt(0).toUpperCase() + name.slice(1).replace(/-./g, x => x[1].toUpperCase());
|
|
1546
|
+
imports.push("import " + componentName + " from '../public/" + name + ".tsx'");
|
|
1547
|
+
const routePath = name === 'index' ? '/' : '/' + name;
|
|
1548
|
+
routes.push(" { path: '" + routePath + "', element: <" + componentName + " /> }");
|
|
1549
|
+
});
|
|
1550
|
+
|
|
1551
|
+
pages.private.forEach(name => {
|
|
1552
|
+
const componentName = 'Private' + name.charAt(0).toUpperCase() + name.slice(1).replace(/-./g, x => x[1].toUpperCase());
|
|
1553
|
+
imports.push("import " + componentName + " from '../private/" + name + ".tsx'");
|
|
1554
|
+
routes.push(" { path: '/" + name + "', element: <" + componentName + " /> }");
|
|
1555
|
+
});
|
|
1556
|
+
|
|
1557
|
+
// Add dev admin route
|
|
1558
|
+
imports.push("import DevAdmin from './dev-admin.jsx'");
|
|
1559
|
+
routes.push(" { path: '/_dev', element: <DevAdmin /> }");
|
|
1560
|
+
|
|
1561
|
+
return `/**
|
|
1562
|
+
* Development Entry Point
|
|
1563
|
+
* Auto-generated by create-promptline-app
|
|
1564
|
+
*/
|
|
1565
|
+
import React from 'react'
|
|
1566
|
+
import ReactDOM from 'react-dom/client'
|
|
1567
|
+
import { createBrowserRouter, RouterProvider } from 'react-router-dom'
|
|
1568
|
+
import { PromptLineProvider } from './sdk-mock.jsx'
|
|
1569
|
+
|
|
1570
|
+
${imports.join('\n')}
|
|
1571
|
+
|
|
1572
|
+
const router = createBrowserRouter([
|
|
1573
|
+
${routes.join(',\n')}
|
|
1574
|
+
])
|
|
1575
|
+
|
|
1576
|
+
ReactDOM.createRoot(document.getElementById('root')).render(
|
|
1577
|
+
<React.StrictMode>
|
|
1578
|
+
<PromptLineProvider>
|
|
1579
|
+
<RouterProvider router={router} />
|
|
1580
|
+
</PromptLineProvider>
|
|
1581
|
+
</React.StrictMode>
|
|
1582
|
+
)
|
|
1583
|
+
`},
|
|
1584
|
+
};
|
|
1585
|
+
|
|
1586
|
+
// Create package function
|
|
1587
|
+
async function createPackage(name, options = {}) {
|
|
1588
|
+
printLogo();
|
|
1589
|
+
|
|
1590
|
+
// Handle absolute paths vs relative names
|
|
1591
|
+
let targetDir, slug;
|
|
1592
|
+
if (path.isAbsolute(name)) {
|
|
1593
|
+
// Absolute path provided
|
|
1594
|
+
targetDir = name;
|
|
1595
|
+
slug = slugify(path.basename(name));
|
|
1596
|
+
} else {
|
|
1597
|
+
// Relative name provided
|
|
1598
|
+
slug = slugify(name);
|
|
1599
|
+
targetDir = path.join(process.cwd(), slug);
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
// Validate package name (security + UX)
|
|
1603
|
+
const validation = validatePackageName(slug);
|
|
1604
|
+
if (!validation.valid) {
|
|
1605
|
+
error(validation.error);
|
|
1606
|
+
process.exit(1);
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
if (fs.existsSync(targetDir)) {
|
|
1610
|
+
error(`Directory '${slug}' already exists`);
|
|
1611
|
+
process.exit(1);
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1614
|
+
console.log(`${c.bold}Creating new PromptLine app: ${c.cyan}${name}${c.reset}\n`);
|
|
1615
|
+
|
|
1616
|
+
// Configuration
|
|
1617
|
+
let config = {
|
|
1618
|
+
name,
|
|
1619
|
+
slug,
|
|
1620
|
+
displayName: name.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' '),
|
|
1621
|
+
description: 'My PromptLine application',
|
|
1622
|
+
primaryColor: '#6366f1',
|
|
1623
|
+
contactEmail: 'contact@example.com',
|
|
1624
|
+
preset: options.preset || 'contact',
|
|
1625
|
+
};
|
|
1626
|
+
|
|
1627
|
+
// Interactive mode
|
|
1628
|
+
if (!options.yes) {
|
|
1629
|
+
const prompt = createPrompt();
|
|
1630
|
+
|
|
1631
|
+
try {
|
|
1632
|
+
// Display name with validation
|
|
1633
|
+
let displayNameValid = false;
|
|
1634
|
+
while (!displayNameValid) {
|
|
1635
|
+
const input = await prompt.question('Display name', config.displayName);
|
|
1636
|
+
const validation = validateString(input, 'Display name', 100);
|
|
1637
|
+
if (validation.valid) {
|
|
1638
|
+
config.displayName = validation.sanitized;
|
|
1639
|
+
displayNameValid = true;
|
|
1640
|
+
} else {
|
|
1641
|
+
error(validation.error);
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
// Description with validation
|
|
1646
|
+
let descriptionValid = false;
|
|
1647
|
+
while (!descriptionValid) {
|
|
1648
|
+
const input = await prompt.question('Description', config.description);
|
|
1649
|
+
const validation = validateString(input, 'Description', 500);
|
|
1650
|
+
if (validation.valid) {
|
|
1651
|
+
config.description = validation.sanitized;
|
|
1652
|
+
descriptionValid = true;
|
|
1653
|
+
} else {
|
|
1654
|
+
error(validation.error);
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
// Primary color with validation
|
|
1659
|
+
let colorValid = false;
|
|
1660
|
+
while (!colorValid) {
|
|
1661
|
+
const input = await prompt.question('Primary color (hex)', config.primaryColor);
|
|
1662
|
+
const validation = validateHexColor(input);
|
|
1663
|
+
if (validation.valid) {
|
|
1664
|
+
config.primaryColor = validation.normalized;
|
|
1665
|
+
colorValid = true;
|
|
1666
|
+
} else {
|
|
1667
|
+
error(validation.error);
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1671
|
+
// Contact email with validation
|
|
1672
|
+
let emailValid = false;
|
|
1673
|
+
while (!emailValid) {
|
|
1674
|
+
const input = await prompt.question('Contact email', config.contactEmail);
|
|
1675
|
+
const validation = validateEmail(input);
|
|
1676
|
+
if (validation.valid) {
|
|
1677
|
+
config.contactEmail = input.trim();
|
|
1678
|
+
emailValid = true;
|
|
1679
|
+
} else {
|
|
1680
|
+
error(validation.error);
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
if (!options.preset) {
|
|
1685
|
+
const presets = [
|
|
1686
|
+
{ name: 'Contact Form', description: 'Landing + form + dashboard', value: 'contact' },
|
|
1687
|
+
{ name: 'SaaS', description: 'Full app with auth & billing', value: 'saas' },
|
|
1688
|
+
{ name: 'API', description: 'Backend-focused, minimal UI', value: 'api' },
|
|
1689
|
+
{ name: 'Blank', description: 'Empty template', value: 'blank' },
|
|
1690
|
+
];
|
|
1691
|
+
config.preset = await prompt.select('Select a template:', presets);
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
prompt.close();
|
|
1695
|
+
} catch (err) {
|
|
1696
|
+
prompt.close();
|
|
1697
|
+
throw err;
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1701
|
+
console.log(`\n${c.bold}Creating package structure...${c.reset}\n`);
|
|
1702
|
+
|
|
1703
|
+
try {
|
|
1704
|
+
// Create directories
|
|
1705
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
1706
|
+
fs.mkdirSync(path.join(targetDir, 'public'));
|
|
1707
|
+
fs.mkdirSync(path.join(targetDir, 'private'));
|
|
1708
|
+
fs.mkdirSync(path.join(targetDir, 'backend'));
|
|
1709
|
+
fs.mkdirSync(path.join(targetDir, 'assets'));
|
|
1710
|
+
|
|
1711
|
+
success(`Created ${path.basename(targetDir)}/`);
|
|
1712
|
+
success('Created public/');
|
|
1713
|
+
success('Created private/');
|
|
1714
|
+
success('Created backend/');
|
|
1715
|
+
success('Created assets/');
|
|
1716
|
+
|
|
1717
|
+
// Create files based on preset
|
|
1718
|
+
if (config.preset === 'blank') {
|
|
1719
|
+
fs.writeFileSync(path.join(targetDir, 'public', 'index.tsx'), templates.blankIndex(config));
|
|
1720
|
+
success('Created public/index.tsx');
|
|
1721
|
+
} else {
|
|
1722
|
+
// Contact, SaaS, API all start with similar base
|
|
1723
|
+
fs.writeFileSync(path.join(targetDir, 'public', 'index.tsx'), templates.publicIndex(config));
|
|
1724
|
+
success('Created public/index.tsx');
|
|
1725
|
+
|
|
1726
|
+
if (config.preset !== 'api') {
|
|
1727
|
+
fs.writeFileSync(path.join(targetDir, 'private', 'dashboard.tsx'), templates.privateDashboard(config));
|
|
1728
|
+
fs.writeFileSync(path.join(targetDir, 'private', 'settings.tsx'), templates.privateSettings(config));
|
|
1729
|
+
success('Created private/dashboard.tsx');
|
|
1730
|
+
success('Created private/settings.tsx');
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
fs.writeFileSync(path.join(targetDir, 'backend', 'api.py'), templates.backendApi(config));
|
|
1734
|
+
success('Created backend/api.py');
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
// Common files
|
|
1738
|
+
fs.writeFileSync(path.join(targetDir, 'promptline.yaml'), templates.config(config));
|
|
1739
|
+
success('Created promptline.yaml');
|
|
1740
|
+
|
|
1741
|
+
fs.writeFileSync(path.join(targetDir, 'README.md'), templates.readme(config));
|
|
1742
|
+
success('Created README.md');
|
|
1743
|
+
|
|
1744
|
+
fs.writeFileSync(path.join(targetDir, '.gitignore'), templates.gitignore());
|
|
1745
|
+
success('Created .gitignore');
|
|
1746
|
+
|
|
1747
|
+
// Dev environment files
|
|
1748
|
+
fs.mkdirSync(path.join(targetDir, 'dev'));
|
|
1749
|
+
success('Created dev/');
|
|
1750
|
+
|
|
1751
|
+
// Collect page names for router
|
|
1752
|
+
const pages = { public: ['index'], private: [] };
|
|
1753
|
+
if (config.preset !== 'blank' && config.preset !== 'api') {
|
|
1754
|
+
pages.private = ['dashboard', 'settings'];
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
fs.writeFileSync(path.join(targetDir, 'package.json'), templates.packageJson(config));
|
|
1758
|
+
success('Created package.json');
|
|
1759
|
+
|
|
1760
|
+
fs.writeFileSync(path.join(targetDir, 'vite.config.js'), templates.viteConfig());
|
|
1761
|
+
success('Created vite.config.js');
|
|
1762
|
+
|
|
1763
|
+
fs.writeFileSync(path.join(targetDir, 'index.html'), templates.indexHtml(config));
|
|
1764
|
+
success('Created index.html');
|
|
1765
|
+
|
|
1766
|
+
fs.writeFileSync(path.join(targetDir, 'dev', 'sdk-mock.jsx'), templates.sdkMock(config));
|
|
1767
|
+
success('Created dev/sdk-mock.jsx');
|
|
1768
|
+
|
|
1769
|
+
fs.writeFileSync(path.join(targetDir, 'dev', 'main.jsx'), templates.mainJsx(config, pages));
|
|
1770
|
+
success('Created dev/main.jsx');
|
|
1771
|
+
|
|
1772
|
+
fs.writeFileSync(path.join(targetDir, 'dev', 'dev-admin.jsx'), templates.devAdmin());
|
|
1773
|
+
success('Created dev/dev-admin.jsx');
|
|
1774
|
+
|
|
1775
|
+
// Success message
|
|
1776
|
+
const dirName = path.basename(targetDir);
|
|
1777
|
+
console.log(`
|
|
1778
|
+
${c.green}╔═══════════════════════════════════════════════════════════╗
|
|
1779
|
+
║ ║
|
|
1780
|
+
║ ${c.bold}Success!${c.reset}${c.green} Created ${dirName} ${c.green}║
|
|
1781
|
+
║ ║
|
|
1782
|
+
╚═══════════════════════════════════════════════════════════════╝${c.reset}
|
|
1783
|
+
|
|
1784
|
+
${c.bold}Next steps:${c.reset}
|
|
1785
|
+
|
|
1786
|
+
${c.cyan}cd ${targetDir}${c.reset}
|
|
1787
|
+
${c.cyan}npm install${c.reset}
|
|
1788
|
+
${c.cyan}npm run dev${c.reset}
|
|
1789
|
+
|
|
1790
|
+
This starts a local dev server at ${c.green}http://localhost:5173${c.reset}
|
|
1791
|
+
|
|
1792
|
+
${c.bold}Configure AI for testing:${c.reset}
|
|
1793
|
+
|
|
1794
|
+
Open ${c.yellow}http://localhost:5173/_dev${c.reset} to configure real AI endpoints
|
|
1795
|
+
|
|
1796
|
+
${c.bold}Edit your files:${c.reset}
|
|
1797
|
+
|
|
1798
|
+
${c.yellow}public/${c.reset} Public pages (no auth)
|
|
1799
|
+
${c.yellow}private/${c.reset} Protected pages (auth required)
|
|
1800
|
+
${c.yellow}promptline.yaml${c.reset} Package configuration
|
|
1801
|
+
|
|
1802
|
+
${c.bold}Deploy:${c.reset}
|
|
1803
|
+
|
|
1804
|
+
1. Create ZIP: ${c.cyan}zip -r ${dirName}.zip ${dirName} -x "node_modules/*" -x ".git/*"${c.reset}
|
|
1805
|
+
2. Upload to ${c.dim}https://app.promptlineops.com/apps/creator${c.reset}
|
|
1806
|
+
|
|
1807
|
+
${c.bold}Documentation:${c.reset} ${c.cyan}https://docs.promptlineops.com${c.reset}
|
|
1808
|
+
`);
|
|
1809
|
+
|
|
1810
|
+
} catch (err) {
|
|
1811
|
+
error(`Failed to create package: ${err.message}`);
|
|
1812
|
+
// Cleanup on failure
|
|
1813
|
+
if (fs.existsSync(targetDir)) {
|
|
1814
|
+
fs.rmSync(targetDir, { recursive: true });
|
|
1815
|
+
}
|
|
1816
|
+
process.exit(1);
|
|
1817
|
+
}
|
|
1818
|
+
}
|
|
1819
|
+
|
|
1820
|
+
// Parse arguments
|
|
1821
|
+
function parseArgs() {
|
|
1822
|
+
const args = process.argv.slice(2);
|
|
1823
|
+
const options = {
|
|
1824
|
+
name: null,
|
|
1825
|
+
preset: null,
|
|
1826
|
+
yes: false,
|
|
1827
|
+
help: false,
|
|
1828
|
+
version: false,
|
|
1829
|
+
};
|
|
1830
|
+
|
|
1831
|
+
for (let i = 0; i < args.length; i++) {
|
|
1832
|
+
const arg = args[i];
|
|
1833
|
+
|
|
1834
|
+
if (arg === '--help' || arg === '-h') {
|
|
1835
|
+
options.help = true;
|
|
1836
|
+
} else if (arg === '--version' || arg === '-v') {
|
|
1837
|
+
options.version = true;
|
|
1838
|
+
} else if (arg === '--yes' || arg === '-y') {
|
|
1839
|
+
options.yes = true;
|
|
1840
|
+
} else if (arg === '--preset' || arg === '-p') {
|
|
1841
|
+
options.preset = args[++i];
|
|
1842
|
+
} else if (!arg.startsWith('-') && !options.name) {
|
|
1843
|
+
options.name = arg;
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1847
|
+
return options;
|
|
1848
|
+
}
|
|
1849
|
+
|
|
1850
|
+
function showHelp() {
|
|
1851
|
+
console.log(`
|
|
1852
|
+
${c.bold}create-promptline-app${c.reset} - Create PromptLine applications
|
|
1853
|
+
|
|
1854
|
+
${c.bold}Usage:${c.reset}
|
|
1855
|
+
npx create-promptline-app <app-name> [options]
|
|
1856
|
+
|
|
1857
|
+
${c.bold}Options:${c.reset}
|
|
1858
|
+
-p, --preset <preset> Template preset (contact, saas, api, blank)
|
|
1859
|
+
-y, --yes Skip prompts, use defaults
|
|
1860
|
+
-h, --help Show this help
|
|
1861
|
+
-v, --version Show version
|
|
1862
|
+
|
|
1863
|
+
${c.bold}Examples:${c.reset}
|
|
1864
|
+
npx create-promptline-app my-app
|
|
1865
|
+
npx create-promptline-app my-api --preset api
|
|
1866
|
+
npx create-promptline-app my-app -y
|
|
1867
|
+
|
|
1868
|
+
${c.bold}Presets:${c.reset}
|
|
1869
|
+
contact Landing page + contact form + dashboard (default)
|
|
1870
|
+
saas Full SaaS with auth, dashboard, billing
|
|
1871
|
+
api Backend-focused, minimal frontend
|
|
1872
|
+
blank Empty template
|
|
1873
|
+
|
|
1874
|
+
${c.bold}Documentation:${c.reset}
|
|
1875
|
+
https://docs.promptlineops.com/packages
|
|
1876
|
+
`);
|
|
1877
|
+
}
|
|
1878
|
+
|
|
1879
|
+
// Main
|
|
1880
|
+
async function main() {
|
|
1881
|
+
const options = parseArgs();
|
|
1882
|
+
|
|
1883
|
+
if (options.help) {
|
|
1884
|
+
showHelp();
|
|
1885
|
+
process.exit(0);
|
|
1886
|
+
}
|
|
1887
|
+
|
|
1888
|
+
if (options.version) {
|
|
1889
|
+
const pkgPath = path.join(__dirname, '..', 'package.json');
|
|
1890
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
1891
|
+
console.log(pkg.version);
|
|
1892
|
+
process.exit(0);
|
|
1893
|
+
}
|
|
1894
|
+
|
|
1895
|
+
if (!options.name) {
|
|
1896
|
+
printLogo();
|
|
1897
|
+
error('Please specify a project name:');
|
|
1898
|
+
console.log(`\n ${c.cyan}npx create-promptline-app${c.reset} ${c.green}<app-name>${c.reset}\n`);
|
|
1899
|
+
console.log('For example:');
|
|
1900
|
+
console.log(` ${c.cyan}npx create-promptline-app${c.reset} ${c.green}my-restaurant-app${c.reset}\n`);
|
|
1901
|
+
console.log(`Run ${c.cyan}npx create-promptline-app --help${c.reset} for more options.`);
|
|
1902
|
+
process.exit(1);
|
|
1903
|
+
}
|
|
1904
|
+
|
|
1905
|
+
await createPackage(options.name, options);
|
|
1906
|
+
}
|
|
1907
|
+
|
|
1908
|
+
main().catch((err) => {
|
|
1909
|
+
error(err.message);
|
|
1910
|
+
process.exit(1);
|
|
1911
|
+
});
|