@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.
Files changed (128) hide show
  1. package/README.md +47 -7
  2. package/changelog.md +14 -0
  3. package/lib/app/apkshell.js +7 -44
  4. package/lib/app/index.js +178 -64
  5. package/lib/app/lpk_build.js +446 -61
  6. package/lib/app/lpk_build_images.js +749 -0
  7. package/lib/app/lpk_create.js +192 -45
  8. package/lib/app/lpk_create_generator.js +141 -13
  9. package/lib/app/lpk_devshell.js +33 -19
  10. package/lib/app/lpk_embed_images.js +257 -0
  11. package/lib/app/lpk_installer.js +17 -9
  12. package/lib/app/manifest_build.js +259 -0
  13. package/lib/app/project_cp.js +59 -0
  14. package/lib/app/project_deploy.js +58 -0
  15. package/lib/app/project_exec.js +82 -0
  16. package/lib/app/project_info.js +106 -0
  17. package/lib/app/project_log.js +62 -0
  18. package/lib/app/project_runtime.js +356 -0
  19. package/lib/app/project_start.js +95 -0
  20. package/lib/app/project_sync.js +499 -0
  21. package/lib/appstore/apkshell.js +50 -0
  22. package/lib/box/index.js +101 -4
  23. package/lib/box/ssh_remote.js +259 -0
  24. package/lib/build_remote.js +21 -0
  25. package/lib/debug_bridge.js +891 -83
  26. package/lib/docker/index.js +30 -10
  27. package/lib/i18n/locales/en/translation.json +262 -255
  28. package/lib/i18n/locales/zh/translation.json +262 -255
  29. package/lib/lpk/core.js +488 -0
  30. package/lib/lpk/index.js +210 -0
  31. package/lib/migrate/index.js +52 -0
  32. package/lib/package_info.js +135 -0
  33. package/lib/shellapi.js +35 -1
  34. package/lib/sig/core.js +254 -0
  35. package/lib/sig/index.js +88 -0
  36. package/lib/utils.js +94 -15
  37. package/package.json +3 -3
  38. package/scripts/cli.js +6 -0
  39. package/scripts/smoke/frontend-dev-entry.mjs +104 -0
  40. package/scripts/smoke/template-project.mjs +311 -0
  41. package/template/_lpk/README.md +15 -4
  42. package/template/_lpk/gui-vnc.manifest.yml.in +18 -0
  43. package/template/_lpk/hello-vue.manifest.yml.in +38 -0
  44. package/template/_lpk/manifest.yml.in +4 -11
  45. package/template/_lpk/package.yml.in +7 -0
  46. package/template/_lpk/todolist-golang.manifest.yml.in +30 -0
  47. package/template/_lpk/todolist-java.manifest.yml.in +29 -0
  48. package/template/_lpk/todolist-python.manifest.yml.in +37 -0
  49. package/template/_lpk/todolist-serverless.manifest.yml.in +38 -0
  50. package/template/_lpk/vue.lzc-build.yml.in +0 -44
  51. package/template/blank/lzc-build.dev.yml +4 -0
  52. package/template/blank/lzc-build.yml +24 -41
  53. package/template/blank/lzc-manifest.yml +7 -9
  54. package/template/blank/package.yml +7 -0
  55. package/template/golang/Dockerfile +19 -0
  56. package/template/golang/Dockerfile.dev +20 -0
  57. package/template/golang/README.md +44 -0
  58. package/template/golang/_gitignore +3 -0
  59. package/template/golang/_lzcdevignore +21 -0
  60. package/template/golang/go.mod +3 -0
  61. package/template/golang/lzc-build.dev.yml +12 -0
  62. package/template/golang/lzc-build.yml +16 -0
  63. package/template/golang/lzc-icon.png +0 -0
  64. package/template/golang/main.go +252 -0
  65. package/template/golang/manifest.dev.page.js +24 -0
  66. package/template/golang/run.sh +10 -0
  67. package/template/golang/web/index.html +238 -0
  68. package/template/gui-vnc/README.md +23 -0
  69. package/template/gui-vnc/_gitignore +2 -0
  70. package/template/gui-vnc/images/Dockerfile +30 -0
  71. package/template/gui-vnc/images/kasmvnc.yaml +33 -0
  72. package/template/gui-vnc/images/startup-script.desktop +9 -0
  73. package/template/gui-vnc/images/startup-script.sh +6 -0
  74. package/template/gui-vnc/lzc-build.dev.yml +4 -0
  75. package/template/gui-vnc/lzc-build.yml +18 -0
  76. package/template/gui-vnc/lzc-icon.png +0 -0
  77. package/template/python/Dockerfile +15 -0
  78. package/template/python/Dockerfile.dev +18 -0
  79. package/template/python/README.md +50 -0
  80. package/template/python/_gitignore +3 -0
  81. package/template/python/_lzcdevignore +21 -0
  82. package/template/python/app.py +110 -0
  83. package/template/python/lzc-build.dev.yml +12 -0
  84. package/template/python/lzc-build.yml +16 -0
  85. package/template/python/lzc-icon.png +0 -0
  86. package/template/python/manifest.dev.page.js +25 -0
  87. package/template/python/requirements.txt +1 -0
  88. package/template/python/run.sh +14 -0
  89. package/template/python/web/index.html +238 -0
  90. package/template/springboot/Dockerfile +20 -0
  91. package/template/springboot/Dockerfile.dev +20 -0
  92. package/template/springboot/README.md +44 -0
  93. package/template/springboot/_gitignore +3 -0
  94. package/template/springboot/_lzcdevignore +21 -0
  95. package/template/springboot/lzc-build.dev.yml +12 -0
  96. package/template/springboot/lzc-build.yml +16 -0
  97. package/template/springboot/lzc-icon.png +0 -0
  98. package/template/springboot/manifest.dev.page.js +24 -0
  99. package/template/springboot/pom.xml +38 -0
  100. package/template/springboot/run.sh +10 -0
  101. package/template/springboot/src/main/java/cloud/lazycat/app/Application.java +132 -0
  102. package/template/springboot/src/main/resources/application.properties +1 -0
  103. package/template/springboot/src/main/resources/static/index.html +238 -0
  104. package/template/vue/README.md +18 -21
  105. package/template/vue/lzc-build.dev.yml +7 -0
  106. package/template/vue/lzc-build.yml +30 -43
  107. package/template/vue/manifest.dev.page.js +50 -0
  108. package/template/vue/src/App.vue +36 -25
  109. package/template/vue/src/style.css +106 -49
  110. package/template/vue-minidb/README.md +26 -0
  111. package/template/vue-minidb/_gitignore +25 -0
  112. package/template/vue-minidb/index.html +13 -0
  113. package/template/vue-minidb/lzc-build.dev.yml +7 -0
  114. package/template/vue-minidb/lzc-build.yml +46 -0
  115. package/template/vue-minidb/lzc-icon.png +0 -0
  116. package/template/vue-minidb/manifest.dev.page.js +50 -0
  117. package/template/vue-minidb/package.json +21 -0
  118. package/template/vue-minidb/public/vite.svg +1 -0
  119. package/template/vue-minidb/src/App.vue +206 -0
  120. package/template/vue-minidb/src/assets/vue.svg +1 -0
  121. package/template/vue-minidb/src/main.ts +5 -0
  122. package/template/vue-minidb/src/style.css +136 -0
  123. package/template/vue-minidb/src/vite-env.d.ts +1 -0
  124. package/template/vue-minidb/tsconfig.app.json +24 -0
  125. package/template/vue-minidb/tsconfig.json +7 -0
  126. package/template/vue-minidb/tsconfig.node.json +22 -0
  127. package/template/vue-minidb/vite.config.ts +10 -0
  128. /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,10 @@
1
+ #!/bin/sh
2
+ set -e
3
+
4
+ mirror_root="/lzcapp/cache/project-mirror"
5
+ if command -v go >/dev/null 2>&1 && [ -f "$mirror_root/main.go" ]; then
6
+ cd "$mirror_root"
7
+ exec go run ./main.go
8
+ fi
9
+
10
+ exec /app/app
@@ -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,2 @@
1
+ *.lpk
2
+ *.lpk.tar
@@ -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,9 @@
1
+ [Desktop Entry]
2
+ Type=Application
3
+ Version=1.0
4
+ Name=Lazycat Startup Links
5
+ Comment=Create desktop links for LPK runtime paths
6
+ Exec=/home/lazycat/.config/autostart/startup-script.sh
7
+ Terminal=false
8
+ X-GNOME-Autostart-enabled=true
9
+ NoDisplay=true
@@ -0,0 +1,6 @@
1
+ #!/bin/bash
2
+ set -e
3
+
4
+ mkdir -p /home/lazycat/Desktop
5
+
6
+ ln -svfn /lzcapp/documents "/home/lazycat/Desktop/Documents (ReadWrite)"
@@ -0,0 +1,4 @@
1
+ # 开发态覆盖配置
2
+
3
+ # pkg_id_suffix: append an extra package id segment for dev deploys
4
+ pkg_id_suffix: dev
@@ -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"]