promptlineapp 1.5.0 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/bin/cli.js +685 -11
- package/package.json +1 -1
package/README.md
CHANGED
package/bin/cli.js
CHANGED
|
@@ -4,14 +4,75 @@
|
|
|
4
4
|
* create-promptline-app CLI
|
|
5
5
|
*
|
|
6
6
|
* Usage:
|
|
7
|
-
* npx
|
|
8
|
-
* npx
|
|
7
|
+
* npx promptlineapp my-app - Create new app from scratch
|
|
8
|
+
* npx promptlineapp get <git-url> - Get existing app from Git
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
const fs = require('fs');
|
|
12
12
|
const path = require('path');
|
|
13
13
|
const readline = require('readline');
|
|
14
14
|
|
|
15
|
+
// Simple YAML parser for extracting ai_bindings
|
|
16
|
+
// Only parses what we need - not a full YAML parser
|
|
17
|
+
function parseBindingsFromYaml(yamlContent) {
|
|
18
|
+
const bindings = [];
|
|
19
|
+
const lines = yamlContent.split('\n');
|
|
20
|
+
let inAiBindings = false;
|
|
21
|
+
let currentIndent = 0;
|
|
22
|
+
|
|
23
|
+
for (let i = 0; i < lines.length; i++) {
|
|
24
|
+
const line = lines[i];
|
|
25
|
+
const trimmed = line.trim();
|
|
26
|
+
|
|
27
|
+
// Skip empty lines and comments
|
|
28
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
29
|
+
|
|
30
|
+
// Detect ai_bindings: section
|
|
31
|
+
if (trimmed === 'ai_bindings:') {
|
|
32
|
+
inAiBindings = true;
|
|
33
|
+
currentIndent = line.search(/\S/);
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// If we're in ai_bindings section
|
|
38
|
+
if (inAiBindings) {
|
|
39
|
+
const lineIndent = line.search(/\S/);
|
|
40
|
+
|
|
41
|
+
// If we hit something at same or lower indent, we're out of ai_bindings
|
|
42
|
+
if (lineIndent <= currentIndent && trimmed && !trimmed.startsWith('#')) {
|
|
43
|
+
inAiBindings = false;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Check if this is a binding name (indent = currentIndent + 2, ends with :)
|
|
48
|
+
if (lineIndent === currentIndent + 2 && trimmed.endsWith(':') && !trimmed.includes(' ')) {
|
|
49
|
+
const bindingName = trimmed.slice(0, -1);
|
|
50
|
+
// Look ahead for type and description
|
|
51
|
+
let type = 'prompt';
|
|
52
|
+
let description = '';
|
|
53
|
+
|
|
54
|
+
for (let j = i + 1; j < Math.min(i + 10, lines.length); j++) {
|
|
55
|
+
const nextLine = lines[j].trim();
|
|
56
|
+
if (nextLine.startsWith('type:')) {
|
|
57
|
+
type = nextLine.split(':')[1].trim().replace(/["']/g, '');
|
|
58
|
+
}
|
|
59
|
+
if (nextLine.startsWith('description:')) {
|
|
60
|
+
description = nextLine.split(':').slice(1).join(':').trim().replace(/["']/g, '');
|
|
61
|
+
}
|
|
62
|
+
// Stop if we hit another binding
|
|
63
|
+
if (lines[j].search(/\S/) === currentIndent + 2 && nextLine.endsWith(':') && !nextLine.includes(' ')) {
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
bindings.push({ name: bindingName, type, description });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return bindings;
|
|
74
|
+
}
|
|
75
|
+
|
|
15
76
|
// ANSI colors
|
|
16
77
|
const colors = {
|
|
17
78
|
reset: '\x1b[0m',
|
|
@@ -2059,13 +2120,26 @@ ${c.bold}Documentation:${c.reset} ${c.cyan}https://docs.promptlineops.com${c.res
|
|
|
2059
2120
|
function parseArgs() {
|
|
2060
2121
|
const args = process.argv.slice(2);
|
|
2061
2122
|
const options = {
|
|
2123
|
+
command: 'new', // 'new' or 'get'
|
|
2062
2124
|
name: null,
|
|
2125
|
+
source: null, // Git URL for 'get' command
|
|
2063
2126
|
preset: null,
|
|
2064
2127
|
yes: false,
|
|
2065
2128
|
help: false,
|
|
2066
2129
|
version: false,
|
|
2067
2130
|
};
|
|
2068
2131
|
|
|
2132
|
+
// Detect 'get' command
|
|
2133
|
+
if (args[0] === 'get') {
|
|
2134
|
+
options.command = 'get';
|
|
2135
|
+
options.source = args[1];
|
|
2136
|
+
// Optional: destination name as third argument
|
|
2137
|
+
if (args[2] && !args[2].startsWith('-')) {
|
|
2138
|
+
options.name = args[2];
|
|
2139
|
+
}
|
|
2140
|
+
return options;
|
|
2141
|
+
}
|
|
2142
|
+
|
|
2069
2143
|
for (let i = 0; i < args.length; i++) {
|
|
2070
2144
|
const arg = args[i];
|
|
2071
2145
|
|
|
@@ -2085,23 +2159,607 @@ function parseArgs() {
|
|
|
2085
2159
|
return options;
|
|
2086
2160
|
}
|
|
2087
2161
|
|
|
2162
|
+
// Templates for injected dev files
|
|
2163
|
+
const devTemplates = {
|
|
2164
|
+
// Hybrid SDK that calls real APIs when configured
|
|
2165
|
+
sdkHybrid: (bindings) => `/**
|
|
2166
|
+
* PromptLine SDK - Hybrid Mode
|
|
2167
|
+
* Calls real PromptLine APIs when endpoints are configured via /_dev
|
|
2168
|
+
*
|
|
2169
|
+
* Auto-generated by: npx promptlineapp get
|
|
2170
|
+
*/
|
|
2171
|
+
import React, { createContext, useContext, useState, useMemo } from 'react'
|
|
2172
|
+
|
|
2173
|
+
const STORAGE_KEY = 'promptline_endpoints_config'
|
|
2174
|
+
const API_KEY_STORAGE = 'promptline_api_key'
|
|
2175
|
+
|
|
2176
|
+
// Bindings detected from promptline.yaml
|
|
2177
|
+
export const availableBindings = ${JSON.stringify(bindings, null, 2)}
|
|
2178
|
+
|
|
2179
|
+
// Get stored configuration
|
|
2180
|
+
function getConfig() {
|
|
2181
|
+
try {
|
|
2182
|
+
return JSON.parse(localStorage.getItem(STORAGE_KEY) || '{}')
|
|
2183
|
+
} catch { return {} }
|
|
2184
|
+
}
|
|
2185
|
+
|
|
2186
|
+
function getApiKey() {
|
|
2187
|
+
return localStorage.getItem(API_KEY_STORAGE) || ''
|
|
2188
|
+
}
|
|
2189
|
+
|
|
2190
|
+
// Convert PromptLine URLs to use local proxy
|
|
2191
|
+
function toProxyUrl(endpointId) {
|
|
2192
|
+
return '/api/promptline/' + endpointId
|
|
2193
|
+
}
|
|
2194
|
+
|
|
2195
|
+
// Call PromptLine API
|
|
2196
|
+
async function callPromptLineAPI(endpointId, apiKey, input, variables = {}) {
|
|
2197
|
+
const proxyUrl = toProxyUrl(endpointId)
|
|
2198
|
+
const fullUrl = proxyUrl + '?mode=sync'
|
|
2199
|
+
|
|
2200
|
+
const res = await fetch(fullUrl, {
|
|
2201
|
+
method: 'POST',
|
|
2202
|
+
headers: {
|
|
2203
|
+
'Content-Type': 'application/json',
|
|
2204
|
+
...(apiKey ? { 'X-API-Key': apiKey } : {})
|
|
2205
|
+
},
|
|
2206
|
+
body: JSON.stringify({
|
|
2207
|
+
input: typeof input === 'string' ? { text: input } : input,
|
|
2208
|
+
variables
|
|
2209
|
+
})
|
|
2210
|
+
})
|
|
2211
|
+
|
|
2212
|
+
const data = await res.json()
|
|
2213
|
+
if (!res.ok || data.error) {
|
|
2214
|
+
throw new Error(data.detail || data.error?.message || data.error || res.statusText)
|
|
2215
|
+
}
|
|
2216
|
+
|
|
2217
|
+
const output = data.data?.output || data.data?.response || data.output || data.response
|
|
2218
|
+
return {
|
|
2219
|
+
response: typeof output === 'string' ? output : JSON.stringify(output),
|
|
2220
|
+
data: data.data || data
|
|
2221
|
+
}
|
|
2222
|
+
}
|
|
2223
|
+
|
|
2224
|
+
const PromptLineContext = createContext(null)
|
|
2225
|
+
|
|
2226
|
+
export function PromptLineProvider({ children }) {
|
|
2227
|
+
const value = {
|
|
2228
|
+
config: {},
|
|
2229
|
+
user: { id: 'dev-user', email: 'dev@example.com', name: 'Dev User' }
|
|
2230
|
+
}
|
|
2231
|
+
return (
|
|
2232
|
+
<PromptLineContext.Provider value={value}>
|
|
2233
|
+
{children}
|
|
2234
|
+
</PromptLineContext.Provider>
|
|
2235
|
+
)
|
|
2236
|
+
}
|
|
2237
|
+
|
|
2238
|
+
export function usePromptLine(bindingName) {
|
|
2239
|
+
const context = useContext(PromptLineContext)
|
|
2240
|
+
const [isLoading, setIsLoading] = useState(false)
|
|
2241
|
+
|
|
2242
|
+
const endpointId = useMemo(() => {
|
|
2243
|
+
const config = getConfig()
|
|
2244
|
+
return config[bindingName] || null
|
|
2245
|
+
}, [bindingName])
|
|
2246
|
+
|
|
2247
|
+
const execute = async (input, variables = {}) => {
|
|
2248
|
+
if (!endpointId) {
|
|
2249
|
+
console.warn(\`[PromptLine] Binding "\${bindingName}" not configured. Go to /_dev\`)
|
|
2250
|
+
return {
|
|
2251
|
+
response: \`[Not Configured] Binding "\${bindingName}" not set. Go to /_dev to configure it.\`,
|
|
2252
|
+
error: true
|
|
2253
|
+
}
|
|
2254
|
+
}
|
|
2255
|
+
|
|
2256
|
+
const apiKey = getApiKey()
|
|
2257
|
+
if (!apiKey) {
|
|
2258
|
+
return {
|
|
2259
|
+
response: '[No API Key] Configure your API key in /_dev',
|
|
2260
|
+
error: true
|
|
2261
|
+
}
|
|
2262
|
+
}
|
|
2263
|
+
|
|
2264
|
+
setIsLoading(true)
|
|
2265
|
+
try {
|
|
2266
|
+
const result = await callPromptLineAPI(endpointId, apiKey, input, variables)
|
|
2267
|
+
return result
|
|
2268
|
+
} catch (err) {
|
|
2269
|
+
console.error(\`[PromptLine] Error calling "\${bindingName}":\`, err)
|
|
2270
|
+
return { response: '[API Error] ' + err.message, error: true }
|
|
2271
|
+
} finally {
|
|
2272
|
+
setIsLoading(false)
|
|
2273
|
+
}
|
|
2274
|
+
}
|
|
2275
|
+
|
|
2276
|
+
return {
|
|
2277
|
+
execute,
|
|
2278
|
+
isLoading,
|
|
2279
|
+
isConfigured: !!endpointId,
|
|
2280
|
+
config: context?.config || {},
|
|
2281
|
+
user: context?.user || {},
|
|
2282
|
+
// Aliases for compatibility
|
|
2283
|
+
callAI: execute,
|
|
2284
|
+
submitForm: async (collection, data) => {
|
|
2285
|
+
console.log('[PromptLine] submitForm:', collection, data)
|
|
2286
|
+
return { success: true, id: 'mock-' + Date.now() }
|
|
2287
|
+
},
|
|
2288
|
+
fetchCollection: async () => ({ items: [], total: 0 })
|
|
2289
|
+
}
|
|
2290
|
+
}
|
|
2291
|
+
|
|
2292
|
+
// For SDK compatibility
|
|
2293
|
+
export const sdk = {
|
|
2294
|
+
ai: {
|
|
2295
|
+
call: async (bindingName, input) => {
|
|
2296
|
+
const config = getConfig()
|
|
2297
|
+
const endpointId = config[bindingName]
|
|
2298
|
+
if (!endpointId) {
|
|
2299
|
+
throw new Error(\`Binding "\${bindingName}" not configured\`)
|
|
2300
|
+
}
|
|
2301
|
+
return callPromptLineAPI(endpointId, getApiKey(), input)
|
|
2302
|
+
}
|
|
2303
|
+
},
|
|
2304
|
+
collections: {
|
|
2305
|
+
create: async (col, data) => ({ id: 'mock-' + Date.now() }),
|
|
2306
|
+
list: async () => ({ data: [] }),
|
|
2307
|
+
query: async () => [],
|
|
2308
|
+
get: async () => null,
|
|
2309
|
+
update: async () => ({ success: true }),
|
|
2310
|
+
delete: async () => ({ success: true })
|
|
2311
|
+
},
|
|
2312
|
+
variables: {},
|
|
2313
|
+
navigate: (path) => { window.location.href = path },
|
|
2314
|
+
getNavigationState: () => undefined
|
|
2315
|
+
}
|
|
2316
|
+
|
|
2317
|
+
export const initSDK = () => sdk
|
|
2318
|
+
export default sdk
|
|
2319
|
+
`,
|
|
2320
|
+
|
|
2321
|
+
// Dev admin page for configuring endpoints
|
|
2322
|
+
devAdmin: (bindings) => `/**
|
|
2323
|
+
* PromptLine Dev Admin - /_dev
|
|
2324
|
+
* Configure your AI endpoints here
|
|
2325
|
+
*
|
|
2326
|
+
* Auto-generated by: npx promptlineapp get
|
|
2327
|
+
*/
|
|
2328
|
+
import React, { useState, useEffect } from 'react'
|
|
2329
|
+
import { Link } from 'react-router-dom'
|
|
2330
|
+
import { availableBindings } from './sdk-mock'
|
|
2331
|
+
|
|
2332
|
+
const STORAGE_KEY = 'promptline_endpoints_config'
|
|
2333
|
+
const API_KEY_STORAGE = 'promptline_api_key'
|
|
2334
|
+
|
|
2335
|
+
export default function DevAdmin() {
|
|
2336
|
+
const [apiKey, setApiKey] = useState('')
|
|
2337
|
+
const [endpoints, setEndpoints] = useState({})
|
|
2338
|
+
const [testBinding, setTestBinding] = useState('')
|
|
2339
|
+
const [testInput, setTestInput] = useState('Hello, test!')
|
|
2340
|
+
const [testResult, setTestResult] = useState('')
|
|
2341
|
+
const [testing, setTesting] = useState(false)
|
|
2342
|
+
const [saved, setSaved] = useState(false)
|
|
2343
|
+
|
|
2344
|
+
useEffect(() => {
|
|
2345
|
+
setApiKey(localStorage.getItem(API_KEY_STORAGE) || '')
|
|
2346
|
+
try {
|
|
2347
|
+
setEndpoints(JSON.parse(localStorage.getItem(STORAGE_KEY) || '{}'))
|
|
2348
|
+
} catch {}
|
|
2349
|
+
if (availableBindings.length > 0) {
|
|
2350
|
+
setTestBinding(availableBindings[0].name)
|
|
2351
|
+
}
|
|
2352
|
+
}, [])
|
|
2353
|
+
|
|
2354
|
+
const save = () => {
|
|
2355
|
+
localStorage.setItem(API_KEY_STORAGE, apiKey)
|
|
2356
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(endpoints))
|
|
2357
|
+
setSaved(true)
|
|
2358
|
+
setTimeout(() => setSaved(false), 2000)
|
|
2359
|
+
}
|
|
2360
|
+
|
|
2361
|
+
const updateEndpoint = (name, value) => {
|
|
2362
|
+
setEndpoints(prev => ({ ...prev, [name]: value }))
|
|
2363
|
+
}
|
|
2364
|
+
|
|
2365
|
+
const testEndpoint = async () => {
|
|
2366
|
+
if (!testBinding || !endpoints[testBinding]) {
|
|
2367
|
+
setTestResult('Error: Endpoint not configured')
|
|
2368
|
+
return
|
|
2369
|
+
}
|
|
2370
|
+
if (!apiKey) {
|
|
2371
|
+
setTestResult('Error: API Key not set')
|
|
2372
|
+
return
|
|
2373
|
+
}
|
|
2374
|
+
|
|
2375
|
+
setTesting(true)
|
|
2376
|
+
setTestResult('Calling API...')
|
|
2377
|
+
|
|
2378
|
+
try {
|
|
2379
|
+
const res = await fetch('/api/promptline/' + endpoints[testBinding] + '?mode=sync', {
|
|
2380
|
+
method: 'POST',
|
|
2381
|
+
headers: {
|
|
2382
|
+
'Content-Type': 'application/json',
|
|
2383
|
+
'X-API-Key': apiKey
|
|
2384
|
+
},
|
|
2385
|
+
body: JSON.stringify({ input: { text: testInput } })
|
|
2386
|
+
})
|
|
2387
|
+
const data = await res.json()
|
|
2388
|
+
if (!res.ok || data.error) {
|
|
2389
|
+
setTestResult('Error: ' + (data.detail || data.error?.message || res.statusText))
|
|
2390
|
+
} else {
|
|
2391
|
+
const output = data.data?.output || data.data?.response || data.output || data.response
|
|
2392
|
+
setTestResult(typeof output === 'string' ? output : JSON.stringify(output, null, 2))
|
|
2393
|
+
}
|
|
2394
|
+
} catch (err) {
|
|
2395
|
+
setTestResult('Error: ' + err.message)
|
|
2396
|
+
}
|
|
2397
|
+
setTesting(false)
|
|
2398
|
+
}
|
|
2399
|
+
|
|
2400
|
+
const configuredCount = Object.values(endpoints).filter(Boolean).length
|
|
2401
|
+
|
|
2402
|
+
return (
|
|
2403
|
+
<div style={{ maxWidth: 900, margin: '0 auto', padding: 24, fontFamily: 'system-ui, sans-serif' }}>
|
|
2404
|
+
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 24 }}>
|
|
2405
|
+
<h1 style={{ margin: 0, color: '#f59e0b' }}>/_dev</h1>
|
|
2406
|
+
<Link to="/" style={{ color: '#6366f1' }}>← Back to App</Link>
|
|
2407
|
+
</div>
|
|
2408
|
+
|
|
2409
|
+
<div style={{ background: '#fefce8', border: '1px solid #fde047', borderRadius: 8, padding: 16, marginBottom: 24 }}>
|
|
2410
|
+
<strong>⚠️ Development Only</strong>
|
|
2411
|
+
<p style={{ margin: '8px 0 0', fontSize: 14 }}>Configure your PromptLine endpoints here. Never commit API keys to git!</p>
|
|
2412
|
+
</div>
|
|
2413
|
+
|
|
2414
|
+
{/* API Key */}
|
|
2415
|
+
<div style={{ background: '#fff', border: '1px solid #e5e7eb', borderRadius: 8, padding: 16, marginBottom: 24 }}>
|
|
2416
|
+
<h2 style={{ margin: '0 0 16px', fontSize: 18 }}>🔑 API Key</h2>
|
|
2417
|
+
<input
|
|
2418
|
+
type="password"
|
|
2419
|
+
value={apiKey}
|
|
2420
|
+
onChange={e => setApiKey(e.target.value)}
|
|
2421
|
+
placeholder="sk_org_..."
|
|
2422
|
+
style={{ width: '100%', padding: 12, border: '1px solid #d1d5db', borderRadius: 6, fontSize: 14 }}
|
|
2423
|
+
/>
|
|
2424
|
+
</div>
|
|
2425
|
+
|
|
2426
|
+
{/* Bindings */}
|
|
2427
|
+
<div style={{ background: '#fff', border: '1px solid #e5e7eb', borderRadius: 8, padding: 16, marginBottom: 24 }}>
|
|
2428
|
+
<h2 style={{ margin: '0 0 16px', fontSize: 18 }}>
|
|
2429
|
+
📋 AI Bindings ({configuredCount}/{availableBindings.length} configured)
|
|
2430
|
+
</h2>
|
|
2431
|
+
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
|
2432
|
+
<thead>
|
|
2433
|
+
<tr style={{ borderBottom: '2px solid #e5e7eb' }}>
|
|
2434
|
+
<th style={{ textAlign: 'left', padding: 8 }}>Binding</th>
|
|
2435
|
+
<th style={{ textAlign: 'left', padding: 8 }}>Endpoint ID</th>
|
|
2436
|
+
<th style={{ textAlign: 'center', padding: 8, width: 60 }}>Status</th>
|
|
2437
|
+
</tr>
|
|
2438
|
+
</thead>
|
|
2439
|
+
<tbody>
|
|
2440
|
+
{availableBindings.map(b => (
|
|
2441
|
+
<tr key={b.name} style={{ borderBottom: '1px solid #f3f4f6' }}>
|
|
2442
|
+
<td style={{ padding: 8 }}>
|
|
2443
|
+
<div style={{ fontWeight: 500 }}>{b.name}</div>
|
|
2444
|
+
<div style={{ fontSize: 12, color: '#6b7280' }}>{b.description?.slice(0, 60) || b.type}</div>
|
|
2445
|
+
</td>
|
|
2446
|
+
<td style={{ padding: 8 }}>
|
|
2447
|
+
<input
|
|
2448
|
+
type="text"
|
|
2449
|
+
value={endpoints[b.name] || ''}
|
|
2450
|
+
onChange={e => updateEndpoint(b.name, e.target.value)}
|
|
2451
|
+
placeholder="ep_..."
|
|
2452
|
+
style={{ width: '100%', padding: 8, border: '1px solid #d1d5db', borderRadius: 4, fontSize: 13 }}
|
|
2453
|
+
/>
|
|
2454
|
+
</td>
|
|
2455
|
+
<td style={{ padding: 8, textAlign: 'center' }}>
|
|
2456
|
+
<span style={{
|
|
2457
|
+
display: 'inline-block',
|
|
2458
|
+
width: 12,
|
|
2459
|
+
height: 12,
|
|
2460
|
+
borderRadius: '50%',
|
|
2461
|
+
background: endpoints[b.name] ? '#22c55e' : '#d1d5db'
|
|
2462
|
+
}} />
|
|
2463
|
+
</td>
|
|
2464
|
+
</tr>
|
|
2465
|
+
))}
|
|
2466
|
+
</tbody>
|
|
2467
|
+
</table>
|
|
2468
|
+
</div>
|
|
2469
|
+
|
|
2470
|
+
{/* Save Button */}
|
|
2471
|
+
<button
|
|
2472
|
+
onClick={save}
|
|
2473
|
+
style={{
|
|
2474
|
+
width: '100%',
|
|
2475
|
+
padding: 14,
|
|
2476
|
+
background: saved ? '#22c55e' : '#6366f1',
|
|
2477
|
+
color: '#fff',
|
|
2478
|
+
border: 'none',
|
|
2479
|
+
borderRadius: 8,
|
|
2480
|
+
fontSize: 16,
|
|
2481
|
+
fontWeight: 600,
|
|
2482
|
+
cursor: 'pointer',
|
|
2483
|
+
marginBottom: 24
|
|
2484
|
+
}}
|
|
2485
|
+
>
|
|
2486
|
+
{saved ? '✓ Saved!' : 'Save Configuration'}
|
|
2487
|
+
</button>
|
|
2488
|
+
|
|
2489
|
+
{/* Test Panel */}
|
|
2490
|
+
<div style={{ background: '#fff', border: '1px solid #e5e7eb', borderRadius: 8, padding: 16 }}>
|
|
2491
|
+
<h2 style={{ margin: '0 0 16px', fontSize: 18 }}>🧪 Test</h2>
|
|
2492
|
+
<div style={{ display: 'flex', gap: 8, marginBottom: 12 }}>
|
|
2493
|
+
<select
|
|
2494
|
+
value={testBinding}
|
|
2495
|
+
onChange={e => setTestBinding(e.target.value)}
|
|
2496
|
+
style={{ flex: 1, padding: 8, border: '1px solid #d1d5db', borderRadius: 4 }}
|
|
2497
|
+
>
|
|
2498
|
+
{availableBindings.map(b => (
|
|
2499
|
+
<option key={b.name} value={b.name}>{b.name}</option>
|
|
2500
|
+
))}
|
|
2501
|
+
</select>
|
|
2502
|
+
<button
|
|
2503
|
+
onClick={testEndpoint}
|
|
2504
|
+
disabled={testing}
|
|
2505
|
+
style={{
|
|
2506
|
+
padding: '8px 16px',
|
|
2507
|
+
background: '#6366f1',
|
|
2508
|
+
color: '#fff',
|
|
2509
|
+
border: 'none',
|
|
2510
|
+
borderRadius: 4,
|
|
2511
|
+
cursor: testing ? 'wait' : 'pointer'
|
|
2512
|
+
}}
|
|
2513
|
+
>
|
|
2514
|
+
{testing ? '...' : 'Test'}
|
|
2515
|
+
</button>
|
|
2516
|
+
</div>
|
|
2517
|
+
<textarea
|
|
2518
|
+
value={testInput}
|
|
2519
|
+
onChange={e => setTestInput(e.target.value)}
|
|
2520
|
+
placeholder="Test input..."
|
|
2521
|
+
rows={2}
|
|
2522
|
+
style={{ width: '100%', padding: 8, border: '1px solid #d1d5db', borderRadius: 4, marginBottom: 12, resize: 'vertical' }}
|
|
2523
|
+
/>
|
|
2524
|
+
{testResult && (
|
|
2525
|
+
<pre style={{
|
|
2526
|
+
background: '#f9fafb',
|
|
2527
|
+
padding: 12,
|
|
2528
|
+
borderRadius: 4,
|
|
2529
|
+
fontSize: 13,
|
|
2530
|
+
overflow: 'auto',
|
|
2531
|
+
maxHeight: 200,
|
|
2532
|
+
whiteSpace: 'pre-wrap'
|
|
2533
|
+
}}>
|
|
2534
|
+
{testResult}
|
|
2535
|
+
</pre>
|
|
2536
|
+
)}
|
|
2537
|
+
</div>
|
|
2538
|
+
</div>
|
|
2539
|
+
)
|
|
2540
|
+
}
|
|
2541
|
+
`,
|
|
2542
|
+
|
|
2543
|
+
// Vite config addition for proxy
|
|
2544
|
+
viteProxyConfig: `
|
|
2545
|
+
// PromptLine API Proxy (added by npx promptlineapp get)
|
|
2546
|
+
proxy: {
|
|
2547
|
+
'/api/promptline': {
|
|
2548
|
+
target: 'https://app.promptlineops.com',
|
|
2549
|
+
changeOrigin: true,
|
|
2550
|
+
rewrite: (path) => path.replace(/^\\/api\\/promptline/, '/api/v1/live'),
|
|
2551
|
+
secure: true
|
|
2552
|
+
}
|
|
2553
|
+
},`
|
|
2554
|
+
}
|
|
2555
|
+
|
|
2556
|
+
// Get project from Git repository
|
|
2557
|
+
async function getProject(gitUrl, destName) {
|
|
2558
|
+
const { execSync } = require('child_process');
|
|
2559
|
+
|
|
2560
|
+
printLogo();
|
|
2561
|
+
|
|
2562
|
+
// Extract repo name from URL if destName not provided
|
|
2563
|
+
if (!destName) {
|
|
2564
|
+
const match = gitUrl.match(/\/([^\/]+?)(\.git)?$/) || gitUrl.match(/:([^\/]+?)(\.git)?$/);
|
|
2565
|
+
destName = match ? match[1] : 'promptline-app';
|
|
2566
|
+
}
|
|
2567
|
+
|
|
2568
|
+
// Validate destination name
|
|
2569
|
+
const validated = validatePackageName(destName);
|
|
2570
|
+
if (!validated.valid) {
|
|
2571
|
+
error(`Invalid project name "${destName}": ${validated.error}`);
|
|
2572
|
+
process.exit(1);
|
|
2573
|
+
}
|
|
2574
|
+
|
|
2575
|
+
const targetDir = path.resolve(process.cwd(), destName);
|
|
2576
|
+
|
|
2577
|
+
if (fs.existsSync(targetDir)) {
|
|
2578
|
+
error(`Directory "${destName}" already exists.`);
|
|
2579
|
+
process.exit(1);
|
|
2580
|
+
}
|
|
2581
|
+
|
|
2582
|
+
console.log(`${c.cyan}→${c.reset} Cloning from ${c.dim}${gitUrl}${c.reset}`);
|
|
2583
|
+
console.log(`${c.cyan}→${c.reset} Destination: ${c.green}${destName}${c.reset}\n`);
|
|
2584
|
+
|
|
2585
|
+
try {
|
|
2586
|
+
// Clone
|
|
2587
|
+
execSync(`git clone --depth 1 "${gitUrl}" "${destName}"`, {
|
|
2588
|
+
stdio: 'inherit',
|
|
2589
|
+
cwd: process.cwd()
|
|
2590
|
+
});
|
|
2591
|
+
|
|
2592
|
+
// Remove .git
|
|
2593
|
+
const gitDir = path.join(targetDir, '.git');
|
|
2594
|
+
if (fs.existsSync(gitDir)) {
|
|
2595
|
+
fs.rmSync(gitDir, { recursive: true });
|
|
2596
|
+
}
|
|
2597
|
+
success('Cloned repository');
|
|
2598
|
+
|
|
2599
|
+
// Init fresh git
|
|
2600
|
+
try {
|
|
2601
|
+
execSync('git init', { cwd: targetDir, stdio: 'pipe' });
|
|
2602
|
+
success('Initialized new git repository');
|
|
2603
|
+
} catch (e) {}
|
|
2604
|
+
|
|
2605
|
+
// Detect structure
|
|
2606
|
+
const hasDevRuntime = fs.existsSync(path.join(targetDir, 'dev-runtime'));
|
|
2607
|
+
const hasPromptlineYaml = fs.existsSync(path.join(targetDir, 'promptline.yaml'));
|
|
2608
|
+
|
|
2609
|
+
// Parse bindings from promptline.yaml
|
|
2610
|
+
let bindings = [];
|
|
2611
|
+
if (hasPromptlineYaml) {
|
|
2612
|
+
try {
|
|
2613
|
+
const yamlContent = fs.readFileSync(path.join(targetDir, 'promptline.yaml'), 'utf8');
|
|
2614
|
+
bindings = parseBindingsFromYaml(yamlContent);
|
|
2615
|
+
success(`Detected ${bindings.length} AI bindings from promptline.yaml`);
|
|
2616
|
+
} catch (e) {
|
|
2617
|
+
console.log(`${c.yellow}!${c.reset} Could not parse promptline.yaml: ${e.message}`);
|
|
2618
|
+
}
|
|
2619
|
+
}
|
|
2620
|
+
|
|
2621
|
+
// Inject dev files if we have dev-runtime
|
|
2622
|
+
if (hasDevRuntime && bindings.length > 0) {
|
|
2623
|
+
const srcDir = path.join(targetDir, 'dev-runtime', 'src');
|
|
2624
|
+
|
|
2625
|
+
// Backup original sdk-mock if exists
|
|
2626
|
+
const sdkPath = path.join(srcDir, 'sdk-mock.ts');
|
|
2627
|
+
if (fs.existsSync(sdkPath)) {
|
|
2628
|
+
fs.renameSync(sdkPath, path.join(srcDir, 'sdk-mock.original.ts'));
|
|
2629
|
+
success('Backed up original sdk-mock.ts');
|
|
2630
|
+
}
|
|
2631
|
+
|
|
2632
|
+
// Write new SDK (as .tsx for JSX support)
|
|
2633
|
+
fs.writeFileSync(path.join(srcDir, 'sdk-mock.tsx'), devTemplates.sdkHybrid(bindings));
|
|
2634
|
+
success('Injected hybrid SDK (sdk-mock.tsx)');
|
|
2635
|
+
|
|
2636
|
+
// Write DevAdmin page
|
|
2637
|
+
fs.writeFileSync(path.join(srcDir, 'DevAdmin.tsx'), devTemplates.devAdmin(bindings));
|
|
2638
|
+
success('Injected /_dev page (DevAdmin.tsx)');
|
|
2639
|
+
|
|
2640
|
+
// Modify Router.tsx to add /_dev route
|
|
2641
|
+
const routerPath = path.join(srcDir, 'Router.tsx');
|
|
2642
|
+
if (fs.existsSync(routerPath)) {
|
|
2643
|
+
let routerContent = fs.readFileSync(routerPath, 'utf8');
|
|
2644
|
+
|
|
2645
|
+
// Add import if not present
|
|
2646
|
+
if (!routerContent.includes('DevAdmin')) {
|
|
2647
|
+
routerContent = routerContent.replace(
|
|
2648
|
+
/(import .+ from ['"].+['"];?\n)/,
|
|
2649
|
+
"$1import DevAdmin from './DevAdmin'\n"
|
|
2650
|
+
);
|
|
2651
|
+
}
|
|
2652
|
+
|
|
2653
|
+
// Add route if not present
|
|
2654
|
+
if (!routerContent.includes('/_dev')) {
|
|
2655
|
+
// Try to find the routes array and add /_dev
|
|
2656
|
+
routerContent = routerContent.replace(
|
|
2657
|
+
/(<Route\s+path=["']\/["']\s+element=)/,
|
|
2658
|
+
"<Route path=\"/_dev\" element={<DevAdmin />} />\n $1"
|
|
2659
|
+
);
|
|
2660
|
+
}
|
|
2661
|
+
|
|
2662
|
+
fs.writeFileSync(routerPath, routerContent);
|
|
2663
|
+
success('Added /_dev route to Router.tsx');
|
|
2664
|
+
}
|
|
2665
|
+
|
|
2666
|
+
// Check and update vite.config.ts for proxy
|
|
2667
|
+
const viteConfigPath = path.join(targetDir, 'dev-runtime', 'vite.config.ts');
|
|
2668
|
+
if (fs.existsSync(viteConfigPath)) {
|
|
2669
|
+
let viteContent = fs.readFileSync(viteConfigPath, 'utf8');
|
|
2670
|
+
if (!viteContent.includes('/api/promptline')) {
|
|
2671
|
+
// Add proxy config to server section
|
|
2672
|
+
if (viteContent.includes('server:')) {
|
|
2673
|
+
viteContent = viteContent.replace(
|
|
2674
|
+
/server:\s*\{/,
|
|
2675
|
+
'server: {' + devTemplates.viteProxyConfig
|
|
2676
|
+
);
|
|
2677
|
+
} else {
|
|
2678
|
+
// Add server section before the closing of defineConfig
|
|
2679
|
+
viteContent = viteContent.replace(
|
|
2680
|
+
/}\s*\)\s*$/,
|
|
2681
|
+
`,\n server: {${devTemplates.viteProxyConfig}\n }\n})`
|
|
2682
|
+
);
|
|
2683
|
+
}
|
|
2684
|
+
fs.writeFileSync(viteConfigPath, viteContent);
|
|
2685
|
+
success('Added API proxy to vite.config.ts');
|
|
2686
|
+
}
|
|
2687
|
+
}
|
|
2688
|
+
}
|
|
2689
|
+
|
|
2690
|
+
// Determine dev directory and port
|
|
2691
|
+
const devDir = hasDevRuntime ? 'dev-runtime' : '.';
|
|
2692
|
+
let devPort = '5173';
|
|
2693
|
+
|
|
2694
|
+
// Try to detect port from vite config
|
|
2695
|
+
const viteConfigPath = path.join(targetDir, hasDevRuntime ? 'dev-runtime' : '', 'vite.config.ts');
|
|
2696
|
+
if (fs.existsSync(viteConfigPath)) {
|
|
2697
|
+
const viteContent = fs.readFileSync(viteConfigPath, 'utf8');
|
|
2698
|
+
const portMatch = viteContent.match(/port:\s*(\d+)/);
|
|
2699
|
+
if (portMatch) devPort = portMatch[1];
|
|
2700
|
+
}
|
|
2701
|
+
|
|
2702
|
+
// Success message
|
|
2703
|
+
console.log(`
|
|
2704
|
+
${c.green}╔═══════════════════════════════════════════════════════════╗
|
|
2705
|
+
║ ║
|
|
2706
|
+
║ ${c.bold}Success!${c.reset}${c.green} Project ready in ${c.bold}${destName}${c.reset}${c.green} ║
|
|
2707
|
+
║ ║
|
|
2708
|
+
╚═══════════════════════════════════════════════════════════════╝${c.reset}
|
|
2709
|
+
|
|
2710
|
+
${c.bold}Next steps:${c.reset}
|
|
2711
|
+
|
|
2712
|
+
${c.cyan}cd ${destName}${hasDevRuntime ? '/dev-runtime' : ''}${c.reset}
|
|
2713
|
+
${c.cyan}npm install${c.reset}
|
|
2714
|
+
${c.cyan}npm run dev${c.reset}
|
|
2715
|
+
|
|
2716
|
+
${c.bold}Configure AI endpoints:${c.reset}
|
|
2717
|
+
|
|
2718
|
+
Open ${c.yellow}http://localhost:${devPort}/_dev${c.reset}
|
|
2719
|
+
1. Enter your API key
|
|
2720
|
+
2. Add endpoint IDs for each binding
|
|
2721
|
+
3. Test your endpoints
|
|
2722
|
+
|
|
2723
|
+
${c.bold}${bindings.length} AI bindings detected:${c.reset}
|
|
2724
|
+
${bindings.slice(0, 5).map(b => ` ${c.dim}•${c.reset} ${b.name}`).join('\n')}${bindings.length > 5 ? `\n ${c.dim}... and ${bindings.length - 5} more${c.reset}` : ''}
|
|
2725
|
+
|
|
2726
|
+
${c.bold}Documentation:${c.reset} ${c.cyan}https://docs.promptlineops.com${c.reset}
|
|
2727
|
+
`);
|
|
2728
|
+
|
|
2729
|
+
} catch (err) {
|
|
2730
|
+
error(`Failed to clone repository: ${err.message}`);
|
|
2731
|
+
// Cleanup on failure
|
|
2732
|
+
if (fs.existsSync(targetDir)) {
|
|
2733
|
+
fs.rmSync(targetDir, { recursive: true });
|
|
2734
|
+
}
|
|
2735
|
+
process.exit(1);
|
|
2736
|
+
}
|
|
2737
|
+
}
|
|
2738
|
+
|
|
2088
2739
|
function showHelp() {
|
|
2089
2740
|
console.log(`
|
|
2090
|
-
${c.bold}
|
|
2741
|
+
${c.bold}promptlineapp${c.reset} - Create and get PromptLine applications
|
|
2091
2742
|
|
|
2092
2743
|
${c.bold}Usage:${c.reset}
|
|
2093
|
-
npx
|
|
2744
|
+
npx promptlineapp <app-name> [options] Create new app from scratch
|
|
2745
|
+
npx promptlineapp get <git-url> [name] Get existing app from Git
|
|
2094
2746
|
|
|
2095
|
-
${c.bold}
|
|
2747
|
+
${c.bold}Create options:${c.reset}
|
|
2096
2748
|
-p, --preset <preset> Template preset (full-app, api)
|
|
2097
2749
|
-y, --yes Skip prompts, use defaults
|
|
2750
|
+
|
|
2751
|
+
${c.bold}General:${c.reset}
|
|
2098
2752
|
-h, --help Show this help
|
|
2099
2753
|
-v, --version Show version
|
|
2100
2754
|
|
|
2101
2755
|
${c.bold}Examples:${c.reset}
|
|
2102
|
-
|
|
2103
|
-
npx
|
|
2104
|
-
npx
|
|
2756
|
+
${c.dim}# Create new app from scratch${c.reset}
|
|
2757
|
+
npx promptlineapp my-app
|
|
2758
|
+
npx promptlineapp my-api --preset api
|
|
2759
|
+
|
|
2760
|
+
${c.dim}# Get existing use case from Git${c.reset}
|
|
2761
|
+
npx promptlineapp get https://github.com/promptline/claude-realestate.git
|
|
2762
|
+
npx promptlineapp get git@github.com:promptline/menumind.git my-restaurant
|
|
2105
2763
|
|
|
2106
2764
|
${c.bold}Presets:${c.reset}
|
|
2107
2765
|
full-app Complete app with pages, dashboard & AI (default)
|
|
@@ -2128,13 +2786,29 @@ async function main() {
|
|
|
2128
2786
|
process.exit(0);
|
|
2129
2787
|
}
|
|
2130
2788
|
|
|
2789
|
+
// Route by command
|
|
2790
|
+
if (options.command === 'get') {
|
|
2791
|
+
if (!options.source) {
|
|
2792
|
+
printLogo();
|
|
2793
|
+
error('Please specify a Git URL:');
|
|
2794
|
+
console.log(`\n ${c.cyan}npx promptlineapp get${c.reset} ${c.green}<git-url>${c.reset} [name]\n`);
|
|
2795
|
+
console.log('For example:');
|
|
2796
|
+
console.log(` ${c.cyan}npx promptlineapp get${c.reset} ${c.green}https://github.com/promptline/claude-realestate.git${c.reset}\n`);
|
|
2797
|
+
console.log(`Run ${c.cyan}npx promptlineapp --help${c.reset} for more options.`);
|
|
2798
|
+
process.exit(1);
|
|
2799
|
+
}
|
|
2800
|
+
await getProject(options.source, options.name);
|
|
2801
|
+
return;
|
|
2802
|
+
}
|
|
2803
|
+
|
|
2804
|
+
// Default: create new app
|
|
2131
2805
|
if (!options.name) {
|
|
2132
2806
|
printLogo();
|
|
2133
2807
|
error('Please specify a project name:');
|
|
2134
|
-
console.log(`\n ${c.cyan}npx
|
|
2808
|
+
console.log(`\n ${c.cyan}npx promptlineapp${c.reset} ${c.green}<app-name>${c.reset}\n`);
|
|
2135
2809
|
console.log('For example:');
|
|
2136
|
-
console.log(` ${c.cyan}npx
|
|
2137
|
-
console.log(`Run ${c.cyan}npx
|
|
2810
|
+
console.log(` ${c.cyan}npx promptlineapp${c.reset} ${c.green}my-restaurant-app${c.reset}\n`);
|
|
2811
|
+
console.log(`Run ${c.cyan}npx promptlineapp --help${c.reset} for more options.`);
|
|
2138
2812
|
process.exit(1);
|
|
2139
2813
|
}
|
|
2140
2814
|
|