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.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/bin/cli.js +685 -11
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -5,7 +5,7 @@ Create AI-powered apps for the [PromptLine Marketplace](https://promptlineops.co
5
5
  ## Quick Start
6
6
 
7
7
  ```bash
8
- npx create-promptline-app my-app
8
+ npx promptlineapp my-app
9
9
  cd my-app
10
10
  npm run dev
11
11
  ```
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',
@@ -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}create-promptline-app${c.reset} - Create PromptLine applications
2741
+ ${c.bold}promptlineapp${c.reset} - Create and get PromptLine applications
2091
2742
 
2092
2743
  ${c.bold}Usage:${c.reset}
2093
- npx create-promptline-app <app-name> [options]
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}Options:${c.reset}
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
- npx create-promptline-app my-app
2103
- npx create-promptline-app my-api --preset api
2104
- npx create-promptline-app my-app -y
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 create-promptline-app${c.reset} ${c.green}<app-name>${c.reset}\n`);
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 create-promptline-app${c.reset} ${c.green}my-restaurant-app${c.reset}\n`);
2137
- console.log(`Run ${c.cyan}npx create-promptline-app --help${c.reset} for more options.`);
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "promptlineapp",
3
- "version": "1.5.0",
3
+ "version": "1.7.0",
4
4
  "description": "Create PromptLine applications with ease",
5
5
  "author": "PromptLine <support@promptlineops.com>",
6
6
  "license": "MIT",