@xeplr/cli 0.1.1
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/LICENSE +21 -0
- package/README.md +95 -0
- package/bin/xeplr.js +77 -0
- package/lib/commands/encrypt.js +144 -0
- package/lib/generate/index.js +147 -0
- package/lib/nextSteps.js +39 -0
- package/lib/plan.js +44 -0
- package/lib/prompt.js +130 -0
- package/lib/questions.js +125 -0
- package/package.json +39 -0
- package/templates/api/app.js +21 -0
- package/templates/api/bin/www +62 -0
- package/templates/api/db/setup.js +10 -0
- package/templates/api/development.env.tpl +47 -0
- package/templates/api/env.required.js +29 -0
- package/templates/api/gitignore.tpl +2 -0
- package/templates/api/migrations/0001_tasks.sql +23 -0
- package/templates/api/migrations-auth/0001_home_menu.sql +14 -0
- package/templates/api/migrations-auth/0002_tasks_menu.sql +11 -0
- package/templates/api/models/Task.js +48 -0
- package/templates/api/models/index.js +7 -0
- package/templates/api/package.json +30 -0
- package/templates/api/routes/index.js +46 -0
- package/templates/ui/dotenv.tpl +8 -0
- package/templates/ui/gitignore.tpl +2 -0
- package/templates/ui/index.html +12 -0
- package/templates/ui/package.json +21 -0
- package/templates/ui/public/logo-wide.svg +6 -0
- package/templates/ui/public/logo.svg +4 -0
- package/templates/ui/src/App.jsx +84 -0
- package/templates/ui/src/api/tasks.js +34 -0
- package/templates/ui/src/index.css +275 -0
- package/templates/ui/src/main.jsx +27 -0
- package/templates/ui/src/pages/Home.jsx +73 -0
- package/templates/ui/src/pages/Tasks.jsx +106 -0
- package/templates/ui/src/pages/TasksHelp.jsx +70 -0
- package/templates/ui/src/useTasksController.js +83 -0
- package/templates/ui/vite.config.js +50 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// The panel beside the sample form. It is here so the first thing anybody
|
|
2
|
+
// builds is explained where they are looking, rather than in a document they
|
|
3
|
+
// have to go and find.
|
|
4
|
+
export default function TasksHelp() {
|
|
5
|
+
return (
|
|
6
|
+
<aside className="app-help">
|
|
7
|
+
<div className="app-help-badge">Sample</div>
|
|
8
|
+
<h2>This page is an example</h2>
|
|
9
|
+
<p>
|
|
10
|
+
A task has a title, a description, a status and a due date. Nothing here
|
|
11
|
+
is special — it is the same five pieces you will write for anything your
|
|
12
|
+
app stores. Delete it once you have your own.
|
|
13
|
+
</p>
|
|
14
|
+
|
|
15
|
+
<h3>Add your own, in five pieces</h3>
|
|
16
|
+
<ol className="app-help-steps">
|
|
17
|
+
<li>
|
|
18
|
+
<strong>The table</strong>
|
|
19
|
+
<code>api/migrations/0002_things.sql</code>
|
|
20
|
+
<span>Runs by itself when the API next starts.</span>
|
|
21
|
+
</li>
|
|
22
|
+
<li>
|
|
23
|
+
<strong>The model</strong>
|
|
24
|
+
<code>api/models/Thing.js</code>
|
|
25
|
+
<span>
|
|
26
|
+
Names the table and lists the fields, with the rules for each. Those
|
|
27
|
+
rules are checked on every save.
|
|
28
|
+
</span>
|
|
29
|
+
</li>
|
|
30
|
+
<li>
|
|
31
|
+
<strong>The routes</strong>
|
|
32
|
+
<code>api/routes/index.js</code>
|
|
33
|
+
<span>
|
|
34
|
+
One line — hand the model to <code>genericRoute</code> and you get
|
|
35
|
+
list, fetch, save and delete. You write none of them.
|
|
36
|
+
</span>
|
|
37
|
+
</li>
|
|
38
|
+
<li>
|
|
39
|
+
<strong>The page</strong>
|
|
40
|
+
<code>ui/src/pages/Things.jsx</code>
|
|
41
|
+
<span>
|
|
42
|
+
Plus a file for the calls and one for the form's behaviour, so a new
|
|
43
|
+
look means replacing the page only.
|
|
44
|
+
</span>
|
|
45
|
+
</li>
|
|
46
|
+
<li>
|
|
47
|
+
<strong>The menu entry</strong>
|
|
48
|
+
<code>api/migrations-auth/000N_things_menu.sql</code>
|
|
49
|
+
<span>
|
|
50
|
+
The side-rail item appears only if its name exists in the database,
|
|
51
|
+
spelled exactly the same.
|
|
52
|
+
</span>
|
|
53
|
+
</li>
|
|
54
|
+
</ol>
|
|
55
|
+
|
|
56
|
+
<h3>Two things that catch people</h3>
|
|
57
|
+
<ul className="app-help-notes">
|
|
58
|
+
<li>
|
|
59
|
+
Keep every data address under <code>/api</code>. Serve data at{' '}
|
|
60
|
+
<code>/things</code> and it hides the <em>page</em> at the same
|
|
61
|
+
address — you get raw data instead of your app.
|
|
62
|
+
</li>
|
|
63
|
+
<li>
|
|
64
|
+
A new menu entry is remembered for ten minutes. Restart the sign-in
|
|
65
|
+
service if you want it immediately.
|
|
66
|
+
</li>
|
|
67
|
+
</ul>
|
|
68
|
+
</aside>
|
|
69
|
+
)
|
|
70
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// CONTROLLER LAYER — state and handlers, no JSX.
|
|
2
|
+
//
|
|
3
|
+
// The same split the @xeplr/ui-* libraries use. A different design can import
|
|
4
|
+
// this hook and render the fields however it likes; the behaviour below is
|
|
5
|
+
// unchanged by that, which is what makes a rebrand safe.
|
|
6
|
+
import { useCallback, useEffect, useState } from 'react'
|
|
7
|
+
import { listTasks, saveTasks, deleteTasks } from './api/tasks.js'
|
|
8
|
+
|
|
9
|
+
const EMPTY = { title: '', description: '', status: 'todo', dueDate: '' }
|
|
10
|
+
|
|
11
|
+
export function useTasksController() {
|
|
12
|
+
const [tasks, setTasks] = useState([])
|
|
13
|
+
const [form, setForm] = useState(EMPTY)
|
|
14
|
+
const [editingId, setEditingId] = useState(null)
|
|
15
|
+
const [busy, setBusy] = useState(false)
|
|
16
|
+
const [error, setError] = useState(null)
|
|
17
|
+
|
|
18
|
+
const refresh = useCallback(async () => {
|
|
19
|
+
try {
|
|
20
|
+
setTasks(await listTasks())
|
|
21
|
+
setError(null)
|
|
22
|
+
} catch (e) {
|
|
23
|
+
setError(e.message)
|
|
24
|
+
}
|
|
25
|
+
}, [])
|
|
26
|
+
|
|
27
|
+
useEffect(() => { refresh() }, [refresh])
|
|
28
|
+
|
|
29
|
+
const setField = useCallback((name, value) => {
|
|
30
|
+
setForm((f) => ({ ...f, [name]: value }))
|
|
31
|
+
}, [])
|
|
32
|
+
|
|
33
|
+
const edit = useCallback((task) => {
|
|
34
|
+
setEditingId(task.id)
|
|
35
|
+
setForm({
|
|
36
|
+
title: task.title || '',
|
|
37
|
+
description: task.description || '',
|
|
38
|
+
status: task.status || 'todo',
|
|
39
|
+
// The API returns a full timestamp; a date input wants just the day.
|
|
40
|
+
dueDate: task.dueDate ? String(task.dueDate).slice(0, 10) : ''
|
|
41
|
+
})
|
|
42
|
+
}, [])
|
|
43
|
+
|
|
44
|
+
const cancel = useCallback(() => {
|
|
45
|
+
setEditingId(null)
|
|
46
|
+
setForm(EMPTY)
|
|
47
|
+
}, [])
|
|
48
|
+
|
|
49
|
+
const submit = useCallback(async (e) => {
|
|
50
|
+
if (e) e.preventDefault()
|
|
51
|
+
setBusy(true)
|
|
52
|
+
try {
|
|
53
|
+
// An entry with no id is a create; with one, an update. Same call.
|
|
54
|
+
const entry = { ...form, dueDate: form.dueDate || null }
|
|
55
|
+
if (editingId) entry.id = editingId
|
|
56
|
+
await saveTasks([entry])
|
|
57
|
+
cancel()
|
|
58
|
+
await refresh()
|
|
59
|
+
setError(null)
|
|
60
|
+
} catch (e2) {
|
|
61
|
+
// The API refuses on the model's own jsonSchema and names the field, so
|
|
62
|
+
// showing its message beats inventing our own.
|
|
63
|
+
setError(e2.message)
|
|
64
|
+
} finally {
|
|
65
|
+
setBusy(false)
|
|
66
|
+
}
|
|
67
|
+
}, [form, editingId, cancel, refresh])
|
|
68
|
+
|
|
69
|
+
const remove = useCallback(async (id) => {
|
|
70
|
+
setBusy(true)
|
|
71
|
+
try {
|
|
72
|
+
await deleteTasks([id])
|
|
73
|
+
if (editingId === id) cancel()
|
|
74
|
+
await refresh()
|
|
75
|
+
} catch (e) {
|
|
76
|
+
setError(e.message)
|
|
77
|
+
} finally {
|
|
78
|
+
setBusy(false)
|
|
79
|
+
}
|
|
80
|
+
}, [editingId, cancel, refresh])
|
|
81
|
+
|
|
82
|
+
return { tasks, form, setField, submit, edit, cancel, remove, editingId, busy, error, refresh }
|
|
83
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { defineConfig, loadEnv } from 'vite'
|
|
2
|
+
import react from '@vitejs/plugin-react'
|
|
3
|
+
|
|
4
|
+
// EVERY API PREFIX THIS APP CALLS.
|
|
5
|
+
//
|
|
6
|
+
// A path missing from this list does NOT fail loudly — Vite falls through to
|
|
7
|
+
// the SPA and serves index.html, so the fetch succeeds and the JSON parse is
|
|
8
|
+
// what breaks: `Unexpected token '<'`. Adding a route to the backend is only
|
|
9
|
+
// half of shipping an endpoint; it has to be listed here too.
|
|
10
|
+
const API_PATHS = [
|
|
11
|
+
'/health',
|
|
12
|
+
'/whoami',
|
|
13
|
+
// Every data address lives under /api, so a PAGE path and a DATA path can
|
|
14
|
+
// never collide. Without that split, adding '/tasks' here shadowed the UI's
|
|
15
|
+
// own /tasks page — opening it showed raw JSON instead of the app.
|
|
16
|
+
'/api'
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
export default defineConfig(({ mode }) => {
|
|
20
|
+
// Prefix '' so plain names load, not just VITE_-prefixed ones. These are dev
|
|
21
|
+
// server settings, not values shipped to the browser.
|
|
22
|
+
const env = loadEnv(mode, process.cwd(), '')
|
|
23
|
+
|
|
24
|
+
// NO PORT LITERALS. A hardcoded fallback is a second answer to a question
|
|
25
|
+
// .env already answers, and it is the one that wins when the file is missing
|
|
26
|
+
// — exactly when you want to be told rather than quietly served elsewhere.
|
|
27
|
+
const port = Number(env.UI_PORT)
|
|
28
|
+
const authUrl = env.AUTH_URL
|
|
29
|
+
const apiUrl = env.API_URL
|
|
30
|
+
if (!port || !authUrl || !apiUrl) {
|
|
31
|
+
throw new Error('ui/.env must set UI_PORT, AUTH_URL and API_URL')
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const proxy = {
|
|
35
|
+
// Only the real network calls (/auth/api/*), NOT bare /auth — that prefix
|
|
36
|
+
// is also used by client-side routes (/auth/login, /auth/profile), which
|
|
37
|
+
// must fall through to Vite's SPA index.html rather than the auth backend.
|
|
38
|
+
'/auth/api': authUrl
|
|
39
|
+
}
|
|
40
|
+
API_PATHS.forEach((p) => { proxy[p] = apiUrl })
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
plugins: [react()],
|
|
44
|
+
// Force every import to THIS app's copy, so hook-using libraries don't
|
|
45
|
+
// load twice and break the rules of hooks in ways that read as impossible.
|
|
46
|
+
resolve: { dedupe: ['react', 'react-dom', 'react-router-dom'] },
|
|
47
|
+
server: { port, strictPort: true, proxy },
|
|
48
|
+
preview: { port, strictPort: true }
|
|
49
|
+
}
|
|
50
|
+
})
|