promptlineapp 1.6.0 → 1.8.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.
Files changed (2) hide show
  1. package/bin/cli.js +715 -38
  2. package/package.json +1 -1
package/bin/cli.js CHANGED
@@ -4,14 +4,75 @@
4
4
  * create-promptline-app CLI
5
5
  *
6
6
  * Usage:
7
- * npx create-promptline-app my-app
8
- * npx create-promptline-app my-app --preset saas
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',
@@ -2098,6 +2159,547 @@ function parseArgs() {
2098
2159
  return options;
2099
2160
  }
2100
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
+ // Helper to extract endpoint ID from URL or return as-is
2336
+ function extractEndpointId(value) {
2337
+ if (!value) return ''
2338
+ // If it's a full URL, extract the endpoint ID
2339
+ const match = value.match(/\\/api\\/v1\\/live\\/([^?/]+)/) || value.match(/\\/live\\/([^?/]+)/)
2340
+ if (match) return match[1]
2341
+ // If it starts with ep_ or similar, use as-is
2342
+ return value
2343
+ }
2344
+
2345
+ export default function DevAdmin() {
2346
+ const [apiKey, setApiKey] = useState('')
2347
+ const [endpoints, setEndpoints] = useState({})
2348
+ const [endpointStatus, setEndpointStatus] = useState({}) // { bindingName: { alive: bool, info: obj, loading: string } }
2349
+ const [testBinding, setTestBinding] = useState('')
2350
+ const [testInput, setTestInput] = useState('Hello, test!')
2351
+ const [testResult, setTestResult] = useState('')
2352
+ const [testing, setTesting] = useState(false)
2353
+ const [saved, setSaved] = useState(false)
2354
+
2355
+ useEffect(() => {
2356
+ setApiKey(localStorage.getItem(API_KEY_STORAGE) || '')
2357
+ try {
2358
+ setEndpoints(JSON.parse(localStorage.getItem(STORAGE_KEY) || '{}'))
2359
+ } catch {}
2360
+ if (availableBindings.length > 0) {
2361
+ setTestBinding(availableBindings[0].name)
2362
+ }
2363
+ }, [])
2364
+
2365
+ const save = () => {
2366
+ // Clean endpoint values (extract IDs from URLs)
2367
+ const cleanedEndpoints = {}
2368
+ Object.entries(endpoints).forEach(([key, value]) => {
2369
+ cleanedEndpoints[key] = extractEndpointId(value)
2370
+ })
2371
+ setEndpoints(cleanedEndpoints)
2372
+ localStorage.setItem(API_KEY_STORAGE, apiKey)
2373
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(cleanedEndpoints))
2374
+ setSaved(true)
2375
+ setTimeout(() => setSaved(false), 2000)
2376
+ }
2377
+
2378
+ const updateEndpoint = (name, value) => {
2379
+ setEndpoints(prev => ({ ...prev, [name]: value }))
2380
+ }
2381
+
2382
+ // Check if endpoint is alive
2383
+ const checkIsAlive = async (bindingName) => {
2384
+ const endpointId = extractEndpointId(endpoints[bindingName])
2385
+ if (!endpointId) {
2386
+ setEndpointStatus(prev => ({ ...prev, [bindingName]: { ...prev[bindingName], alive: false, error: 'No endpoint ID' } }))
2387
+ return
2388
+ }
2389
+
2390
+ setEndpointStatus(prev => ({ ...prev, [bindingName]: { ...prev[bindingName], loading: 'alive' } }))
2391
+
2392
+ try {
2393
+ const res = await fetch('/api/promptline/' + endpointId + '/isAlive', {
2394
+ method: 'GET',
2395
+ headers: apiKey ? { 'X-API-Key': apiKey } : {}
2396
+ })
2397
+ const data = await res.json().catch(() => ({}))
2398
+ setEndpointStatus(prev => ({
2399
+ ...prev,
2400
+ [bindingName]: {
2401
+ ...prev[bindingName],
2402
+ alive: res.ok && data.alive === true,
2403
+ loading: null,
2404
+ error: res.ok ? null : (data.detail || res.statusText)
2405
+ }
2406
+ }))
2407
+ } catch (err) {
2408
+ setEndpointStatus(prev => ({
2409
+ ...prev,
2410
+ [bindingName]: { ...prev[bindingName], alive: false, loading: null, error: err.message }
2411
+ }))
2412
+ }
2413
+ }
2414
+
2415
+ // Get endpoint info
2416
+ const fetchInfo = async (bindingName) => {
2417
+ const endpointId = extractEndpointId(endpoints[bindingName])
2418
+ if (!endpointId) {
2419
+ setEndpointStatus(prev => ({ ...prev, [bindingName]: { ...prev[bindingName], error: 'No endpoint ID' } }))
2420
+ return
2421
+ }
2422
+
2423
+ setEndpointStatus(prev => ({ ...prev, [bindingName]: { ...prev[bindingName], loading: 'info' } }))
2424
+
2425
+ try {
2426
+ const res = await fetch('/api/promptline/' + endpointId + '/info', {
2427
+ method: 'GET',
2428
+ headers: apiKey ? { 'X-API-Key': apiKey } : {}
2429
+ })
2430
+ const data = await res.json().catch(() => ({}))
2431
+ setEndpointStatus(prev => ({
2432
+ ...prev,
2433
+ [bindingName]: {
2434
+ ...prev[bindingName],
2435
+ info: res.ok ? data : null,
2436
+ loading: null,
2437
+ error: res.ok ? null : (data.detail || res.statusText)
2438
+ }
2439
+ }))
2440
+ } catch (err) {
2441
+ setEndpointStatus(prev => ({
2442
+ ...prev,
2443
+ [bindingName]: { ...prev[bindingName], info: null, loading: null, error: err.message }
2444
+ }))
2445
+ }
2446
+ }
2447
+
2448
+ const testEndpoint = async () => {
2449
+ const endpointId = extractEndpointId(endpoints[testBinding])
2450
+ if (!testBinding || !endpointId) {
2451
+ setTestResult('Error: Endpoint not configured')
2452
+ return
2453
+ }
2454
+ if (!apiKey) {
2455
+ setTestResult('Error: API Key not set')
2456
+ return
2457
+ }
2458
+
2459
+ setTesting(true)
2460
+ setTestResult('Calling API...')
2461
+
2462
+ try {
2463
+ const res = await fetch('/api/promptline/' + endpointId + '?mode=sync', {
2464
+ method: 'POST',
2465
+ headers: {
2466
+ 'Content-Type': 'application/json',
2467
+ 'X-API-Key': apiKey
2468
+ },
2469
+ body: JSON.stringify({ input: { text: testInput } })
2470
+ })
2471
+ const data = await res.json()
2472
+ if (!res.ok || data.error) {
2473
+ setTestResult('Error: ' + (data.detail || data.error?.message || res.statusText))
2474
+ } else {
2475
+ const output = data.data?.output || data.data?.response || data.output || data.response
2476
+ setTestResult(typeof output === 'string' ? output : JSON.stringify(output, null, 2))
2477
+ }
2478
+ } catch (err) {
2479
+ setTestResult('Error: ' + err.message)
2480
+ }
2481
+ setTesting(false)
2482
+ }
2483
+
2484
+ const configuredCount = Object.values(endpoints).filter(Boolean).length
2485
+
2486
+ const btnStyle = (loading) => ({
2487
+ padding: '4px 8px',
2488
+ fontSize: 11,
2489
+ border: '1px solid #d1d5db',
2490
+ borderRadius: 4,
2491
+ background: loading ? '#f3f4f6' : '#fff',
2492
+ cursor: loading ? 'wait' : 'pointer',
2493
+ marginRight: 4
2494
+ })
2495
+
2496
+ return (
2497
+ <div style={{ maxWidth: 1000, margin: '0 auto', padding: 24, fontFamily: 'system-ui, sans-serif' }}>
2498
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 24 }}>
2499
+ <h1 style={{ margin: 0, color: '#f59e0b' }}>/_dev</h1>
2500
+ <Link to="/" style={{ color: '#6366f1' }}>← Back to App</Link>
2501
+ </div>
2502
+
2503
+ <div style={{ background: '#fefce8', border: '1px solid #fde047', borderRadius: 8, padding: 16, marginBottom: 24 }}>
2504
+ <strong>⚠️ Development Only</strong>
2505
+ <p style={{ margin: '8px 0 0', fontSize: 14 }}>Configure your PromptLine endpoints here. Never commit API keys to git!</p>
2506
+ </div>
2507
+
2508
+ {/* API Key */}
2509
+ <div style={{ background: '#fff', border: '1px solid #e5e7eb', borderRadius: 8, padding: 16, marginBottom: 24 }}>
2510
+ <h2 style={{ margin: '0 0 16px', fontSize: 18 }}>🔑 API Key</h2>
2511
+ <input
2512
+ type="password"
2513
+ value={apiKey}
2514
+ onChange={e => setApiKey(e.target.value)}
2515
+ placeholder="sk_org_... or sk_ws_..."
2516
+ style={{ width: '100%', padding: 12, border: '1px solid #d1d5db', borderRadius: 6, fontSize: 14 }}
2517
+ />
2518
+ <p style={{ margin: '8px 0 0', fontSize: 12, color: '#6b7280' }}>
2519
+ This API key will be used for Info and IsAlive checks
2520
+ </p>
2521
+ </div>
2522
+
2523
+ {/* Bindings */}
2524
+ <div style={{ background: '#fff', border: '1px solid #e5e7eb', borderRadius: 8, padding: 16, marginBottom: 24 }}>
2525
+ <h2 style={{ margin: '0 0 16px', fontSize: 18 }}>
2526
+ 📋 AI Bindings ({configuredCount}/{availableBindings.length} configured)
2527
+ </h2>
2528
+ <table style={{ width: '100%', borderCollapse: 'collapse' }}>
2529
+ <thead>
2530
+ <tr style={{ borderBottom: '2px solid #e5e7eb' }}>
2531
+ <th style={{ textAlign: 'left', padding: 8 }}>Binding</th>
2532
+ <th style={{ textAlign: 'left', padding: 8 }}>Endpoint ID</th>
2533
+ <th style={{ textAlign: 'center', padding: 8, width: 140 }}>Actions</th>
2534
+ <th style={{ textAlign: 'center', padding: 8, width: 60 }}>Status</th>
2535
+ </tr>
2536
+ </thead>
2537
+ <tbody>
2538
+ {availableBindings.map(b => {
2539
+ const status = endpointStatus[b.name] || {}
2540
+ return (
2541
+ <tr key={b.name} style={{ borderBottom: '1px solid #f3f4f6' }}>
2542
+ <td style={{ padding: 8 }}>
2543
+ <div style={{ fontWeight: 500 }}>{b.name}</div>
2544
+ <div style={{ fontSize: 12, color: '#6b7280' }}>{b.description?.slice(0, 50) || b.type}</div>
2545
+ </td>
2546
+ <td style={{ padding: 8 }}>
2547
+ <input
2548
+ type="text"
2549
+ value={endpoints[b.name] || ''}
2550
+ onChange={e => updateEndpoint(b.name, e.target.value)}
2551
+ placeholder="ep_... or full URL"
2552
+ style={{ width: '100%', padding: 8, border: '1px solid #d1d5db', borderRadius: 4, fontSize: 12 }}
2553
+ />
2554
+ </td>
2555
+ <td style={{ padding: 8, textAlign: 'center' }}>
2556
+ <button
2557
+ onClick={() => checkIsAlive(b.name)}
2558
+ disabled={status.loading === 'alive'}
2559
+ style={btnStyle(status.loading === 'alive')}
2560
+ title="Check if endpoint is alive"
2561
+ >
2562
+ {status.loading === 'alive' ? '...' : '♥ Alive'}
2563
+ </button>
2564
+ <button
2565
+ onClick={() => fetchInfo(b.name)}
2566
+ disabled={status.loading === 'info'}
2567
+ style={btnStyle(status.loading === 'info')}
2568
+ title="Get endpoint info"
2569
+ >
2570
+ {status.loading === 'info' ? '...' : 'ℹ Info'}
2571
+ </button>
2572
+ </td>
2573
+ <td style={{ padding: 8, textAlign: 'center' }}>
2574
+ <span
2575
+ style={{
2576
+ display: 'inline-block',
2577
+ width: 12,
2578
+ height: 12,
2579
+ borderRadius: '50%',
2580
+ background: status.alive === true ? '#22c55e' : status.alive === false ? '#ef4444' : (endpoints[b.name] ? '#fbbf24' : '#d1d5db')
2581
+ }}
2582
+ title={status.error || (status.alive ? 'Alive' : status.alive === false ? 'Not responding' : 'Not checked')}
2583
+ />
2584
+ </td>
2585
+ </tr>
2586
+ )
2587
+ })}
2588
+ </tbody>
2589
+ </table>
2590
+
2591
+ {/* Show info panel if any endpoint has info */}
2592
+ {Object.entries(endpointStatus).some(([_, s]) => s.info) && (
2593
+ <div style={{ marginTop: 16, padding: 12, background: '#f9fafb', borderRadius: 6, fontSize: 12 }}>
2594
+ <strong>Endpoint Info:</strong>
2595
+ {Object.entries(endpointStatus).filter(([_, s]) => s.info).map(([name, s]) => (
2596
+ <div key={name} style={{ marginTop: 8 }}>
2597
+ <strong>{name}:</strong>
2598
+ <pre style={{ margin: '4px 0', whiteSpace: 'pre-wrap', fontSize: 11 }}>
2599
+ {JSON.stringify(s.info, null, 2)}
2600
+ </pre>
2601
+ </div>
2602
+ ))}
2603
+ </div>
2604
+ )}
2605
+
2606
+ {/* Show errors */}
2607
+ {Object.entries(endpointStatus).some(([_, s]) => s.error) && (
2608
+ <div style={{ marginTop: 16, padding: 12, background: '#fef2f2', borderRadius: 6, fontSize: 12, color: '#dc2626' }}>
2609
+ <strong>Errors:</strong>
2610
+ {Object.entries(endpointStatus).filter(([_, s]) => s.error).map(([name, s]) => (
2611
+ <div key={name}>{name}: {s.error}</div>
2612
+ ))}
2613
+ </div>
2614
+ )}
2615
+ </div>
2616
+
2617
+ {/* Save Button */}
2618
+ <button
2619
+ onClick={save}
2620
+ style={{
2621
+ width: '100%',
2622
+ padding: 14,
2623
+ background: saved ? '#22c55e' : '#6366f1',
2624
+ color: '#fff',
2625
+ border: 'none',
2626
+ borderRadius: 8,
2627
+ fontSize: 16,
2628
+ fontWeight: 600,
2629
+ cursor: 'pointer',
2630
+ marginBottom: 24
2631
+ }}
2632
+ >
2633
+ {saved ? '✓ Saved!' : 'Save Configuration'}
2634
+ </button>
2635
+
2636
+ {/* Test Panel */}
2637
+ <div style={{ background: '#fff', border: '1px solid #e5e7eb', borderRadius: 8, padding: 16 }}>
2638
+ <h2 style={{ margin: '0 0 16px', fontSize: 18 }}>🧪 Test AI Call</h2>
2639
+ <div style={{ display: 'flex', gap: 8, marginBottom: 12 }}>
2640
+ <select
2641
+ value={testBinding}
2642
+ onChange={e => setTestBinding(e.target.value)}
2643
+ style={{ flex: 1, padding: 8, border: '1px solid #d1d5db', borderRadius: 4 }}
2644
+ >
2645
+ {availableBindings.map(b => (
2646
+ <option key={b.name} value={b.name}>{b.name}</option>
2647
+ ))}
2648
+ </select>
2649
+ <button
2650
+ onClick={testEndpoint}
2651
+ disabled={testing}
2652
+ style={{
2653
+ padding: '8px 16px',
2654
+ background: '#6366f1',
2655
+ color: '#fff',
2656
+ border: 'none',
2657
+ borderRadius: 4,
2658
+ cursor: testing ? 'wait' : 'pointer'
2659
+ }}
2660
+ >
2661
+ {testing ? '...' : 'Send'}
2662
+ </button>
2663
+ </div>
2664
+ <textarea
2665
+ value={testInput}
2666
+ onChange={e => setTestInput(e.target.value)}
2667
+ placeholder="Test input..."
2668
+ rows={2}
2669
+ style={{ width: '100%', padding: 8, border: '1px solid #d1d5db', borderRadius: 4, marginBottom: 12, resize: 'vertical' }}
2670
+ />
2671
+ {testResult && (
2672
+ <pre style={{
2673
+ background: '#f9fafb',
2674
+ padding: 12,
2675
+ borderRadius: 4,
2676
+ fontSize: 13,
2677
+ overflow: 'auto',
2678
+ maxHeight: 200,
2679
+ whiteSpace: 'pre-wrap'
2680
+ }}>
2681
+ {testResult}
2682
+ </pre>
2683
+ )}
2684
+ </div>
2685
+ </div>
2686
+ )
2687
+ }
2688
+ `,
2689
+
2690
+ // Vite config addition for proxy
2691
+ viteProxyConfig: `
2692
+ // PromptLine API Proxy (added by npx promptlineapp get)
2693
+ proxy: {
2694
+ '/api/promptline': {
2695
+ target: 'https://app.promptlineops.com',
2696
+ changeOrigin: true,
2697
+ rewrite: (path) => path.replace(/^\\/api\\/promptline/, '/api/v1/live'),
2698
+ secure: true
2699
+ }
2700
+ },`
2701
+ }
2702
+
2101
2703
  // Get project from Git repository
2102
2704
  async function getProject(gitUrl, destName) {
2103
2705
  const { execSync } = require('child_process');
@@ -2106,9 +2708,6 @@ async function getProject(gitUrl, destName) {
2106
2708
 
2107
2709
  // Extract repo name from URL if destName not provided
2108
2710
  if (!destName) {
2109
- // Handle various URL formats
2110
- // https://github.com/user/repo.git -> repo
2111
- // git@github.com:user/repo.git -> repo
2112
2711
  const match = gitUrl.match(/\/([^\/]+?)(\.git)?$/) || gitUrl.match(/:([^\/]+?)(\.git)?$/);
2113
2712
  destName = match ? match[1] : 'promptline-app';
2114
2713
  }
@@ -2122,7 +2721,6 @@ async function getProject(gitUrl, destName) {
2122
2721
 
2123
2722
  const targetDir = path.resolve(process.cwd(), destName);
2124
2723
 
2125
- // Check if directory exists
2126
2724
  if (fs.existsSync(targetDir)) {
2127
2725
  error(`Directory "${destName}" already exists.`);
2128
2726
  process.exit(1);
@@ -2132,34 +2730,123 @@ async function getProject(gitUrl, destName) {
2132
2730
  console.log(`${c.cyan}→${c.reset} Destination: ${c.green}${destName}${c.reset}\n`);
2133
2731
 
2134
2732
  try {
2135
- // Clone with depth 1 for speed, stdio inherit for git auth prompts
2733
+ // Clone
2136
2734
  execSync(`git clone --depth 1 "${gitUrl}" "${destName}"`, {
2137
2735
  stdio: 'inherit',
2138
2736
  cwd: process.cwd()
2139
2737
  });
2140
2738
 
2141
- // Remove .git directory
2739
+ // Remove .git
2142
2740
  const gitDir = path.join(targetDir, '.git');
2143
2741
  if (fs.existsSync(gitDir)) {
2144
2742
  fs.rmSync(gitDir, { recursive: true });
2145
2743
  }
2146
- success('Removed .git directory');
2744
+ success('Cloned repository');
2147
2745
 
2148
- // Initialize fresh git repo
2746
+ // Init fresh git
2149
2747
  try {
2150
2748
  execSync('git init', { cwd: targetDir, stdio: 'pipe' });
2151
2749
  success('Initialized new git repository');
2152
- } catch (e) {
2153
- // Git init is optional
2154
- }
2750
+ } catch (e) {}
2155
2751
 
2156
- // Detect project structure for instructions
2752
+ // Detect structure
2157
2753
  const hasDevRuntime = fs.existsSync(path.join(targetDir, 'dev-runtime'));
2158
- const hasDev = fs.existsSync(path.join(targetDir, 'dev'));
2159
- const hasPackageJson = fs.existsSync(path.join(targetDir, 'package.json'));
2160
- const hasDevRuntimePackage = fs.existsSync(path.join(targetDir, 'dev-runtime', 'package.json'));
2754
+ const hasPromptlineYaml = fs.existsSync(path.join(targetDir, 'promptline.yaml'));
2755
+
2756
+ // Parse bindings from promptline.yaml
2757
+ let bindings = [];
2758
+ if (hasPromptlineYaml) {
2759
+ try {
2760
+ const yamlContent = fs.readFileSync(path.join(targetDir, 'promptline.yaml'), 'utf8');
2761
+ bindings = parseBindingsFromYaml(yamlContent);
2762
+ success(`Detected ${bindings.length} AI bindings from promptline.yaml`);
2763
+ } catch (e) {
2764
+ console.log(`${c.yellow}!${c.reset} Could not parse promptline.yaml: ${e.message}`);
2765
+ }
2766
+ }
2767
+
2768
+ // Inject dev files if we have dev-runtime
2769
+ if (hasDevRuntime && bindings.length > 0) {
2770
+ const srcDir = path.join(targetDir, 'dev-runtime', 'src');
2771
+
2772
+ // Backup original sdk-mock if exists
2773
+ const sdkPath = path.join(srcDir, 'sdk-mock.ts');
2774
+ if (fs.existsSync(sdkPath)) {
2775
+ fs.renameSync(sdkPath, path.join(srcDir, 'sdk-mock.original.ts'));
2776
+ success('Backed up original sdk-mock.ts');
2777
+ }
2778
+
2779
+ // Write new SDK (as .tsx for JSX support)
2780
+ fs.writeFileSync(path.join(srcDir, 'sdk-mock.tsx'), devTemplates.sdkHybrid(bindings));
2781
+ success('Injected hybrid SDK (sdk-mock.tsx)');
2782
+
2783
+ // Write DevAdmin page
2784
+ fs.writeFileSync(path.join(srcDir, 'DevAdmin.tsx'), devTemplates.devAdmin(bindings));
2785
+ success('Injected /_dev page (DevAdmin.tsx)');
2786
+
2787
+ // Modify Router.tsx to add /_dev route
2788
+ const routerPath = path.join(srcDir, 'Router.tsx');
2789
+ if (fs.existsSync(routerPath)) {
2790
+ let routerContent = fs.readFileSync(routerPath, 'utf8');
2791
+
2792
+ // Add import if not present
2793
+ if (!routerContent.includes('DevAdmin')) {
2794
+ routerContent = routerContent.replace(
2795
+ /(import .+ from ['"].+['"];?\n)/,
2796
+ "$1import DevAdmin from './DevAdmin'\n"
2797
+ );
2798
+ }
2799
+
2800
+ // Add route if not present
2801
+ if (!routerContent.includes('/_dev')) {
2802
+ // Try to find the routes array and add /_dev
2803
+ routerContent = routerContent.replace(
2804
+ /(<Route\s+path=["']\/["']\s+element=)/,
2805
+ "<Route path=\"/_dev\" element={<DevAdmin />} />\n $1"
2806
+ );
2807
+ }
2808
+
2809
+ fs.writeFileSync(routerPath, routerContent);
2810
+ success('Added /_dev route to Router.tsx');
2811
+ }
2161
2812
 
2162
- // Success message with appropriate instructions
2813
+ // Check and update vite.config.ts for proxy
2814
+ const viteConfigPath = path.join(targetDir, 'dev-runtime', 'vite.config.ts');
2815
+ if (fs.existsSync(viteConfigPath)) {
2816
+ let viteContent = fs.readFileSync(viteConfigPath, 'utf8');
2817
+ if (!viteContent.includes('/api/promptline')) {
2818
+ // Add proxy config to server section
2819
+ if (viteContent.includes('server:')) {
2820
+ viteContent = viteContent.replace(
2821
+ /server:\s*\{/,
2822
+ 'server: {' + devTemplates.viteProxyConfig
2823
+ );
2824
+ } else {
2825
+ // Add server section before the closing of defineConfig
2826
+ viteContent = viteContent.replace(
2827
+ /}\s*\)\s*$/,
2828
+ `,\n server: {${devTemplates.viteProxyConfig}\n }\n})`
2829
+ );
2830
+ }
2831
+ fs.writeFileSync(viteConfigPath, viteContent);
2832
+ success('Added API proxy to vite.config.ts');
2833
+ }
2834
+ }
2835
+ }
2836
+
2837
+ // Determine dev directory and port
2838
+ const devDir = hasDevRuntime ? 'dev-runtime' : '.';
2839
+ let devPort = '5173';
2840
+
2841
+ // Try to detect port from vite config
2842
+ const viteConfigPath = path.join(targetDir, hasDevRuntime ? 'dev-runtime' : '', 'vite.config.ts');
2843
+ if (fs.existsSync(viteConfigPath)) {
2844
+ const viteContent = fs.readFileSync(viteConfigPath, 'utf8');
2845
+ const portMatch = viteContent.match(/port:\s*(\d+)/);
2846
+ if (portMatch) devPort = portMatch[1];
2847
+ }
2848
+
2849
+ // Success message
2163
2850
  console.log(`
2164
2851
  ${c.green}╔═══════════════════════════════════════════════════════════╗
2165
2852
  ║ ║
@@ -2169,29 +2856,19 @@ ${c.green}╔══════════════════════
2169
2856
 
2170
2857
  ${c.bold}Next steps:${c.reset}
2171
2858
 
2172
- ${c.cyan}cd ${destName}${c.reset}`);
2173
-
2174
- if (hasDevRuntime && hasDevRuntimePackage) {
2175
- console.log(`
2176
- ${c.dim}# This project has a dev-runtime environment${c.reset}
2177
- ${c.cyan}cd dev-runtime${c.reset}
2859
+ ${c.cyan}cd ${destName}${hasDevRuntime ? '/dev-runtime' : ''}${c.reset}
2178
2860
  ${c.cyan}npm install${c.reset}
2179
- ${c.cyan}npm run dev${c.reset}`);
2180
- } else if (hasDev || hasPackageJson) {
2181
- console.log(`
2182
- ${c.cyan}npm install${c.reset}
2183
- ${c.cyan}npm run dev${c.reset}`);
2184
- } else {
2185
- console.log(`
2186
- ${c.dim}# Check the project README for setup instructions${c.reset}`);
2187
- }
2861
+ ${c.cyan}npm run dev${c.reset}
2188
2862
 
2189
- console.log(`
2190
- ${c.bold}Key files:${c.reset}
2863
+ ${c.bold}Configure AI endpoints:${c.reset}
2864
+
2865
+ Open ${c.yellow}http://localhost:${devPort}/_dev${c.reset}
2866
+ 1. Enter your API key
2867
+ 2. Add endpoint IDs for each binding
2868
+ 3. Test your endpoints
2191
2869
 
2192
- ${c.yellow}promptline.yaml${c.reset} Main configuration
2193
- ${c.yellow}public/${c.reset} Public pages
2194
- ${c.yellow}private/${c.reset} Admin/dashboard pages
2870
+ ${c.bold}${bindings.length} AI bindings detected:${c.reset}
2871
+ ${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}` : ''}
2195
2872
 
2196
2873
  ${c.bold}Documentation:${c.reset} ${c.cyan}https://docs.promptlineops.com${c.reset}
2197
2874
  `);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "promptlineapp",
3
- "version": "1.6.0",
3
+ "version": "1.8.0",
4
4
  "description": "Create PromptLine applications with ease",
5
5
  "author": "PromptLine <support@promptlineops.com>",
6
6
  "license": "MIT",