@lazycatcloud/lzc-cli 1.3.17 → 2.0.0-pre.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/README.md +47 -7
- package/changelog.md +14 -0
- package/lib/app/apkshell.js +7 -44
- package/lib/app/index.js +178 -64
- package/lib/app/lpk_build.js +446 -61
- package/lib/app/lpk_build_images.js +749 -0
- package/lib/app/lpk_create.js +192 -45
- package/lib/app/lpk_create_generator.js +141 -13
- package/lib/app/lpk_devshell.js +33 -19
- package/lib/app/lpk_embed_images.js +257 -0
- package/lib/app/lpk_installer.js +17 -9
- package/lib/app/manifest_build.js +259 -0
- package/lib/app/project_cp.js +59 -0
- package/lib/app/project_deploy.js +58 -0
- package/lib/app/project_exec.js +82 -0
- package/lib/app/project_info.js +106 -0
- package/lib/app/project_log.js +62 -0
- package/lib/app/project_runtime.js +356 -0
- package/lib/app/project_start.js +95 -0
- package/lib/app/project_sync.js +499 -0
- package/lib/appstore/apkshell.js +50 -0
- package/lib/box/index.js +101 -4
- package/lib/box/ssh_remote.js +259 -0
- package/lib/build_remote.js +21 -0
- package/lib/debug_bridge.js +891 -83
- package/lib/docker/index.js +30 -10
- package/lib/i18n/locales/en/translation.json +262 -255
- package/lib/i18n/locales/zh/translation.json +262 -255
- package/lib/lpk/core.js +488 -0
- package/lib/lpk/index.js +210 -0
- package/lib/migrate/index.js +52 -0
- package/lib/package_info.js +135 -0
- package/lib/shellapi.js +35 -1
- package/lib/sig/core.js +254 -0
- package/lib/sig/index.js +88 -0
- package/lib/utils.js +94 -15
- package/package.json +3 -3
- package/scripts/cli.js +6 -0
- package/scripts/smoke/frontend-dev-entry.mjs +104 -0
- package/scripts/smoke/template-project.mjs +311 -0
- package/template/_lpk/README.md +15 -4
- package/template/_lpk/gui-vnc.manifest.yml.in +18 -0
- package/template/_lpk/hello-vue.manifest.yml.in +38 -0
- package/template/_lpk/manifest.yml.in +4 -11
- package/template/_lpk/package.yml.in +7 -0
- package/template/_lpk/todolist-golang.manifest.yml.in +30 -0
- package/template/_lpk/todolist-java.manifest.yml.in +29 -0
- package/template/_lpk/todolist-python.manifest.yml.in +37 -0
- package/template/_lpk/todolist-serverless.manifest.yml.in +38 -0
- package/template/_lpk/vue.lzc-build.yml.in +0 -44
- package/template/blank/lzc-build.dev.yml +4 -0
- package/template/blank/lzc-build.yml +24 -41
- package/template/blank/lzc-manifest.yml +7 -9
- package/template/blank/package.yml +7 -0
- package/template/golang/Dockerfile +19 -0
- package/template/golang/Dockerfile.dev +20 -0
- package/template/golang/README.md +44 -0
- package/template/golang/_gitignore +3 -0
- package/template/golang/_lzcdevignore +21 -0
- package/template/golang/go.mod +3 -0
- package/template/golang/lzc-build.dev.yml +12 -0
- package/template/golang/lzc-build.yml +16 -0
- package/template/golang/lzc-icon.png +0 -0
- package/template/golang/main.go +252 -0
- package/template/golang/manifest.dev.page.js +24 -0
- package/template/golang/run.sh +10 -0
- package/template/golang/web/index.html +238 -0
- package/template/gui-vnc/README.md +23 -0
- package/template/gui-vnc/_gitignore +2 -0
- package/template/gui-vnc/images/Dockerfile +30 -0
- package/template/gui-vnc/images/kasmvnc.yaml +33 -0
- package/template/gui-vnc/images/startup-script.desktop +9 -0
- package/template/gui-vnc/images/startup-script.sh +6 -0
- package/template/gui-vnc/lzc-build.dev.yml +4 -0
- package/template/gui-vnc/lzc-build.yml +18 -0
- package/template/gui-vnc/lzc-icon.png +0 -0
- package/template/python/Dockerfile +15 -0
- package/template/python/Dockerfile.dev +18 -0
- package/template/python/README.md +50 -0
- package/template/python/_gitignore +3 -0
- package/template/python/_lzcdevignore +21 -0
- package/template/python/app.py +110 -0
- package/template/python/lzc-build.dev.yml +12 -0
- package/template/python/lzc-build.yml +16 -0
- package/template/python/lzc-icon.png +0 -0
- package/template/python/manifest.dev.page.js +25 -0
- package/template/python/requirements.txt +1 -0
- package/template/python/run.sh +14 -0
- package/template/python/web/index.html +238 -0
- package/template/springboot/Dockerfile +20 -0
- package/template/springboot/Dockerfile.dev +20 -0
- package/template/springboot/README.md +44 -0
- package/template/springboot/_gitignore +3 -0
- package/template/springboot/_lzcdevignore +21 -0
- package/template/springboot/lzc-build.dev.yml +12 -0
- package/template/springboot/lzc-build.yml +16 -0
- package/template/springboot/lzc-icon.png +0 -0
- package/template/springboot/manifest.dev.page.js +24 -0
- package/template/springboot/pom.xml +38 -0
- package/template/springboot/run.sh +10 -0
- package/template/springboot/src/main/java/cloud/lazycat/app/Application.java +132 -0
- package/template/springboot/src/main/resources/application.properties +1 -0
- package/template/springboot/src/main/resources/static/index.html +238 -0
- package/template/vue/README.md +18 -21
- package/template/vue/lzc-build.dev.yml +7 -0
- package/template/vue/lzc-build.yml +30 -43
- package/template/vue/manifest.dev.page.js +50 -0
- package/template/vue/src/App.vue +36 -25
- package/template/vue/src/style.css +106 -49
- package/template/vue-minidb/README.md +26 -0
- package/template/vue-minidb/_gitignore +25 -0
- package/template/vue-minidb/index.html +13 -0
- package/template/vue-minidb/lzc-build.dev.yml +7 -0
- package/template/vue-minidb/lzc-build.yml +46 -0
- package/template/vue-minidb/lzc-icon.png +0 -0
- package/template/vue-minidb/manifest.dev.page.js +50 -0
- package/template/vue-minidb/package.json +21 -0
- package/template/vue-minidb/public/vite.svg +1 -0
- package/template/vue-minidb/src/App.vue +206 -0
- package/template/vue-minidb/src/assets/vue.svg +1 -0
- package/template/vue-minidb/src/main.ts +5 -0
- package/template/vue-minidb/src/style.css +136 -0
- package/template/vue-minidb/src/vite-env.d.ts +1 -0
- package/template/vue-minidb/tsconfig.app.json +24 -0
- package/template/vue-minidb/tsconfig.json +7 -0
- package/template/vue-minidb/tsconfig.node.json +22 -0
- package/template/vue-minidb/vite.config.ts +10 -0
- /package/template/{vue → vue-minidb}/src/components/HelloWorld.vue +0 -0
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
package main
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"encoding/json"
|
|
5
|
+
"net/http"
|
|
6
|
+
"os"
|
|
7
|
+
"path/filepath"
|
|
8
|
+
"sort"
|
|
9
|
+
"strconv"
|
|
10
|
+
"strings"
|
|
11
|
+
"sync"
|
|
12
|
+
"sync/atomic"
|
|
13
|
+
"time"
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
type todoItem struct {
|
|
17
|
+
ID int64 `json:"id"`
|
|
18
|
+
Title string `json:"title"`
|
|
19
|
+
Done bool `json:"done"`
|
|
20
|
+
UpdatedAt int64 `json:"updatedAt"`
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
type todoStore struct {
|
|
24
|
+
mu sync.RWMutex
|
|
25
|
+
nextID atomic.Int64
|
|
26
|
+
items map[int64]todoItem
|
|
27
|
+
file string
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
func newTodoStore(file string) (*todoStore, error) {
|
|
31
|
+
store := &todoStore{
|
|
32
|
+
items: map[int64]todoItem{},
|
|
33
|
+
file: file,
|
|
34
|
+
}
|
|
35
|
+
store.nextID.Store(1)
|
|
36
|
+
if err := os.MkdirAll(filepath.Dir(file), 0o755); err != nil {
|
|
37
|
+
return nil, err
|
|
38
|
+
}
|
|
39
|
+
if err := store.load(); err != nil {
|
|
40
|
+
return nil, err
|
|
41
|
+
}
|
|
42
|
+
return store, nil
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
func (s *todoStore) load() error {
|
|
46
|
+
bs, err := os.ReadFile(s.file)
|
|
47
|
+
if err != nil {
|
|
48
|
+
if os.IsNotExist(err) {
|
|
49
|
+
return nil
|
|
50
|
+
}
|
|
51
|
+
return err
|
|
52
|
+
}
|
|
53
|
+
var items []todoItem
|
|
54
|
+
if len(bs) == 0 {
|
|
55
|
+
return nil
|
|
56
|
+
}
|
|
57
|
+
if err := json.Unmarshal(bs, &items); err != nil {
|
|
58
|
+
return err
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
var maxID int64 = 0
|
|
62
|
+
for _, item := range items {
|
|
63
|
+
s.items[item.ID] = item
|
|
64
|
+
if item.ID > maxID {
|
|
65
|
+
maxID = item.ID
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
s.nextID.Store(maxID + 1)
|
|
69
|
+
return nil
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
func (s *todoStore) persistLocked() error {
|
|
73
|
+
items := make([]todoItem, 0, len(s.items))
|
|
74
|
+
for _, item := range s.items {
|
|
75
|
+
items = append(items, item)
|
|
76
|
+
}
|
|
77
|
+
sort.Slice(items, func(i, j int) bool {
|
|
78
|
+
return items[i].ID < items[j].ID
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
bs, err := json.MarshalIndent(items, "", " ")
|
|
82
|
+
if err != nil {
|
|
83
|
+
return err
|
|
84
|
+
}
|
|
85
|
+
tmpFile := s.file + ".tmp"
|
|
86
|
+
if err := os.WriteFile(tmpFile, bs, 0o644); err != nil {
|
|
87
|
+
return err
|
|
88
|
+
}
|
|
89
|
+
return os.Rename(tmpFile, s.file)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
func (s *todoStore) list() []todoItem {
|
|
93
|
+
s.mu.RLock()
|
|
94
|
+
defer s.mu.RUnlock()
|
|
95
|
+
items := make([]todoItem, 0, len(s.items))
|
|
96
|
+
for _, item := range s.items {
|
|
97
|
+
items = append(items, item)
|
|
98
|
+
}
|
|
99
|
+
sort.Slice(items, func(i, j int) bool {
|
|
100
|
+
return items[i].UpdatedAt > items[j].UpdatedAt
|
|
101
|
+
})
|
|
102
|
+
return items
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
func (s *todoStore) add(title string) (todoItem, error) {
|
|
106
|
+
id := s.nextID.Add(1) - 1
|
|
107
|
+
item := todoItem{
|
|
108
|
+
ID: id,
|
|
109
|
+
Title: title,
|
|
110
|
+
Done: false,
|
|
111
|
+
UpdatedAt: time.Now().UnixMilli(),
|
|
112
|
+
}
|
|
113
|
+
s.mu.Lock()
|
|
114
|
+
s.items[id] = item
|
|
115
|
+
err := s.persistLocked()
|
|
116
|
+
s.mu.Unlock()
|
|
117
|
+
if err != nil {
|
|
118
|
+
return todoItem{}, err
|
|
119
|
+
}
|
|
120
|
+
return item, nil
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
func (s *todoStore) toggle(id int64) (todoItem, bool, error) {
|
|
124
|
+
s.mu.Lock()
|
|
125
|
+
defer s.mu.Unlock()
|
|
126
|
+
item, ok := s.items[id]
|
|
127
|
+
if !ok {
|
|
128
|
+
return todoItem{}, false, nil
|
|
129
|
+
}
|
|
130
|
+
item.Done = !item.Done
|
|
131
|
+
item.UpdatedAt = time.Now().UnixMilli()
|
|
132
|
+
s.items[id] = item
|
|
133
|
+
if err := s.persistLocked(); err != nil {
|
|
134
|
+
return todoItem{}, false, err
|
|
135
|
+
}
|
|
136
|
+
return item, true, nil
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
func (s *todoStore) remove(id int64) (bool, error) {
|
|
140
|
+
s.mu.Lock()
|
|
141
|
+
defer s.mu.Unlock()
|
|
142
|
+
if _, ok := s.items[id]; !ok {
|
|
143
|
+
return false, nil
|
|
144
|
+
}
|
|
145
|
+
delete(s.items, id)
|
|
146
|
+
if err := s.persistLocked(); err != nil {
|
|
147
|
+
return false, err
|
|
148
|
+
}
|
|
149
|
+
return true, nil
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
func writeJSON(w http.ResponseWriter, status int, value any) {
|
|
153
|
+
w.Header().Set("Content-Type", "application/json")
|
|
154
|
+
w.WriteHeader(status)
|
|
155
|
+
_ = json.NewEncoder(w).Encode(value)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
func main() {
|
|
159
|
+
store, err := newTodoStore("/lzcapp/var/todos.json")
|
|
160
|
+
if err != nil {
|
|
161
|
+
panic(err)
|
|
162
|
+
}
|
|
163
|
+
mux := http.NewServeMux()
|
|
164
|
+
|
|
165
|
+
mux.HandleFunc("/api/health", func(w http.ResponseWriter, r *http.Request) {
|
|
166
|
+
if r.Method != http.MethodGet {
|
|
167
|
+
w.WriteHeader(http.StatusMethodNotAllowed)
|
|
168
|
+
return
|
|
169
|
+
}
|
|
170
|
+
writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
mux.HandleFunc("/api/todos", func(w http.ResponseWriter, r *http.Request) {
|
|
174
|
+
switch r.Method {
|
|
175
|
+
case http.MethodGet:
|
|
176
|
+
writeJSON(w, http.StatusOK, map[string]any{"items": store.list()})
|
|
177
|
+
case http.MethodPost:
|
|
178
|
+
var payload struct {
|
|
179
|
+
Title string `json:"title"`
|
|
180
|
+
}
|
|
181
|
+
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
|
182
|
+
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid json"})
|
|
183
|
+
return
|
|
184
|
+
}
|
|
185
|
+
title := strings.TrimSpace(payload.Title)
|
|
186
|
+
if title == "" {
|
|
187
|
+
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "title is required"})
|
|
188
|
+
return
|
|
189
|
+
}
|
|
190
|
+
item, err := store.add(title)
|
|
191
|
+
if err != nil {
|
|
192
|
+
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "failed to persist todos"})
|
|
193
|
+
return
|
|
194
|
+
}
|
|
195
|
+
writeJSON(w, http.StatusOK, item)
|
|
196
|
+
default:
|
|
197
|
+
w.WriteHeader(http.StatusMethodNotAllowed)
|
|
198
|
+
}
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
mux.HandleFunc("/api/todos/", func(w http.ResponseWriter, r *http.Request) {
|
|
202
|
+
tail := strings.TrimPrefix(r.URL.Path, "/api/todos/")
|
|
203
|
+
if strings.HasSuffix(tail, "/toggle") {
|
|
204
|
+
if r.Method != http.MethodPut {
|
|
205
|
+
w.WriteHeader(http.StatusMethodNotAllowed)
|
|
206
|
+
return
|
|
207
|
+
}
|
|
208
|
+
idPart := strings.TrimSuffix(tail, "/toggle")
|
|
209
|
+
idPart = strings.TrimSuffix(idPart, "/")
|
|
210
|
+
id, err := strconv.ParseInt(idPart, 10, 64)
|
|
211
|
+
if err != nil {
|
|
212
|
+
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid id"})
|
|
213
|
+
return
|
|
214
|
+
}
|
|
215
|
+
item, ok, err := store.toggle(id)
|
|
216
|
+
if err != nil {
|
|
217
|
+
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "failed to persist todos"})
|
|
218
|
+
return
|
|
219
|
+
}
|
|
220
|
+
if !ok {
|
|
221
|
+
writeJSON(w, http.StatusNotFound, map[string]string{"error": "todo not found"})
|
|
222
|
+
return
|
|
223
|
+
}
|
|
224
|
+
writeJSON(w, http.StatusOK, item)
|
|
225
|
+
return
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if r.Method != http.MethodDelete {
|
|
229
|
+
w.WriteHeader(http.StatusMethodNotAllowed)
|
|
230
|
+
return
|
|
231
|
+
}
|
|
232
|
+
idPart := strings.TrimSuffix(tail, "/")
|
|
233
|
+
id, err := strconv.ParseInt(idPart, 10, 64)
|
|
234
|
+
if err != nil {
|
|
235
|
+
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid id"})
|
|
236
|
+
return
|
|
237
|
+
}
|
|
238
|
+
removed, err := store.remove(id)
|
|
239
|
+
if err != nil {
|
|
240
|
+
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "failed to persist todos"})
|
|
241
|
+
return
|
|
242
|
+
}
|
|
243
|
+
if !removed {
|
|
244
|
+
writeJSON(w, http.StatusNotFound, map[string]string{"error": "todo not found"})
|
|
245
|
+
return
|
|
246
|
+
}
|
|
247
|
+
writeJSON(w, http.StatusOK, map[string]bool{"ok": true})
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
mux.Handle("/", http.FileServer(http.Dir("/app/web")))
|
|
251
|
+
_ = http.ListenAndServe(":3000", mux)
|
|
252
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
function renderDevPage(port) {
|
|
2
|
+
const title = "Backend dev service is not ready";
|
|
3
|
+
const subtitle = "This app is running in dev mode and expects the backend service to be started manually inside the app container.";
|
|
4
|
+
const pageTitle = "Backend Dev";
|
|
5
|
+
const steps = [
|
|
6
|
+
"Sync your latest code into the container with <code>lzc-cli project sync --watch</code> or copy it with <code>lzc-cli project cp</code>.",
|
|
7
|
+
"Open a shell with <code>lzc-cli project exec /bin/sh</code> and start or restart the backend process with <code>/app/run.sh</code>.",
|
|
8
|
+
"Refresh this page after port <code>" + String(port) + "</code> is ready.",
|
|
9
|
+
];
|
|
10
|
+
const portText = port === undefined || port === null ? "" : String(port);
|
|
11
|
+
const items = steps.map(function (step) {
|
|
12
|
+
return "<li>" + step + "</li>";
|
|
13
|
+
}).join("");
|
|
14
|
+
const portBadge = portText ? "<div class=\"pill\">Expected local port: " + portText + "</div>" : "";
|
|
15
|
+
return [
|
|
16
|
+
"<!doctype html>",
|
|
17
|
+
"<html lang=\"en\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"><title>", pageTitle, "</title><style>",
|
|
18
|
+
"body{margin:0;padding:32px;font-family:ui-sans-serif,system-ui,sans-serif;background:#f5f8fc;color:#112033}",
|
|
19
|
+
".card{max-width:760px;margin:0 auto;background:#fff;border:1px solid #d9e4f2;border-radius:18px;padding:24px;box-shadow:0 16px 40px rgba(17,32,51,.08)}",
|
|
20
|
+
"h1{margin:0 0 12px;font-size:32px}.muted{color:#51627a}.pill{display:inline-block;margin:16px 0 0;padding:6px 10px;border-radius:999px;background:#eef5ff;color:#0c5dd6;font-weight:600}",
|
|
21
|
+
"ol{margin:18px 0 0;padding-left:20px;line-height:1.8}code{background:#f2f6fb;border-radius:8px;padding:2px 6px}button{margin-top:20px;border:0;border-radius:10px;padding:10px 14px;background:#112033;color:#fff;cursor:pointer}",
|
|
22
|
+
"</style></head><body><main class=\"card\"><h1>", title, "</h1><p class=\"muted\">", subtitle, "</p>", portBadge, "<ol>", items, "</ol><button onclick=\"location.reload()\">Refresh</button></main></body></html>",
|
|
23
|
+
].join("");
|
|
24
|
+
}
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Lazycat Golang Todo Template</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root {
|
|
9
|
+
--bg-1: #f8fbff;
|
|
10
|
+
--bg-2: #ecf6ff;
|
|
11
|
+
--card: #ffffff;
|
|
12
|
+
--text: #142033;
|
|
13
|
+
--muted: #4e617b;
|
|
14
|
+
--line: #d8e4f2;
|
|
15
|
+
--brand: #0076d6;
|
|
16
|
+
--brand-2: #00a5a5;
|
|
17
|
+
}
|
|
18
|
+
* { box-sizing: border-box; }
|
|
19
|
+
body {
|
|
20
|
+
margin: 0;
|
|
21
|
+
min-height: 100vh;
|
|
22
|
+
display: grid;
|
|
23
|
+
place-items: center;
|
|
24
|
+
padding: 24px;
|
|
25
|
+
font-family: 'Segoe UI', 'Helvetica Neue', Arial, sans-serif;
|
|
26
|
+
background: radial-gradient(circle at top right, var(--bg-2), var(--bg-1) 42%);
|
|
27
|
+
color: var(--text);
|
|
28
|
+
}
|
|
29
|
+
.card {
|
|
30
|
+
width: min(920px, 100%);
|
|
31
|
+
background: var(--card);
|
|
32
|
+
border: 1px solid var(--line);
|
|
33
|
+
border-radius: 22px;
|
|
34
|
+
padding: 26px;
|
|
35
|
+
box-shadow: 0 18px 48px rgba(20, 32, 51, 0.08);
|
|
36
|
+
}
|
|
37
|
+
.tag {
|
|
38
|
+
margin: 0;
|
|
39
|
+
color: var(--brand);
|
|
40
|
+
font-weight: 700;
|
|
41
|
+
letter-spacing: .08em;
|
|
42
|
+
text-transform: uppercase;
|
|
43
|
+
font-size: .85rem;
|
|
44
|
+
}
|
|
45
|
+
h1 { margin: 10px 0 12px; font-size: clamp(1.8rem, 3.2vw, 2.7rem); }
|
|
46
|
+
p { margin: 0; color: var(--muted); }
|
|
47
|
+
.btns {
|
|
48
|
+
margin-top: 18px;
|
|
49
|
+
display: flex;
|
|
50
|
+
gap: 10px;
|
|
51
|
+
flex-wrap: wrap;
|
|
52
|
+
}
|
|
53
|
+
a {
|
|
54
|
+
display: inline-flex;
|
|
55
|
+
align-items: center;
|
|
56
|
+
justify-content: center;
|
|
57
|
+
text-decoration: none;
|
|
58
|
+
border-radius: 10px;
|
|
59
|
+
padding: 10px 14px;
|
|
60
|
+
border: 1px solid var(--line);
|
|
61
|
+
color: var(--text);
|
|
62
|
+
font-weight: 600;
|
|
63
|
+
}
|
|
64
|
+
a.primary {
|
|
65
|
+
color: #fff;
|
|
66
|
+
border-color: transparent;
|
|
67
|
+
background: linear-gradient(135deg, var(--brand), var(--brand-2));
|
|
68
|
+
}
|
|
69
|
+
.panel {
|
|
70
|
+
margin-top: 22px;
|
|
71
|
+
border: 1px solid var(--line);
|
|
72
|
+
border-radius: 14px;
|
|
73
|
+
padding: 14px;
|
|
74
|
+
background: #fbfdff;
|
|
75
|
+
}
|
|
76
|
+
.sync-note {
|
|
77
|
+
margin-top: 8px;
|
|
78
|
+
font-size: .92rem;
|
|
79
|
+
}
|
|
80
|
+
.form {
|
|
81
|
+
margin-top: 12px;
|
|
82
|
+
display: flex;
|
|
83
|
+
gap: 10px;
|
|
84
|
+
}
|
|
85
|
+
.form input {
|
|
86
|
+
flex: 1;
|
|
87
|
+
border: 1px solid var(--line);
|
|
88
|
+
border-radius: 10px;
|
|
89
|
+
padding: 10px 12px;
|
|
90
|
+
min-width: 0;
|
|
91
|
+
}
|
|
92
|
+
button {
|
|
93
|
+
border: 1px solid var(--line);
|
|
94
|
+
border-radius: 10px;
|
|
95
|
+
padding: 9px 12px;
|
|
96
|
+
cursor: pointer;
|
|
97
|
+
background: #fff;
|
|
98
|
+
}
|
|
99
|
+
ul {
|
|
100
|
+
margin: 12px 0 0;
|
|
101
|
+
padding: 0;
|
|
102
|
+
list-style: none;
|
|
103
|
+
display: grid;
|
|
104
|
+
gap: 10px;
|
|
105
|
+
}
|
|
106
|
+
li {
|
|
107
|
+
border: 1px solid var(--line);
|
|
108
|
+
border-radius: 10px;
|
|
109
|
+
padding: 10px 12px;
|
|
110
|
+
display: flex;
|
|
111
|
+
justify-content: space-between;
|
|
112
|
+
align-items: center;
|
|
113
|
+
gap: 8px;
|
|
114
|
+
background: #fff;
|
|
115
|
+
}
|
|
116
|
+
label {
|
|
117
|
+
display: flex;
|
|
118
|
+
align-items: center;
|
|
119
|
+
gap: 8px;
|
|
120
|
+
}
|
|
121
|
+
.done {
|
|
122
|
+
text-decoration: line-through;
|
|
123
|
+
color: var(--muted);
|
|
124
|
+
}
|
|
125
|
+
.danger {
|
|
126
|
+
color: #a63e37;
|
|
127
|
+
}
|
|
128
|
+
@media (max-width: 640px) {
|
|
129
|
+
.form {
|
|
130
|
+
flex-direction: column;
|
|
131
|
+
}
|
|
132
|
+
li {
|
|
133
|
+
flex-direction: column;
|
|
134
|
+
align-items: flex-start;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
</style>
|
|
138
|
+
</head>
|
|
139
|
+
<body>
|
|
140
|
+
<main class="card">
|
|
141
|
+
<p class="tag">Golang Version</p>
|
|
142
|
+
<h1>Todo demo powered by Go API</h1>
|
|
143
|
+
<p>
|
|
144
|
+
This template includes a Golang backend and a simple Todo app to help you start fast.
|
|
145
|
+
</p>
|
|
146
|
+
|
|
147
|
+
<div class="btns">
|
|
148
|
+
<a class="primary" href="https://lazycat.cloud/" target="_blank" rel="noreferrer">Visit lazycat.cloud</a>
|
|
149
|
+
<a href="https://developer.lazycat.cloud/" target="_blank" rel="noreferrer">Open Developer Docs</a>
|
|
150
|
+
</div>
|
|
151
|
+
|
|
152
|
+
<section class="panel">
|
|
153
|
+
<strong>Todo quick demo</strong>
|
|
154
|
+
<p class="sync-note">Data syncs automatically across devices when opened with the same app account.</p>
|
|
155
|
+
<div class="form">
|
|
156
|
+
<input id="todoInput" placeholder="Add a task for your app kickoff" />
|
|
157
|
+
<button id="addBtn" type="button">Add</button>
|
|
158
|
+
</div>
|
|
159
|
+
<ul id="todoList"></ul>
|
|
160
|
+
</section>
|
|
161
|
+
</main>
|
|
162
|
+
<script>
|
|
163
|
+
const todoInput = document.getElementById('todoInput');
|
|
164
|
+
const addBtn = document.getElementById('addBtn');
|
|
165
|
+
const todoList = document.getElementById('todoList');
|
|
166
|
+
|
|
167
|
+
async function fetchTodos() {
|
|
168
|
+
const resp = await fetch('/api/todos');
|
|
169
|
+
const data = await resp.json();
|
|
170
|
+
return Array.isArray(data.items) ? data.items : [];
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function renderTodos(items) {
|
|
174
|
+
todoList.innerHTML = '';
|
|
175
|
+
for (const todo of items) {
|
|
176
|
+
const li = document.createElement('li');
|
|
177
|
+
|
|
178
|
+
const label = document.createElement('label');
|
|
179
|
+
const checkbox = document.createElement('input');
|
|
180
|
+
checkbox.type = 'checkbox';
|
|
181
|
+
checkbox.checked = !!todo.done;
|
|
182
|
+
checkbox.addEventListener('change', async () => {
|
|
183
|
+
await fetch(`/api/todos/${todo.id}/toggle`, { method: 'PUT' });
|
|
184
|
+
await refreshTodos();
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
const span = document.createElement('span');
|
|
188
|
+
span.textContent = todo.title;
|
|
189
|
+
if (todo.done) {
|
|
190
|
+
span.className = 'done';
|
|
191
|
+
}
|
|
192
|
+
label.append(checkbox, span);
|
|
193
|
+
|
|
194
|
+
const delBtn = document.createElement('button');
|
|
195
|
+
delBtn.type = 'button';
|
|
196
|
+
delBtn.className = 'danger';
|
|
197
|
+
delBtn.textContent = 'Delete';
|
|
198
|
+
delBtn.addEventListener('click', async () => {
|
|
199
|
+
await fetch(`/api/todos/${todo.id}`, { method: 'DELETE' });
|
|
200
|
+
await refreshTodos();
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
li.append(label, delBtn);
|
|
204
|
+
todoList.appendChild(li);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async function refreshTodos() {
|
|
209
|
+
const items = await fetchTodos();
|
|
210
|
+
renderTodos(items);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async function addTodo() {
|
|
214
|
+
const title = todoInput.value.trim();
|
|
215
|
+
if (!title) return;
|
|
216
|
+
await fetch('/api/todos', {
|
|
217
|
+
method: 'POST',
|
|
218
|
+
headers: { 'Content-Type': 'application/json' },
|
|
219
|
+
body: JSON.stringify({ title }),
|
|
220
|
+
});
|
|
221
|
+
todoInput.value = '';
|
|
222
|
+
await refreshTodos();
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
addBtn.addEventListener('click', addTodo);
|
|
226
|
+
todoInput.addEventListener('keyup', async (event) => {
|
|
227
|
+
if (event.key === 'Enter') {
|
|
228
|
+
await addTodo();
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
refreshTodos();
|
|
233
|
+
setInterval(refreshTodos, 2500);
|
|
234
|
+
document.addEventListener('visibilitychange', refreshTodos);
|
|
235
|
+
window.addEventListener('focus', refreshTodos);
|
|
236
|
+
</script>
|
|
237
|
+
</body>
|
|
238
|
+
</html>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Lazycat GUI VNC App
|
|
2
|
+
|
|
3
|
+
## First Deploy
|
|
4
|
+
```bash
|
|
5
|
+
lzc-cli project deploy
|
|
6
|
+
lzc-cli project info
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
By default, project commands use `lzc-build.dev.yml` when it exists.
|
|
10
|
+
Each command prints the active `Build config`.
|
|
11
|
+
Use `--release` if you want to operate on `lzc-build.yml`.
|
|
12
|
+
|
|
13
|
+
Then open the app from launcher or browser.
|
|
14
|
+
|
|
15
|
+
## Build LPK
|
|
16
|
+
```bash
|
|
17
|
+
lzc-cli project release -o gui-vnc.lpk
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
```bash
|
|
22
|
+
lzc-cli lpk install gui-vnc.lpk
|
|
23
|
+
```
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
FROM registry.lazycat.cloud/kasm-debian-bookworm:0.0.1
|
|
2
|
+
|
|
3
|
+
USER root
|
|
4
|
+
|
|
5
|
+
RUN usermod -l lazycat kasm-user \
|
|
6
|
+
&& echo 'lazycat ALL=(ALL) NOPASSWD: ALL' >> /etc/sudoers \
|
|
7
|
+
&& sed -i 's/kasm_user/lazycat/g' /dockerstartup/vnc_startup.sh \
|
|
8
|
+
&& sed -i '5i sudo chown -R lazycat:kasm-user /home/lazycat/' /dockerstartup/kasm_default_profile.sh
|
|
9
|
+
|
|
10
|
+
RUN apt-get update \
|
|
11
|
+
&& apt-get install -y --no-install-recommends x11-apps \
|
|
12
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
13
|
+
|
|
14
|
+
RUN chown -R lazycat:kasm-user /home/kasm-default-profile \
|
|
15
|
+
&& chmod -R u+rwX,g+rX /home/kasm-default-profile
|
|
16
|
+
|
|
17
|
+
ENV HOME=/home/lazycat
|
|
18
|
+
WORKDIR /home/lazycat
|
|
19
|
+
|
|
20
|
+
COPY --chown=lazycat:kasm-user kasmvnc.yaml /home/lazycat/.vnc/kasmvnc.yaml
|
|
21
|
+
COPY --chown=lazycat:kasm-user startup-script.sh /home/lazycat/.config/autostart/startup-script.sh
|
|
22
|
+
# Uncomment to enable autostart link creation for /lzcapp/documents.
|
|
23
|
+
# COPY --chown=lazycat:kasm-user startup-script.desktop /home/lazycat/.config/autostart/startup-script.desktop
|
|
24
|
+
|
|
25
|
+
RUN chmod +x /home/lazycat/.config/autostart/startup-script.sh
|
|
26
|
+
|
|
27
|
+
ENV VNCOPTIONS="-PreferBandwidth -disableBasicAuth -DynamicQualityMin=4 -DynamicQualityMax=7 -DLP_ClipDelay=0 -sslOnly=0"
|
|
28
|
+
ENV VNC_PW=lazycat
|
|
29
|
+
|
|
30
|
+
USER lazycat
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
network:
|
|
2
|
+
ssl:
|
|
3
|
+
pem_certificate: ${HOME}/.vnc/self.pem
|
|
4
|
+
pem_key: ${HOME}/.vnc/self.pem
|
|
5
|
+
require_ssl: false
|
|
6
|
+
udp:
|
|
7
|
+
public_ip: 127.0.0.1
|
|
8
|
+
|
|
9
|
+
runtime_configuration:
|
|
10
|
+
allow_override_standard_vnc_server_settings: false
|
|
11
|
+
allow_client_to_override_kasm_server_settings: false
|
|
12
|
+
allow_override_list: []
|
|
13
|
+
|
|
14
|
+
encoding:
|
|
15
|
+
max_frame_rate: 60
|
|
16
|
+
full_frame_updates: 60
|
|
17
|
+
|
|
18
|
+
video_encoding_mode:
|
|
19
|
+
jpeg_quality: -1
|
|
20
|
+
webp_quality: -1
|
|
21
|
+
max_resolution:
|
|
22
|
+
width: 1920
|
|
23
|
+
height: 1080
|
|
24
|
+
scaling_algorithm: progressive_bilinear
|
|
25
|
+
|
|
26
|
+
compare_framebuffer: off
|
|
27
|
+
zrle_zlib_level: 0
|
|
28
|
+
hextile_improved_compression: false
|
|
29
|
+
|
|
30
|
+
desktop:
|
|
31
|
+
gpu:
|
|
32
|
+
hw3d: false
|
|
33
|
+
drinode: /dev/dri/renderD128
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# manifest: 指定 lpk 的 manifest 文件
|
|
2
|
+
manifest: ./lzc-manifest.yml
|
|
3
|
+
|
|
4
|
+
# pkgout: lpk 输出目录
|
|
5
|
+
pkgout: ./
|
|
6
|
+
|
|
7
|
+
# icon: 应用图标(png)
|
|
8
|
+
icon: ./lzc-icon.png
|
|
9
|
+
|
|
10
|
+
# images: 构建容器镜像并写入 lpk v2 的 images 目录
|
|
11
|
+
images:
|
|
12
|
+
app-runtime:
|
|
13
|
+
# context: Docker 构建上下文目录
|
|
14
|
+
context: ./images
|
|
15
|
+
# dockerfile: 镜像构建文件
|
|
16
|
+
dockerfile: ./images/Dockerfile
|
|
17
|
+
# upstream-match: 上游镜像前缀,用于层拆分(upstream/embed)
|
|
18
|
+
upstream-match: registry.lazycat.cloud
|
|
Binary file
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
FROM registry.lazycat.cloud/lzc/lzcapp:3.20.3
|
|
2
|
+
|
|
3
|
+
RUN apk add --no-cache python3 py3-pip
|
|
4
|
+
WORKDIR /app
|
|
5
|
+
|
|
6
|
+
COPY requirements.txt /app/requirements.txt
|
|
7
|
+
RUN pip3 install --no-cache-dir -r /app/requirements.txt --break-system-packages
|
|
8
|
+
|
|
9
|
+
COPY app.py /app/app.py
|
|
10
|
+
COPY run.sh /app/run.sh
|
|
11
|
+
RUN chmod +x /app/run.sh
|
|
12
|
+
COPY web /app/web
|
|
13
|
+
|
|
14
|
+
EXPOSE 3000
|
|
15
|
+
CMD ["/app/run.sh"]
|