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.
- package/bin/cli.js +715 -38
- 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
|
|
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',
|
|
@@ -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
|
|
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
|
|
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('
|
|
2744
|
+
success('Cloned repository');
|
|
2147
2745
|
|
|
2148
|
-
//
|
|
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
|
|
2752
|
+
// Detect structure
|
|
2157
2753
|
const hasDevRuntime = fs.existsSync(path.join(targetDir, 'dev-runtime'));
|
|
2158
|
-
const
|
|
2159
|
-
|
|
2160
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2190
|
-
|
|
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
|
-
|
|
2193
|
-
${c.
|
|
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
|
`);
|