promptlineapp 1.4.1 → 1.6.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 +45 -107
  2. package/bin/cli.js +574 -193
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,136 +1,74 @@
1
- # promptlineapp
1
+ # create-promptline-app
2
2
 
3
- Create AI-powered applications with [PromptLine](https://promptlineops.com) - no configuration needed.
3
+ Create AI-powered apps for the [PromptLine Marketplace](https://promptlineops.com).
4
4
 
5
5
  ## Quick Start
6
6
 
7
7
  ```bash
8
8
  npx promptlineapp my-app
9
9
  cd my-app
10
+ npm run dev
10
11
  ```
11
12
 
12
- That's it! You now have a fully-configured PromptLine package ready for development.
13
+ ## Local Development
13
14
 
14
- ## Usage
15
+ ### 1. Configure your endpoints
15
16
 
16
- ### Interactive Mode (Recommended)
17
+ Open **http://localhost:5173/_dev** and add your PromptLine endpoints:
17
18
 
18
- ```bash
19
- npx promptlineapp my-app
20
- ```
21
-
22
- You'll be prompted for:
23
- - Display name
24
- - Description
25
- - Brand color
26
- - Contact email
27
- - Template preset
28
-
29
- ### Non-Interactive Mode
30
-
31
- ```bash
32
- npx promptlineapp my-app --yes
33
- npx promptlineapp my-app --preset full-app -y
34
- ```
35
-
36
- ## Template Presets
37
-
38
- | Preset | Description | Best For |
39
- |--------|-------------|----------|
40
- | `full-app` | Complete app with landing, dashboard, settings & AI | SaaS products, internal tools (default) |
41
- | `api` | Minimal frontend, backend-focused | API services, integrations |
42
-
43
- ```bash
44
- npx promptlineapp my-app --preset full-app
45
- npx promptlineapp my-api --preset api
46
- ```
47
-
48
- ## What You Get
19
+ - **Alias**: name used in code (e.g., `chatbot`, `traduction`)
20
+ - **URL**: endpoint URL from Creator Studio
21
+ - **API Key**: your API key (`sk_org_...` or `sk_ws_...`)
49
22
 
50
- ```
51
- my-app/
52
- ├── public/ # Public pages (no auth required)
53
- │ └── index.tsx # Landing page
54
- ├── private/ # Protected pages (auth required)
55
- │ ├── dashboard.tsx # Dashboard
56
- │ └── settings.tsx # Settings
57
- ├── backend/ # Custom API endpoints (FastAPI)
58
- │ └── api.py
59
- ├── assets/ # Static files
60
- ├── promptline.yaml # Package configuration
61
- ├── README.md
62
- └── .gitignore
63
- ```
23
+ Click "Check Health" to verify, then "Add Endpoint".
64
24
 
65
- ## Configuration
66
-
67
- Your app is configured via `promptline.yaml`:
68
-
69
- ```yaml
70
- name: "My App"
71
- version: "1.0.0"
72
-
73
- # Connect AI prompts/agents
74
- ai_sources:
75
- main:
76
- type: prompt
77
- source_id: null # Set in Creator Studio
78
-
79
- # Define data models
80
- collections:
81
- submissions:
82
- fields:
83
- - name: email
84
- type: email
85
- required: true
86
-
87
- # Instance variables
88
- variables:
89
- app_name:
90
- type: string
91
- default: "My App"
92
- ```
25
+ ### 2. Use in your code
93
26
 
94
- ## SDK Usage
95
-
96
- In your React pages:
97
-
98
- ```tsx
27
+ ```jsx
99
28
  import { usePromptLine } from '@promptline/sdk'
100
29
 
101
- export default function MyPage() {
102
- const { config, submitForm, callAI, user } = usePromptLine()
103
-
104
- // Access instance config
105
- const appName = config.app_name
30
+ function MyComponent() {
31
+ const { execute, isConfigured } = usePromptLine('chatbot')
106
32
 
107
- // Submit data
108
- await submitForm('submissions', { email: 'user@example.com' })
33
+ const handleClick = async () => {
34
+ const result = await execute({ text: 'Hello!' })
35
+ console.log(result.response)
36
+ }
109
37
 
110
- // Call AI
111
- const result = await callAI('main', { text: 'Hello!' })
38
+ if (!isConfigured) {
39
+ return <p>Configure 'chatbot' at <a href="/_dev">/_dev</a></p>
40
+ }
112
41
 
113
- return <h1 style={{ color: config.primary_color }}>{appName}</h1>
42
+ return <button onClick={handleClick}>Ask AI</button>
114
43
  }
115
44
  ```
116
45
 
117
- ## Deployment
118
-
119
- 1. **Upload** - Zip your package and upload to [PromptLine Creator Studio](https://app.promptlineops.com/apps/creator)
120
- 2. **Configure** - Connect your AI sources and set variables
121
- 3. **Publish** - Deploy to your users or the App Store
46
+ ### 3. Multiple endpoints
122
47
 
123
- ## Documentation
48
+ ```jsx
49
+ const { execute: chat } = usePromptLine('chatbot')
50
+ const { execute: translate } = usePromptLine('traduction')
124
51
 
125
- - [Package Development Guide](https://docs.promptlineops.com/packages)
126
- - [SDK Reference](https://docs.promptlineops.com/sdk)
127
- - [API Endpoints](https://docs.promptlineops.com/packages/backend)
52
+ // Use them independently
53
+ const chatResult = await chat({ text: 'Hello' })
54
+ const translatedResult = await translate({ text: 'Bonjour', target: 'en' })
55
+ ```
128
56
 
129
- ## Requirements
57
+ ## Project Structure
130
58
 
131
- - Node.js 16+
132
- - npm or yarn
59
+ ```
60
+ my-app/
61
+ ├── public/ # Public pages
62
+ ├── private/ # Auth-protected pages
63
+ ├── backend/ # Custom API (FastAPI)
64
+ ├── dev/ # Local dev tools (not deployed)
65
+ │ ├── sdk-mock.jsx # SDK for local testing
66
+ │ └── dev-admin.jsx # /_dev page
67
+ └── promptline.yaml # App configuration
68
+ ```
133
69
 
134
- ## License
70
+ ## Deployment
135
71
 
136
- MIT - [PromptLine](https://promptlineops.com)
72
+ 1. Click "Download Manifest" in `/_dev` to get `promptline.config.json`
73
+ 2. Upload your app to [Creator Studio](https://app.promptlineops.com)
74
+ 3. Bind your endpoints and publish
package/bin/cli.js CHANGED
@@ -1066,6 +1066,9 @@ Thumbs.db
1066
1066
  import react from '@vitejs/plugin-react'
1067
1067
  import path from 'path'
1068
1068
 
1069
+ // Internal PromptLine dev mode: set PROMPTLINE_LOCAL_DEV=true to enable local API
1070
+ const isLocalDev = process.env.PROMPTLINE_LOCAL_DEV === 'true'
1071
+
1069
1072
  export default defineConfig({
1070
1073
  plugins: [react()],
1071
1074
  resolve: {
@@ -1076,16 +1079,19 @@ export default defineConfig({
1076
1079
  server: {
1077
1080
  port: 5173,
1078
1081
  host: 'localhost',
1079
- open: true,
1082
+ open: '/_dev',
1080
1083
  strictPort: false,
1081
1084
  proxy: {
1082
- // Proxy API calls to avoid CORS issues in development
1083
- '/api/promptline-local': {
1084
- target: 'https://app.local.promptlineops.com',
1085
- changeOrigin: true,
1086
- rewrite: (path) => path.replace(/^\\/api\\/promptline-local/, '/api/v1/live'),
1087
- secure: true
1088
- },
1085
+ // Local API first (more specific path - requires PROMPTLINE_LOCAL_DEV=true)
1086
+ ...(isLocalDev ? {
1087
+ '/api/promptline-local': {
1088
+ target: 'https://app.local.promptlineops.com',
1089
+ changeOrigin: true,
1090
+ rewrite: (path) => path.replace(/^\\/api\\/promptline-local/, '/api/v1/live'),
1091
+ secure: false
1092
+ }
1093
+ } : {}),
1094
+ // Production API (marketplace developers)
1089
1095
  '/api/promptline': {
1090
1096
  target: 'https://app.promptlineops.com',
1091
1097
  changeOrigin: true,
@@ -1097,7 +1103,7 @@ export default defineConfig({
1097
1103
  })
1098
1104
  `,
1099
1105
 
1100
- // SDK mock for local development with real API support
1106
+ // SDK mock for local development with multi-endpoint support
1101
1107
  sdkMock: (config) => {
1102
1108
  const mockConfig = {
1103
1109
  app_name: config.displayName,
@@ -1107,10 +1113,16 @@ export default defineConfig({
1107
1113
  return `/**
1108
1114
  * PromptLine SDK Mock for Local Development
1109
1115
  * Supports real API calls when configured via /_dev page
1116
+ *
1117
+ * Usage:
1118
+ * const { execute } = usePromptLine('chatbot')
1119
+ * const result = await execute({ text: 'Hello!' })
1110
1120
  */
1111
- import React, { createContext, useContext, useState } from 'react'
1121
+ import React, { createContext, useContext, useState, useMemo } from 'react'
1112
1122
  import { Link as RouterLink } from 'react-router-dom'
1113
1123
 
1124
+ const ENDPOINTS_STORAGE_KEY = 'promptline_dev_endpoints'
1125
+
1114
1126
  const mockConfig = ${JSON.stringify(mockConfig, null, 2)}
1115
1127
 
1116
1128
  const mockUser = {
@@ -1119,21 +1131,39 @@ const mockUser = {
1119
1131
  name: 'Dev User'
1120
1132
  }
1121
1133
 
1122
- // Get PromptLine API config from localStorage
1123
- function getAPIConfig() {
1134
+ // Get all configured endpoints from localStorage
1135
+ function getEndpoints() {
1124
1136
  try {
1125
- return JSON.parse(localStorage.getItem('promptline_dev_config') || 'null')
1126
- } catch { return null }
1137
+ return JSON.parse(localStorage.getItem(ENDPOINTS_STORAGE_KEY) || '[]')
1138
+ } catch { return [] }
1127
1139
  }
1128
1140
 
1129
- // Call PromptLine API (sync mode)
1130
- async function callPromptLineAPI(config, input) {
1131
- const { endpoint, apiKey } = config
1141
+ // Get a specific endpoint by alias
1142
+ function getEndpoint(alias) {
1143
+ const endpoints = getEndpoints()
1144
+ return endpoints.find(ep => ep.alias === alias)
1145
+ }
1132
1146
 
1133
- // Add ?mode=sync for synchronous response
1134
- const url = endpoint.includes('?') ? endpoint + '&mode=sync' : endpoint + '?mode=sync'
1147
+ // Convert PromptLine URLs to use local proxy
1148
+ function toProxyUrl(url) {
1149
+ const localMatch = url.match(/https?:\\/\\/app\\.local\\.promptlineops\\.com\\/api\\/v1\\/live\\/([^?]+)/)
1150
+ if (localMatch) {
1151
+ return '/api/promptline-local/' + localMatch[1]
1152
+ }
1153
+ const prodMatch = url.match(/https?:\\/\\/app\\.promptlineops\\.com\\/api\\/v1\\/live\\/([^?]+)/)
1154
+ if (prodMatch) {
1155
+ return '/api/promptline/' + prodMatch[1]
1156
+ }
1157
+ return url
1158
+ }
1135
1159
 
1136
- const res = await fetch(url, {
1160
+ // Call PromptLine API (sync mode)
1161
+ async function callPromptLineAPI(endpoint, input, variables = {}) {
1162
+ const { url, apiKey } = endpoint
1163
+ const proxyUrl = toProxyUrl(url)
1164
+ const fullUrl = proxyUrl + '?mode=sync'
1165
+
1166
+ const res = await fetch(fullUrl, {
1137
1167
  method: 'POST',
1138
1168
  headers: {
1139
1169
  'Content-Type': 'application/json',
@@ -1141,25 +1171,28 @@ async function callPromptLineAPI(config, input) {
1141
1171
  },
1142
1172
  body: JSON.stringify({
1143
1173
  input: typeof input === 'string' ? { text: input } : input,
1144
- variables: {}
1174
+ variables
1145
1175
  })
1146
1176
  })
1177
+
1147
1178
  const data = await res.json()
1148
- if (!res.ok || data.error) throw new Error(data.detail || data.error?.message || data.error || res.statusText)
1149
- // Sync mode returns data.data.output or data.data.response
1179
+ if (!res.ok || data.error) {
1180
+ throw new Error(data.detail || data.error?.message || data.error || res.statusText)
1181
+ }
1182
+
1150
1183
  const output = data.data?.output || data.data?.response || data.output || data.response
1151
- return { response: typeof output === 'string' ? output : JSON.stringify(output) }
1184
+ return {
1185
+ response: typeof output === 'string' ? output : JSON.stringify(output),
1186
+ data: data.data || data
1187
+ }
1152
1188
  }
1153
1189
 
1154
1190
  const PromptLineContext = createContext(null)
1155
1191
 
1156
1192
  export function PromptLineProvider({ children }) {
1157
- const [isLoading, setIsLoading] = useState(false)
1158
-
1159
1193
  const value = {
1160
1194
  config: mockConfig,
1161
1195
  user: mockUser,
1162
- isLoading,
1163
1196
  submitForm: async (collection, data) => {
1164
1197
  console.log('[PromptLine SDK] submitForm:', collection, data)
1165
1198
  alert('Form submitted to "' + collection + '"\\n\\nData: ' + JSON.stringify(data, null, 2))
@@ -1168,23 +1201,6 @@ export function PromptLineProvider({ children }) {
1168
1201
  fetchCollection: async (collection, query) => {
1169
1202
  console.log('[PromptLine SDK] fetchCollection:', collection, query)
1170
1203
  return { items: [], total: 0 }
1171
- },
1172
- callAI: async (sourceId, input) => {
1173
- console.log('[PromptLine SDK] callAI:', sourceId, input)
1174
- const config = getAPIConfig()
1175
-
1176
- if (config && config.endpoint) {
1177
- console.log('[PromptLine SDK] Using PromptLine API:', config.endpoint)
1178
- try {
1179
- return await callPromptLineAPI(config, input)
1180
- } catch (err) {
1181
- console.error('[PromptLine SDK] API Error:', err)
1182
- return { response: '[API Error] ' + err.message, error: true }
1183
- }
1184
- }
1185
-
1186
- console.log('[PromptLine SDK] No config found, using mock. Configure at /_dev')
1187
- return { response: '[Mock] Configure your PromptLine endpoint at /_dev' }
1188
1204
  }
1189
1205
  }
1190
1206
 
@@ -1195,48 +1211,105 @@ export function PromptLineProvider({ children }) {
1195
1211
  )
1196
1212
  }
1197
1213
 
1198
- export function usePromptLine() {
1214
+ export function usePromptLine(alias) {
1199
1215
  const context = useContext(PromptLineContext)
1200
- if (!context) {
1201
- return {
1202
- config: mockConfig,
1203
- user: mockUser,
1204
- isLoading: false,
1205
- submitForm: async (c, d) => { console.log('[PromptLine SDK] submitForm:', c, d); return { success: true } },
1206
- fetchCollection: async (c, q) => { console.log('[PromptLine SDK] fetchCollection:', c, q); return { items: [], total: 0 } },
1207
- callAI: async (s, i) => { console.log('[PromptLine SDK] callAI:', s, i); return { response: '[Mock] Configure at /_dev' } }
1216
+
1217
+ const endpointData = useMemo(() => {
1218
+ if (!alias) return null
1219
+ return getEndpoint(alias)
1220
+ }, [alias])
1221
+
1222
+ const [isLoading, setIsLoading] = useState(false)
1223
+
1224
+ const execute = async (input, variables = {}) => {
1225
+ if (!endpointData) {
1226
+ console.warn('[PromptLine SDK] Endpoint "' + alias + '" not configured. Go to /_dev to configure it.')
1227
+ return {
1228
+ response: '[Not Configured] Endpoint "' + alias + '" not found. Configure it at /_dev',
1229
+ error: true
1230
+ }
1231
+ }
1232
+
1233
+ console.log('[PromptLine SDK] Calling endpoint "' + alias + '":', endpointData.url)
1234
+ setIsLoading(true)
1235
+
1236
+ try {
1237
+ const result = await callPromptLineAPI(endpointData, input, variables)
1238
+ return result
1239
+ } catch (err) {
1240
+ console.error('[PromptLine SDK] Error calling "' + alias + '":', err)
1241
+ return { response: '[API Error] ' + err.message, error: true }
1242
+ } finally {
1243
+ setIsLoading(false)
1244
+ }
1245
+ }
1246
+
1247
+ return {
1248
+ execute,
1249
+ isLoading,
1250
+ isConfigured: !!endpointData,
1251
+ endpointInfo: endpointData?.info || null,
1252
+ config: context?.config || mockConfig,
1253
+ user: context?.user || mockUser,
1254
+ submitForm: context?.submitForm || (async () => ({ success: true })),
1255
+ fetchCollection: context?.fetchCollection || (async () => ({ items: [], total: 0 })),
1256
+ callAI: async (sourceIdOrInput, inputOrVariables) => {
1257
+ let targetAlias = alias
1258
+ let input = sourceIdOrInput
1259
+ if (typeof sourceIdOrInput === 'string' && typeof inputOrVariables === 'object') {
1260
+ targetAlias = sourceIdOrInput
1261
+ input = inputOrVariables
1262
+ }
1263
+ const ep = getEndpoint(targetAlias) || getEndpoints()[0]
1264
+ if (!ep) {
1265
+ return { response: '[Not Configured] No endpoints configured. Go to /_dev', error: true }
1266
+ }
1267
+ try {
1268
+ return await callPromptLineAPI(ep, input)
1269
+ } catch (err) {
1270
+ return { response: '[API Error] ' + err.message, error: true }
1271
+ }
1208
1272
  }
1209
1273
  }
1210
- return context
1274
+ }
1275
+
1276
+ export function usePromptLineEndpoints() {
1277
+ return useMemo(() => getEndpoints(), [])
1211
1278
  }
1212
1279
 
1213
1280
  export const Link = ({ to, children, ...props }) => (
1214
1281
  <RouterLink to={to} {...props}>{children}</RouterLink>
1215
1282
  )
1216
1283
 
1217
- export default { usePromptLine, PromptLineProvider, Link }
1284
+ export default { usePromptLine, usePromptLineEndpoints, PromptLineProvider, Link }
1218
1285
  `},
1219
1286
 
1220
- // Dev admin page for configuring PromptLine endpoint
1287
+ // Dev admin page for configuring PromptLine endpoints (multi-endpoint)
1221
1288
  devAdmin: () => `/**
1222
- * PromptLine Dev Admin - Endpoint Configuration
1289
+ * PromptLine Dev Admin - Multi-Endpoint Configuration
1223
1290
  *
1224
1291
  * This page is ONLY for local development.
1225
- * Configure your PromptLine endpoint here to test with real API.
1292
+ * Configure your PromptLine endpoints here to test with real API.
1226
1293
  *
1227
1294
  * NEVER commit API keys to git!
1228
1295
  */
1229
1296
  import React, { useState, useEffect } from 'react'
1230
1297
  import { Link } from 'react-router-dom'
1231
1298
 
1299
+ const STORAGE_KEY = 'promptline_dev_endpoints'
1300
+
1232
1301
  export default function DevAdminPage() {
1233
- const [endpoint, setEndpoint] = useState('')
1234
- const [apiKey, setApiKey] = useState('')
1235
- const [saved, setSaved] = useState(false)
1302
+ const [endpoints, setEndpoints] = useState([])
1303
+ const [formAlias, setFormAlias] = useState('')
1304
+ const [formUrl, setFormUrl] = useState('')
1305
+ const [formApiKey, setFormApiKey] = useState('')
1306
+ const [formInfo, setFormInfo] = useState(null)
1307
+ const [formStatus, setFormStatus] = useState('')
1308
+ const [editingIndex, setEditingIndex] = useState(-1)
1309
+ const [selectedEndpoint, setSelectedEndpoint] = useState('')
1236
1310
  const [testInput, setTestInput] = useState('Hello! What is 2+2?')
1237
1311
  const [testResult, setTestResult] = useState('')
1238
1312
  const [testing, setTesting] = useState(false)
1239
- const [status, setStatus] = useState('') // 'connected' | 'error' | 'testing' | ''
1240
1313
  const [logs, setLogs] = useState([])
1241
1314
 
1242
1315
  const addLog = (type, message, details = null) => {
@@ -1245,121 +1318,145 @@ export default function DevAdminPage() {
1245
1318
  }
1246
1319
 
1247
1320
  useEffect(() => {
1248
- const config = JSON.parse(localStorage.getItem('promptline_dev_config') || 'null')
1249
- if (config) {
1250
- setEndpoint(config.endpoint || '')
1251
- setApiKey(config.apiKey || '')
1252
- setSaved(true)
1253
- }
1321
+ const saved = JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]')
1322
+ setEndpoints(saved)
1323
+ if (saved.length > 0) setSelectedEndpoint(saved[0].alias)
1254
1324
  }, [])
1255
1325
 
1256
- const saveConfig = () => {
1257
- if (!endpoint) return
1258
- const config = { endpoint, apiKey }
1259
- localStorage.setItem('promptline_dev_config', JSON.stringify(config))
1260
- setSaved(true)
1261
- addLog('info', 'Configuration saved')
1326
+ const saveEndpoints = (newEndpoints) => {
1327
+ setEndpoints(newEndpoints)
1328
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(newEndpoints))
1329
+ generateManifest(newEndpoints)
1330
+ addLog('info', 'Saved ' + newEndpoints.length + ' endpoint(s)')
1262
1331
  }
1263
1332
 
1264
- const clearConfig = () => {
1265
- localStorage.removeItem('promptline_dev_config')
1266
- setEndpoint('')
1267
- setApiKey('')
1268
- setSaved(false)
1269
- setStatus('')
1270
- addLog('info', 'Configuration cleared')
1333
+ const generateManifest = (eps) => {
1334
+ const manifest = { name: 'my-promptline-app', version: '1.0.0', endpoints: {} }
1335
+ eps.forEach(ep => {
1336
+ manifest.endpoints[ep.alias] = {
1337
+ name: ep.info?.name || ep.alias,
1338
+ description: ep.info?.description || '',
1339
+ type: ep.info?.endpoint_type || 'prompt',
1340
+ required_variables: ep.info?.required_variables || []
1341
+ }
1342
+ })
1343
+ localStorage.setItem('promptline_manifest', JSON.stringify(manifest, null, 2))
1271
1344
  }
1272
1345
 
1273
- // Convert PromptLine URLs to use local proxy (avoids CORS in development)
1274
1346
  const toProxyUrl = (url) => {
1275
- // Match app.local.promptlineops.com (local dev environment)
1276
1347
  const localMatch = url.match(/https?:\\/\\/app\\.local\\.promptlineops\\.com\\/api\\/v1\\/live\\/([^?]+)/)
1277
- if (localMatch) {
1278
- return '/api/promptline-local/' + localMatch[1]
1279
- }
1280
- // Match app.promptlineops.com (production environment)
1348
+ if (localMatch) return '/api/promptline-local/' + localMatch[1]
1281
1349
  const prodMatch = url.match(/https?:\\/\\/app\\.promptlineops\\.com\\/api\\/v1\\/live\\/([^?]+)/)
1282
- if (prodMatch) {
1283
- return '/api/promptline/' + prodMatch[1]
1284
- }
1285
- return url // Non-PromptLine URLs pass through unchanged
1350
+ if (prodMatch) return '/api/promptline/' + prodMatch[1]
1351
+ return url
1286
1352
  }
1287
1353
 
1288
- const testConnection = async () => {
1289
- if (!endpoint) return
1290
- if (!apiKey) {
1291
- setStatus('error')
1292
- addLog('error', 'API Key required for health check')
1293
- return
1354
+ const checkHealth = async () => {
1355
+ if (!formUrl || !formApiKey) return
1356
+ setFormStatus('checking')
1357
+ const proxyUrl = toProxyUrl(formUrl)
1358
+ addLog('info', 'Checking health: ' + (formAlias || 'new endpoint'))
1359
+ try {
1360
+ const res = await fetch(proxyUrl + '/isAlive', { method: 'GET', headers: { 'X-API-Key': formApiKey } })
1361
+ const data = await res.json().catch(() => ({}))
1362
+ if (res.ok && data.alive === true) {
1363
+ setFormStatus('valid')
1364
+ addLog('success', 'Endpoint is alive')
1365
+ } else {
1366
+ setFormStatus('error')
1367
+ addLog('error', 'Health check failed: ' + res.status, data)
1368
+ }
1369
+ } catch (err) {
1370
+ setFormStatus('error')
1371
+ addLog('error', 'Network error', { message: err.message })
1294
1372
  }
1295
- setStatus('testing')
1296
- const proxyUrl = toProxyUrl(endpoint)
1297
- // Use /isAlive endpoint for health check (no LLM execution, no token consumption)
1298
- const isAliveUrl = proxyUrl + '/isAlive'
1299
- addLog('info', 'Testing connection (isAlive)...', { endpoint: isAliveUrl, original: endpoint })
1373
+ }
1300
1374
 
1301
- const startTime = Date.now()
1375
+ const fetchInfo = async () => {
1376
+ if (!formUrl || !formApiKey) return
1377
+ const proxyUrl = toProxyUrl(formUrl)
1378
+ addLog('info', 'Fetching info: ' + (formAlias || 'new endpoint'))
1302
1379
  try {
1303
- const res = await fetch(isAliveUrl, {
1304
- method: 'GET',
1305
- headers: {
1306
- 'X-API-Key': apiKey
1307
- }
1308
- })
1380
+ const res = await fetch(proxyUrl + '/info', { method: 'GET', headers: { 'X-API-Key': formApiKey } })
1309
1381
  const data = await res.json().catch(() => ({}))
1310
- const duration = Date.now() - startTime
1311
-
1312
- if (res.ok && data.alive === true) {
1313
- setStatus('connected')
1314
- addLog('success', 'Endpoint is alive (' + duration + 'ms)', { alive: true })
1315
- } else if (res.ok && data.alive === false) {
1316
- setStatus('error')
1317
- addLog('error', 'Endpoint exists but is inactive (' + duration + 'ms)', { alive: false })
1382
+ if (res.ok) {
1383
+ setFormInfo(data)
1384
+ addLog('success', 'Info retrieved', data)
1318
1385
  } else {
1319
- setStatus('error')
1320
- addLog('error', res.status + ' ' + res.statusText + ' (' + duration + 'ms)', data)
1386
+ addLog('error', 'Info fetch failed: ' + res.status, data)
1321
1387
  }
1322
1388
  } catch (err) {
1323
- const duration = Date.now() - startTime
1324
- setStatus('error')
1325
- addLog('error', 'Network error (' + duration + 'ms)', {
1326
- message: err.message,
1327
- hint: err.message.includes('Failed to fetch') ? 'CORS issue or endpoint not found' : null
1328
- })
1389
+ addLog('error', 'Network error', { message: err.message })
1390
+ }
1391
+ }
1392
+
1393
+ const saveEndpoint = () => {
1394
+ if (!formAlias || !formUrl) {
1395
+ addLog('error', 'Alias and URL are required')
1396
+ return
1329
1397
  }
1398
+ const existingIndex = endpoints.findIndex(ep => ep.alias === formAlias)
1399
+ if (existingIndex !== -1 && existingIndex !== editingIndex) {
1400
+ addLog('error', 'Alias "' + formAlias + '" already exists')
1401
+ return
1402
+ }
1403
+ const newEndpoint = { alias: formAlias, url: formUrl, apiKey: formApiKey, info: formInfo, status: formStatus === 'valid' ? 'valid' : 'unchecked' }
1404
+ let newEndpoints = editingIndex >= 0 ? [...endpoints] : [...endpoints, newEndpoint]
1405
+ if (editingIndex >= 0) newEndpoints[editingIndex] = newEndpoint
1406
+ saveEndpoints(newEndpoints)
1407
+ clearForm()
1408
+ if (!selectedEndpoint) setSelectedEndpoint(formAlias)
1409
+ }
1410
+
1411
+ const editEndpoint = (index) => {
1412
+ const ep = endpoints[index]
1413
+ setFormAlias(ep.alias)
1414
+ setFormUrl(ep.url)
1415
+ setFormApiKey(ep.apiKey)
1416
+ setFormInfo(ep.info)
1417
+ setFormStatus(ep.status === 'valid' ? 'valid' : '')
1418
+ setEditingIndex(index)
1419
+ }
1420
+
1421
+ const deleteEndpoint = (index) => {
1422
+ const ep = endpoints[index]
1423
+ const newEndpoints = endpoints.filter((_, i) => i !== index)
1424
+ saveEndpoints(newEndpoints)
1425
+ if (selectedEndpoint === ep.alias) setSelectedEndpoint(newEndpoints[0]?.alias || '')
1426
+ if (editingIndex === index) clearForm()
1427
+ }
1428
+
1429
+ const clearForm = () => {
1430
+ setFormAlias('')
1431
+ setFormUrl('')
1432
+ setFormApiKey('')
1433
+ setFormInfo(null)
1434
+ setFormStatus('')
1435
+ setEditingIndex(-1)
1330
1436
  }
1331
1437
 
1332
1438
  const testAI = async () => {
1333
- if (!endpoint) return
1439
+ const ep = endpoints.find(e => e.alias === selectedEndpoint)
1440
+ if (!ep) return
1334
1441
  setTesting(true)
1335
1442
  setTestResult('')
1336
- const proxyUrl = toProxyUrl(endpoint)
1337
- const url = proxyUrl.includes('?') ? proxyUrl + '&mode=sync' : proxyUrl + '?mode=sync'
1338
- addLog('request', 'POST ' + url, { input: testInput })
1339
-
1340
- const startTime = Date.now()
1443
+ const proxyUrl = toProxyUrl(ep.url)
1444
+ addLog('request', 'POST ' + proxyUrl + ' (' + ep.alias + ')', { input: testInput })
1341
1445
  try {
1342
- const res = await fetch(url, {
1446
+ const res = await fetch(proxyUrl + '?mode=sync', {
1343
1447
  method: 'POST',
1344
- headers: {
1345
- 'Content-Type': 'application/json',
1346
- ...(apiKey ? { 'X-API-Key': apiKey } : {})
1347
- },
1448
+ headers: { 'Content-Type': 'application/json', ...(ep.apiKey ? { 'X-API-Key': ep.apiKey } : {}) },
1348
1449
  body: JSON.stringify({ input: { text: testInput }, variables: {} })
1349
1450
  })
1350
1451
  const data = await res.json()
1351
- const duration = Date.now() - startTime
1352
-
1353
1452
  if (!res.ok || data.error) {
1354
- const errMsg = data.detail || data.error?.message || data.error || res.statusText
1355
- setTestResult('Error: ' + errMsg)
1356
- addLog('error', res.status + ' (' + duration + 'ms)', data)
1453
+ setTestResult('Error: ' + (data.detail || data.error?.message || data.error || res.statusText))
1454
+ addLog('error', res.status + '', data)
1357
1455
  } else {
1358
- // Sync mode returns data.data.output or data.data.response
1359
1456
  const output = data.data?.output || data.data?.response || data.output || data.response || JSON.stringify(data, null, 2)
1360
1457
  const outputStr = typeof output === 'string' ? output : JSON.stringify(output, null, 2)
1361
1458
  setTestResult(outputStr)
1362
- addLog('success', '200 OK (' + duration + 'ms)', { output: outputStr.substring(0, 200) })
1459
+ addLog('success', '200 OK', { output: outputStr.substring(0, 200) })
1363
1460
  }
1364
1461
  } catch (err) {
1365
1462
  setTestResult('Error: ' + err.message)
@@ -1368,17 +1465,39 @@ export default function DevAdminPage() {
1368
1465
  setTesting(false)
1369
1466
  }
1370
1467
 
1468
+ const downloadManifest = () => {
1469
+ const manifest = localStorage.getItem('promptline_manifest')
1470
+ if (!manifest) return
1471
+ const blob = new Blob([manifest], { type: 'application/json' })
1472
+ const url = URL.createObjectURL(blob)
1473
+ const a = document.createElement('a')
1474
+ a.href = url
1475
+ a.download = 'promptline.config.json'
1476
+ a.click()
1477
+ URL.revokeObjectURL(url)
1478
+ addLog('info', 'Downloaded promptline.config.json')
1479
+ }
1480
+
1371
1481
  return (
1372
1482
  <div className="min-h-screen bg-gray-900 text-white p-8">
1373
- <div className="max-w-2xl mx-auto">
1483
+ <div className="max-w-4xl mx-auto">
1374
1484
  <div className="flex items-center justify-between mb-8">
1375
1485
  <div>
1376
1486
  <h1 className="text-3xl font-bold text-yellow-400">/_dev</h1>
1377
- <p className="text-gray-400 mt-1">PromptLine Endpoint (Local Only)</p>
1487
+ <p className="text-gray-400 mt-1">PromptLine Endpoints (Local Only)</p>
1488
+ </div>
1489
+ <div className="flex gap-3">
1490
+ <button
1491
+ onClick={downloadManifest}
1492
+ disabled={endpoints.length === 0}
1493
+ className="px-4 py-2 bg-indigo-600 rounded hover:bg-indigo-500 disabled:opacity-50"
1494
+ >
1495
+ Download Manifest
1496
+ </button>
1497
+ <Link to="/" className="px-4 py-2 bg-gray-700 rounded hover:bg-gray-600">
1498
+ Back to App
1499
+ </Link>
1378
1500
  </div>
1379
- <Link to="/" className="px-4 py-2 bg-gray-700 rounded hover:bg-gray-600">
1380
- Back to App
1381
- </Link>
1382
1501
  </div>
1383
1502
 
1384
1503
  <div className="bg-yellow-900/30 border border-yellow-600 rounded-lg p-4 mb-8">
@@ -1387,73 +1506,186 @@ export default function DevAdminPage() {
1387
1506
  </p>
1388
1507
  </div>
1389
1508
 
1390
- {/* Configuration */}
1509
+ {/* Your Pages */}
1391
1510
  <div className="bg-gray-800 rounded-lg p-6 mb-8">
1392
- <div className="flex items-center justify-between mb-4">
1393
- <h2 className="text-xl font-semibold">Configuration</h2>
1394
- {saved && (
1395
- <span className="px-2 py-1 bg-green-600/30 text-green-400 text-xs rounded">Saved</span>
1396
- )}
1511
+ <h2 className="text-xl font-semibold mb-4">Your Pages</h2>
1512
+ <div className="grid grid-cols-2 gap-4">
1513
+ <div>
1514
+ <h3 className="text-sm font-medium text-gray-400 mb-2">Public</h3>
1515
+ <div className="space-y-2">
1516
+ <Link to="/" className="block px-3 py-2 bg-gray-700 rounded hover:bg-gray-600 text-blue-400">
1517
+ / → index.tsx
1518
+ </Link>
1519
+ </div>
1520
+ </div>
1521
+ <div>
1522
+ <h3 className="text-sm font-medium text-gray-400 mb-2">Private (auth required)</h3>
1523
+ <div className="space-y-2">
1524
+ <Link to="/dashboard" className="block px-3 py-2 bg-gray-700 rounded hover:bg-gray-600 text-blue-400">
1525
+ /dashboard → dashboard.tsx
1526
+ </Link>
1527
+ <Link to="/settings" className="block px-3 py-2 bg-gray-700 rounded hover:bg-gray-600 text-blue-400">
1528
+ /settings → settings.tsx
1529
+ </Link>
1530
+ </div>
1531
+ </div>
1397
1532
  </div>
1533
+ </div>
1534
+
1535
+ {/* Saved Endpoints List */}
1536
+ {endpoints.length > 0 && (
1537
+ <div className="bg-gray-800 rounded-lg p-6 mb-8">
1538
+ <h2 className="text-xl font-semibold mb-4">Configured Endpoints ({endpoints.length})</h2>
1539
+ <div className="space-y-3">
1540
+ {endpoints.map((ep, i) => (
1541
+ <div key={ep.alias} className="flex items-center justify-between bg-gray-700 rounded p-4">
1542
+ <div className="flex items-center gap-4">
1543
+ <span className={
1544
+ ep.status === 'valid' ? 'text-green-400' :
1545
+ ep.status === 'error' ? 'text-red-400' :
1546
+ 'text-gray-400'
1547
+ }>●</span>
1548
+ <div>
1549
+ <div className="font-medium text-white">{ep.alias}</div>
1550
+ <div className="text-sm text-gray-400">
1551
+ {ep.info?.name || 'No info'} • {ep.info?.endpoint_type || 'unknown'}
1552
+ </div>
1553
+ </div>
1554
+ </div>
1555
+ <div className="flex gap-2">
1556
+ <button
1557
+ onClick={() => editEndpoint(i)}
1558
+ className="px-3 py-1 bg-gray-600 rounded text-sm hover:bg-gray-500"
1559
+ >
1560
+ Edit
1561
+ </button>
1562
+ <button
1563
+ onClick={() => deleteEndpoint(i)}
1564
+ className="px-3 py-1 bg-red-600/30 text-red-400 rounded text-sm hover:bg-red-600/50"
1565
+ >
1566
+ Delete
1567
+ </button>
1568
+ </div>
1569
+ </div>
1570
+ ))}
1571
+ </div>
1572
+ </div>
1573
+ )}
1574
+
1575
+ {/* Add/Edit Endpoint Form */}
1576
+ <div className="bg-gray-800 rounded-lg p-6 mb-8">
1577
+ <h2 className="text-xl font-semibold mb-4">
1578
+ {editingIndex >= 0 ? $BACKTICK}Edit Endpoint: \${formAlias}$BACKTICK : 'Add New Endpoint'}
1579
+ </h2>
1398
1580
 
1399
1581
  <div className="space-y-4">
1582
+ <div>
1583
+ <label className="block text-sm text-gray-400 mb-1">Alias (used in code)</label>
1584
+ <input
1585
+ value={formAlias}
1586
+ onChange={(e) => setFormAlias(e.target.value.replace(/[^a-zA-Z0-9_-]/g, ''))}
1587
+ className="w-full bg-gray-700 p-3 rounded text-white font-mono text-sm"
1588
+ placeholder="chatbot, traduction, analyse..."
1589
+ />
1590
+ <p className="text-xs text-gray-500 mt-1">Used in usePromptLine('{formAlias || 'alias'}')</p>
1591
+ </div>
1592
+
1400
1593
  <div>
1401
1594
  <label className="block text-sm text-gray-400 mb-1">Endpoint URL</label>
1402
1595
  <input
1403
- value={endpoint}
1404
- onChange={(e) => { setEndpoint(e.target.value); setSaved(false) }}
1596
+ value={formUrl}
1597
+ onChange={(e) => setFormUrl(e.target.value)}
1405
1598
  className="w-full bg-gray-700 p-3 rounded text-white font-mono text-sm"
1406
1599
  placeholder="https://app.promptlineops.com/api/v1/live/..."
1407
1600
  />
1408
- <p className="text-xs text-gray-500 mt-1">Copy the Live API URL from Creator Studio</p>
1409
1601
  </div>
1410
1602
 
1411
1603
  <div>
1412
1604
  <label className="block text-sm text-gray-400 mb-1">API Key</label>
1413
1605
  <input
1414
1606
  type="password"
1415
- value={apiKey}
1416
- onChange={(e) => { setApiKey(e.target.value); setSaved(false) }}
1607
+ value={formApiKey}
1608
+ onChange={(e) => setFormApiKey(e.target.value)}
1417
1609
  className="w-full bg-gray-700 p-3 rounded text-white font-mono text-sm"
1418
- placeholder="Your API key..."
1610
+ placeholder="sk_org_... or sk_ws_..."
1419
1611
  />
1420
- <p className="text-xs text-gray-500 mt-1">Required for health check. Get from /api-keys</p>
1421
1612
  </div>
1422
1613
 
1423
- <div className="flex gap-3">
1614
+ <div className="flex gap-3 flex-wrap">
1424
1615
  <button
1425
- onClick={saveConfig}
1426
- disabled={!endpoint}
1427
- className="px-6 py-2 bg-indigo-600 rounded font-medium hover:bg-indigo-500 disabled:opacity-50"
1616
+ onClick={checkHealth}
1617
+ disabled={!formUrl || !formApiKey || formStatus === 'checking'}
1618
+ className="px-4 py-2 bg-gray-600 rounded hover:bg-gray-500 disabled:opacity-50 flex items-center gap-2"
1619
+ >
1620
+ {formStatus === 'checking' ? 'Checking...' : 'Check Health'}
1621
+ {formStatus === 'valid' && <span className="text-green-400">●</span>}
1622
+ {formStatus === 'error' && <span className="text-red-400">●</span>}
1623
+ </button>
1624
+ <button
1625
+ onClick={fetchInfo}
1626
+ disabled={!formUrl || !formApiKey}
1627
+ className="px-4 py-2 bg-gray-600 rounded hover:bg-gray-500 disabled:opacity-50"
1428
1628
  >
1429
- Save
1629
+ Fetch Info
1430
1630
  </button>
1431
1631
  <button
1432
- onClick={testConnection}
1433
- disabled={!endpoint || !apiKey || status === 'testing'}
1434
- className="px-6 py-2 bg-gray-600 rounded font-medium hover:bg-gray-500 disabled:opacity-50 flex items-center gap-2"
1435
- title="Uses /isAlive endpoint (no LLM cost)"
1632
+ onClick={saveEndpoint}
1633
+ disabled={!formAlias || !formUrl}
1634
+ className="px-6 py-2 bg-indigo-600 rounded font-medium hover:bg-indigo-500 disabled:opacity-50"
1436
1635
  >
1437
- {status === 'testing' ? 'Checking...' : 'Check Health'}
1438
- {status === 'connected' && <span className="text-green-400">●</span>}
1439
- {status === 'error' && <span className="text-red-400">●</span>}
1636
+ {editingIndex >= 0 ? 'Update' : 'Add Endpoint'}
1440
1637
  </button>
1441
- {saved && (
1638
+ {(formAlias || formUrl || editingIndex >= 0) && (
1442
1639
  <button
1443
- onClick={clearConfig}
1444
- className="px-4 py-2 bg-red-600/30 text-red-400 rounded hover:bg-red-600/50"
1640
+ onClick={clearForm}
1641
+ className="px-4 py-2 bg-gray-700 rounded hover:bg-gray-600"
1445
1642
  >
1446
- Clear
1643
+ Cancel
1447
1644
  </button>
1448
1645
  )}
1449
1646
  </div>
1450
1647
  </div>
1648
+
1649
+ {/* Info Preview */}
1650
+ {formInfo && (
1651
+ <div className="mt-6 p-4 bg-gray-700 rounded">
1652
+ <h3 className="font-medium mb-3 text-gray-300">Endpoint Info</h3>
1653
+ <div className="grid grid-cols-2 gap-3 text-sm">
1654
+ <div><span className="text-gray-400">Name:</span> <span className="text-white">{formInfo.name}</span></div>
1655
+ <div><span className="text-gray-400">Status:</span> <span className={formInfo.status === 'active' ? 'text-green-400' : 'text-red-400'}>{formInfo.status}</span></div>
1656
+ <div><span className="text-gray-400">Type:</span> <span className="text-white">{formInfo.endpoint_type}</span></div>
1657
+ <div><span className="text-gray-400">Model:</span> <span className="text-white">{formInfo.ai_model}</span></div>
1658
+ {formInfo.required_variables?.length > 0 && (
1659
+ <div className="col-span-2">
1660
+ <span className="text-gray-400">Variables:</span>
1661
+ <span className="ml-2">{formInfo.required_variables.map(v => (
1662
+ <span key={v} className="ml-1 px-2 py-0.5 bg-gray-600 rounded text-xs text-blue-300">{v}</span>
1663
+ ))}</span>
1664
+ </div>
1665
+ )}
1666
+ </div>
1667
+ </div>
1668
+ )}
1451
1669
  </div>
1452
1670
 
1453
1671
  {/* Test AI */}
1454
- {saved && (
1672
+ {endpoints.length > 0 && (
1455
1673
  <div className="bg-gray-800 rounded-lg p-6 mb-8">
1456
1674
  <h2 className="text-xl font-semibold mb-4">Test AI</h2>
1675
+
1676
+ <div className="mb-4">
1677
+ <label className="block text-sm text-gray-400 mb-1">Select Endpoint</label>
1678
+ <select
1679
+ value={selectedEndpoint}
1680
+ onChange={(e) => setSelectedEndpoint(e.target.value)}
1681
+ className="w-full bg-gray-700 p-3 rounded text-white"
1682
+ >
1683
+ {endpoints.map(ep => (
1684
+ <option key={ep.alias} value={ep.alias}>{ep.alias} - {ep.info?.name || 'Unknown'}</option>
1685
+ ))}
1686
+ </select>
1687
+ </div>
1688
+
1457
1689
  <textarea
1458
1690
  value={testInput}
1459
1691
  onChange={(e) => setTestInput(e.target.value)}
@@ -1463,7 +1695,7 @@ export default function DevAdminPage() {
1463
1695
  />
1464
1696
  <button
1465
1697
  onClick={testAI}
1466
- disabled={testing}
1698
+ disabled={testing || !selectedEndpoint}
1467
1699
  className="px-6 py-2 bg-green-600 rounded font-medium hover:bg-green-500 disabled:opacity-50"
1468
1700
  >
1469
1701
  {testing ? 'Sending...' : 'Send Request'}
@@ -1478,7 +1710,7 @@ export default function DevAdminPage() {
1478
1710
  )}
1479
1711
 
1480
1712
  {/* Logs */}
1481
- <div className="bg-gray-800 rounded-lg p-6">
1713
+ <div className="bg-gray-800 rounded-lg p-6 mb-8">
1482
1714
  <div className="flex items-center justify-between mb-4">
1483
1715
  <h2 className="text-xl font-semibold">Logs</h2>
1484
1716
  <button onClick={() => setLogs([])} className="px-3 py-1 bg-gray-700 rounded text-sm hover:bg-gray-600">
@@ -1513,13 +1745,18 @@ export default function DevAdminPage() {
1513
1745
  </div>
1514
1746
 
1515
1747
  {/* Usage */}
1516
- <div className="mt-8 p-6 bg-gray-800 rounded-lg">
1748
+ <div className="bg-gray-800 rounded-lg p-6">
1517
1749
  <h2 className="text-xl font-semibold mb-4">Usage in Your Code</h2>
1518
1750
  <pre className="bg-gray-900 p-4 rounded text-sm overflow-x-auto">
1519
- {$BACKTICK}const { callAI } = usePromptLine()
1751
+ {$BACKTICK}// Use specific endpoint by alias
1752
+ const { execute } = usePromptLine('\${endpoints[0]?.alias || 'chatbot'}')
1520
1753
 
1521
- const result = await callAI('main', { text: 'Hello!' })
1522
- console.log(result.response)$BACKTICK}
1754
+ const result = await execute({ text: 'Hello!' })
1755
+ console.log(result.response)
1756
+
1757
+ // Multiple endpoints
1758
+ const { execute: chat } = usePromptLine('chatbot')
1759
+ const { execute: translate } = usePromptLine('traduction')$BACKTICK}
1523
1760
  </pre>
1524
1761
  </div>
1525
1762
  </div>
@@ -1822,13 +2059,26 @@ ${c.bold}Documentation:${c.reset} ${c.cyan}https://docs.promptlineops.com${c.res
1822
2059
  function parseArgs() {
1823
2060
  const args = process.argv.slice(2);
1824
2061
  const options = {
2062
+ command: 'new', // 'new' or 'get'
1825
2063
  name: null,
2064
+ source: null, // Git URL for 'get' command
1826
2065
  preset: null,
1827
2066
  yes: false,
1828
2067
  help: false,
1829
2068
  version: false,
1830
2069
  };
1831
2070
 
2071
+ // Detect 'get' command
2072
+ if (args[0] === 'get') {
2073
+ options.command = 'get';
2074
+ options.source = args[1];
2075
+ // Optional: destination name as third argument
2076
+ if (args[2] && !args[2].startsWith('-')) {
2077
+ options.name = args[2];
2078
+ }
2079
+ return options;
2080
+ }
2081
+
1832
2082
  for (let i = 0; i < args.length; i++) {
1833
2083
  const arg = args[i];
1834
2084
 
@@ -1848,23 +2098,138 @@ function parseArgs() {
1848
2098
  return options;
1849
2099
  }
1850
2100
 
2101
+ // Get project from Git repository
2102
+ async function getProject(gitUrl, destName) {
2103
+ const { execSync } = require('child_process');
2104
+
2105
+ printLogo();
2106
+
2107
+ // Extract repo name from URL if destName not provided
2108
+ if (!destName) {
2109
+ // Handle various URL formats
2110
+ // https://github.com/user/repo.git -> repo
2111
+ // git@github.com:user/repo.git -> repo
2112
+ const match = gitUrl.match(/\/([^\/]+?)(\.git)?$/) || gitUrl.match(/:([^\/]+?)(\.git)?$/);
2113
+ destName = match ? match[1] : 'promptline-app';
2114
+ }
2115
+
2116
+ // Validate destination name
2117
+ const validated = validatePackageName(destName);
2118
+ if (!validated.valid) {
2119
+ error(`Invalid project name "${destName}": ${validated.error}`);
2120
+ process.exit(1);
2121
+ }
2122
+
2123
+ const targetDir = path.resolve(process.cwd(), destName);
2124
+
2125
+ // Check if directory exists
2126
+ if (fs.existsSync(targetDir)) {
2127
+ error(`Directory "${destName}" already exists.`);
2128
+ process.exit(1);
2129
+ }
2130
+
2131
+ console.log(`${c.cyan}→${c.reset} Cloning from ${c.dim}${gitUrl}${c.reset}`);
2132
+ console.log(`${c.cyan}→${c.reset} Destination: ${c.green}${destName}${c.reset}\n`);
2133
+
2134
+ try {
2135
+ // Clone with depth 1 for speed, stdio inherit for git auth prompts
2136
+ execSync(`git clone --depth 1 "${gitUrl}" "${destName}"`, {
2137
+ stdio: 'inherit',
2138
+ cwd: process.cwd()
2139
+ });
2140
+
2141
+ // Remove .git directory
2142
+ const gitDir = path.join(targetDir, '.git');
2143
+ if (fs.existsSync(gitDir)) {
2144
+ fs.rmSync(gitDir, { recursive: true });
2145
+ }
2146
+ success('Removed .git directory');
2147
+
2148
+ // Initialize fresh git repo
2149
+ try {
2150
+ execSync('git init', { cwd: targetDir, stdio: 'pipe' });
2151
+ success('Initialized new git repository');
2152
+ } catch (e) {
2153
+ // Git init is optional
2154
+ }
2155
+
2156
+ // Detect project structure for instructions
2157
+ 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'));
2161
+
2162
+ // Success message with appropriate instructions
2163
+ console.log(`
2164
+ ${c.green}╔═══════════════════════════════════════════════════════════╗
2165
+ ║ ║
2166
+ ║ ${c.bold}Success!${c.reset}${c.green} Project ready in ${c.bold}${destName}${c.reset}${c.green} ║
2167
+ ║ ║
2168
+ ╚═══════════════════════════════════════════════════════════════╝${c.reset}
2169
+
2170
+ ${c.bold}Next steps:${c.reset}
2171
+
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}
2178
+ ${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
+ }
2188
+
2189
+ console.log(`
2190
+ ${c.bold}Key files:${c.reset}
2191
+
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
2195
+
2196
+ ${c.bold}Documentation:${c.reset} ${c.cyan}https://docs.promptlineops.com${c.reset}
2197
+ `);
2198
+
2199
+ } catch (err) {
2200
+ error(`Failed to clone repository: ${err.message}`);
2201
+ // Cleanup on failure
2202
+ if (fs.existsSync(targetDir)) {
2203
+ fs.rmSync(targetDir, { recursive: true });
2204
+ }
2205
+ process.exit(1);
2206
+ }
2207
+ }
2208
+
1851
2209
  function showHelp() {
1852
2210
  console.log(`
1853
- ${c.bold}create-promptline-app${c.reset} - Create PromptLine applications
2211
+ ${c.bold}promptlineapp${c.reset} - Create and get PromptLine applications
1854
2212
 
1855
2213
  ${c.bold}Usage:${c.reset}
1856
- npx create-promptline-app <app-name> [options]
2214
+ npx promptlineapp <app-name> [options] Create new app from scratch
2215
+ npx promptlineapp get <git-url> [name] Get existing app from Git
1857
2216
 
1858
- ${c.bold}Options:${c.reset}
2217
+ ${c.bold}Create options:${c.reset}
1859
2218
  -p, --preset <preset> Template preset (full-app, api)
1860
2219
  -y, --yes Skip prompts, use defaults
2220
+
2221
+ ${c.bold}General:${c.reset}
1861
2222
  -h, --help Show this help
1862
2223
  -v, --version Show version
1863
2224
 
1864
2225
  ${c.bold}Examples:${c.reset}
1865
- npx create-promptline-app my-app
1866
- npx create-promptline-app my-api --preset api
1867
- npx create-promptline-app my-app -y
2226
+ ${c.dim}# Create new app from scratch${c.reset}
2227
+ npx promptlineapp my-app
2228
+ npx promptlineapp my-api --preset api
2229
+
2230
+ ${c.dim}# Get existing use case from Git${c.reset}
2231
+ npx promptlineapp get https://github.com/promptline/claude-realestate.git
2232
+ npx promptlineapp get git@github.com:promptline/menumind.git my-restaurant
1868
2233
 
1869
2234
  ${c.bold}Presets:${c.reset}
1870
2235
  full-app Complete app with pages, dashboard & AI (default)
@@ -1891,13 +2256,29 @@ async function main() {
1891
2256
  process.exit(0);
1892
2257
  }
1893
2258
 
2259
+ // Route by command
2260
+ if (options.command === 'get') {
2261
+ if (!options.source) {
2262
+ printLogo();
2263
+ error('Please specify a Git URL:');
2264
+ console.log(`\n ${c.cyan}npx promptlineapp get${c.reset} ${c.green}<git-url>${c.reset} [name]\n`);
2265
+ console.log('For example:');
2266
+ console.log(` ${c.cyan}npx promptlineapp get${c.reset} ${c.green}https://github.com/promptline/claude-realestate.git${c.reset}\n`);
2267
+ console.log(`Run ${c.cyan}npx promptlineapp --help${c.reset} for more options.`);
2268
+ process.exit(1);
2269
+ }
2270
+ await getProject(options.source, options.name);
2271
+ return;
2272
+ }
2273
+
2274
+ // Default: create new app
1894
2275
  if (!options.name) {
1895
2276
  printLogo();
1896
2277
  error('Please specify a project name:');
1897
- console.log(`\n ${c.cyan}npx create-promptline-app${c.reset} ${c.green}<app-name>${c.reset}\n`);
2278
+ console.log(`\n ${c.cyan}npx promptlineapp${c.reset} ${c.green}<app-name>${c.reset}\n`);
1898
2279
  console.log('For example:');
1899
- console.log(` ${c.cyan}npx create-promptline-app${c.reset} ${c.green}my-restaurant-app${c.reset}\n`);
1900
- console.log(`Run ${c.cyan}npx create-promptline-app --help${c.reset} for more options.`);
2280
+ console.log(` ${c.cyan}npx promptlineapp${c.reset} ${c.green}my-restaurant-app${c.reset}\n`);
2281
+ console.log(`Run ${c.cyan}npx promptlineapp --help${c.reset} for more options.`);
1901
2282
  process.exit(1);
1902
2283
  }
1903
2284
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "promptlineapp",
3
- "version": "1.4.1",
3
+ "version": "1.6.0",
4
4
  "description": "Create PromptLine applications with ease",
5
5
  "author": "PromptLine <support@promptlineops.com>",
6
6
  "license": "MIT",