@lazycatcloud/lzc-cli 1.3.13 → 2.0.0-pre.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.
- package/README.md +30 -5
- package/changelog.md +16 -0
- package/lib/app/index.js +174 -58
- package/lib/app/lpk_build.js +197 -18
- package/lib/app/lpk_build_images.js +728 -0
- package/lib/app/lpk_create.js +96 -23
- package/lib/app/lpk_create_generator.js +150 -12
- package/lib/app/lpk_devshell.js +35 -21
- package/lib/app/lpk_embed_images.js +257 -0
- package/lib/app/lpk_installer.js +15 -7
- package/lib/app/project_cp.js +64 -0
- package/lib/app/project_deploy.js +33 -0
- package/lib/app/project_exec.js +45 -0
- package/lib/app/project_info.js +106 -0
- package/lib/app/project_log.js +67 -0
- package/lib/app/project_runtime.js +261 -0
- package/lib/app/project_start.js +100 -0
- package/lib/appstore/index.js +56 -16
- package/lib/appstore/publish.js +16 -13
- package/lib/box/index.js +103 -6
- package/lib/box/ssh_remote.js +259 -0
- package/lib/build_remote.js +22 -0
- package/lib/config/index.js +4 -3
- package/lib/debug_bridge.js +837 -44
- package/lib/docker/index.js +30 -10
- package/lib/i18n/index.js +1 -0
- package/lib/i18n/locales/en/translation.json +263 -250
- package/lib/i18n/locales/zh/translation.json +57 -44
- package/lib/lpk/core.js +487 -0
- package/lib/lpk/index.js +210 -0
- package/lib/shellapi.js +5 -5
- package/lib/sig/core.js +254 -0
- package/lib/sig/index.js +88 -0
- package/lib/utils.js +17 -12
- package/package.json +4 -3
- package/scripts/cli.js +4 -0
- package/template/_lpk/README.md +11 -3
- package/template/_lpk/gui-vnc.manifest.yml.in +27 -0
- package/template/_lpk/manifest.yml.in +4 -2
- package/template/_lpk/todolist-golang.manifest.yml.in +16 -0
- package/template/_lpk/todolist-java.manifest.yml.in +15 -0
- package/template/_lpk/todolist-python.manifest.yml.in +15 -0
- package/template/_lpk/vue.lzc-build.yml.in +0 -44
- package/template/blank/_gitignore +1 -0
- package/template/blank/lzc-build.yml +25 -40
- package/template/blank/lzc-manifest.yml +14 -7
- package/template/golang/Dockerfile +19 -0
- package/template/golang/README.md +33 -0
- package/template/golang/_gitignore +3 -0
- package/template/golang/go.mod +3 -0
- package/template/golang/lzc-build.yml +21 -0
- package/template/golang/lzc-icon.png +0 -0
- package/template/golang/main.go +252 -0
- package/template/golang/run.sh +3 -0
- package/template/golang/web/index.html +238 -0
- package/template/gui-vnc/README.md +19 -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.yml +23 -0
- package/template/gui-vnc/lzc-icon.png +0 -0
- package/template/python/Dockerfile +15 -0
- package/template/python/README.md +33 -0
- package/template/python/_gitignore +3 -0
- package/template/python/app.py +110 -0
- package/template/python/lzc-build.yml +21 -0
- package/template/python/lzc-icon.png +0 -0
- package/template/python/requirements.txt +1 -0
- package/template/python/run.sh +3 -0
- package/template/python/web/index.html +238 -0
- package/template/springboot/Dockerfile +20 -0
- package/template/springboot/README.md +33 -0
- package/template/springboot/_gitignore +3 -0
- package/template/springboot/lzc-build.yml +21 -0
- package/template/springboot/lzc-icon.png +0 -0
- package/template/springboot/pom.xml +38 -0
- package/template/springboot/run.sh +3 -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 +17 -7
- package/template/vue/_gitignore +1 -0
- package/template/vue/lzc-build.yml +31 -42
- package/template/vue/src/App.vue +36 -25
- package/template/vue/src/style.css +106 -49
- package/template/vue-minidb/README.md +34 -0
- package/template/vue-minidb/_gitignore +26 -0
- package/template/vue-minidb/index.html +13 -0
- package/template/vue-minidb/lzc-build.yml +48 -0
- package/template/vue-minidb/lzc-icon.png +0 -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,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,19 @@
|
|
|
1
|
+
# Lazycat GUI VNC App
|
|
2
|
+
|
|
3
|
+
## First Deploy
|
|
4
|
+
```bash
|
|
5
|
+
lzc-cli project deploy
|
|
6
|
+
lzc-cli project info
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
Then open the app from launcher or browser.
|
|
10
|
+
|
|
11
|
+
## Build LPK
|
|
12
|
+
```bash
|
|
13
|
+
lzc-cli project release -f lzc-build.yml -o gui-vnc.lpk
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
```bash
|
|
18
|
+
lzc-cli lpk install gui-vnc.lpk
|
|
19
|
+
```
|
|
@@ -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,23 @@
|
|
|
1
|
+
# 可选基础文件: lzc-build.base.yml(与当前文件同目录,先加载再与当前文件合并)
|
|
2
|
+
|
|
3
|
+
# manifest: 指定 lpk 的 manifest 文件
|
|
4
|
+
manifest: ./lzc-manifest.yml
|
|
5
|
+
|
|
6
|
+
# contentdir: 需要打包进 lpk 的目录
|
|
7
|
+
contentdir: ./
|
|
8
|
+
|
|
9
|
+
# pkgout: lpk 输出目录
|
|
10
|
+
pkgout: ./
|
|
11
|
+
|
|
12
|
+
# icon: 应用图标(png)
|
|
13
|
+
icon: ./lzc-icon.png
|
|
14
|
+
|
|
15
|
+
# images: 构建容器镜像并写入 lpk v2 的 images 目录
|
|
16
|
+
images:
|
|
17
|
+
app-runtime:
|
|
18
|
+
# context: Docker 构建上下文目录
|
|
19
|
+
context: ./images
|
|
20
|
+
# dockerfile: 镜像构建文件
|
|
21
|
+
dockerfile: ./images/Dockerfile
|
|
22
|
+
# upstream-match: 上游镜像前缀,用于层拆分(upstream/embed)
|
|
23
|
+
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 /lzcapp/pkg/content/web
|
|
13
|
+
|
|
14
|
+
EXPOSE 3000
|
|
15
|
+
CMD ["python3", "/app/app.py"]
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Lazycat Python Todo App
|
|
2
|
+
|
|
3
|
+
## First Deploy
|
|
4
|
+
```bash
|
|
5
|
+
lzc-cli project deploy
|
|
6
|
+
lzc-cli project info
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
Then open the app from launcher or browser.
|
|
10
|
+
|
|
11
|
+
## API
|
|
12
|
+
```text
|
|
13
|
+
GET /api/health
|
|
14
|
+
GET /api/todos
|
|
15
|
+
POST /api/todos
|
|
16
|
+
PUT /api/todos/{id}/toggle
|
|
17
|
+
DELETE /api/todos/{id}
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Data Path
|
|
21
|
+
```text
|
|
22
|
+
/lzcapp/var/todos.json
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Build LPK
|
|
26
|
+
```bash
|
|
27
|
+
lzc-cli project build -o python-app.lpk
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Install
|
|
31
|
+
```bash
|
|
32
|
+
lzc-cli lpk install python-app.lpk
|
|
33
|
+
```
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
import time
|
|
4
|
+
from flask import Flask, send_from_directory, request
|
|
5
|
+
|
|
6
|
+
app = Flask(__name__, static_folder="/lzcapp/pkg/content/web", static_url_path="")
|
|
7
|
+
DATA_FILE = "/lzcapp/var/todos.json"
|
|
8
|
+
todos = []
|
|
9
|
+
next_id = 1
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def now_ms():
|
|
13
|
+
return int(time.time() * 1000)
|
|
14
|
+
|
|
15
|
+
def load_todos():
|
|
16
|
+
global todos, next_id
|
|
17
|
+
os.makedirs(os.path.dirname(DATA_FILE), exist_ok=True)
|
|
18
|
+
if not os.path.exists(DATA_FILE):
|
|
19
|
+
return
|
|
20
|
+
with open(DATA_FILE, "r", encoding="utf-8") as fp:
|
|
21
|
+
raw = fp.read().strip()
|
|
22
|
+
if not raw:
|
|
23
|
+
return
|
|
24
|
+
loaded = json.loads(raw)
|
|
25
|
+
if not isinstance(loaded, list):
|
|
26
|
+
raise ValueError("todos data must be a list")
|
|
27
|
+
todos = loaded
|
|
28
|
+
max_id = 0
|
|
29
|
+
for item in todos:
|
|
30
|
+
if isinstance(item, dict):
|
|
31
|
+
max_id = max(max_id, int(item.get("id", 0)))
|
|
32
|
+
next_id = max_id + 1
|
|
33
|
+
|
|
34
|
+
def save_todos():
|
|
35
|
+
os.makedirs(os.path.dirname(DATA_FILE), exist_ok=True)
|
|
36
|
+
tmp_file = DATA_FILE + ".tmp"
|
|
37
|
+
with open(tmp_file, "w", encoding="utf-8") as fp:
|
|
38
|
+
json.dump(todos, fp, ensure_ascii=False, indent=2)
|
|
39
|
+
os.replace(tmp_file, DATA_FILE)
|
|
40
|
+
|
|
41
|
+
load_todos()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@app.get("/")
|
|
45
|
+
def index():
|
|
46
|
+
return send_from_directory(app.static_folder, "index.html")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@app.get('/api/health')
|
|
50
|
+
def health():
|
|
51
|
+
return {'status': 'ok'}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@app.get("/api/todos")
|
|
55
|
+
def list_todos():
|
|
56
|
+
sorted_todos = sorted(todos, key=lambda item: item["updatedAt"], reverse=True)
|
|
57
|
+
return {"items": sorted_todos}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@app.post("/api/todos")
|
|
61
|
+
def add_todo():
|
|
62
|
+
global next_id
|
|
63
|
+
payload = request.get_json(silent=True) or {}
|
|
64
|
+
title = str(payload.get("title", "")).strip()
|
|
65
|
+
if not title:
|
|
66
|
+
return {"error": "title is required"}, 400
|
|
67
|
+
todo = {
|
|
68
|
+
"id": next_id,
|
|
69
|
+
"title": title,
|
|
70
|
+
"done": False,
|
|
71
|
+
"updatedAt": now_ms(),
|
|
72
|
+
}
|
|
73
|
+
next_id += 1
|
|
74
|
+
todos.append(todo)
|
|
75
|
+
try:
|
|
76
|
+
save_todos()
|
|
77
|
+
except Exception:
|
|
78
|
+
return {"error": "failed to persist todos"}, 500
|
|
79
|
+
return todo
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@app.put("/api/todos/<int:todo_id>/toggle")
|
|
83
|
+
def toggle_todo(todo_id):
|
|
84
|
+
for todo in todos:
|
|
85
|
+
if todo["id"] == todo_id:
|
|
86
|
+
todo["done"] = not todo["done"]
|
|
87
|
+
todo["updatedAt"] = now_ms()
|
|
88
|
+
try:
|
|
89
|
+
save_todos()
|
|
90
|
+
except Exception:
|
|
91
|
+
return {"error": "failed to persist todos"}, 500
|
|
92
|
+
return todo
|
|
93
|
+
return {"error": "todo not found"}, 404
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@app.delete("/api/todos/<int:todo_id>")
|
|
97
|
+
def delete_todo(todo_id):
|
|
98
|
+
for index, todo in enumerate(todos):
|
|
99
|
+
if todo["id"] == todo_id:
|
|
100
|
+
todos.pop(index)
|
|
101
|
+
try:
|
|
102
|
+
save_todos()
|
|
103
|
+
except Exception:
|
|
104
|
+
return {"error": "failed to persist todos"}, 500
|
|
105
|
+
return {"ok": True}
|
|
106
|
+
return {"error": "todo not found"}, 404
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
if __name__ == '__main__':
|
|
110
|
+
app.run(host='0.0.0.0', port=3000)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# 可选基础文件: lzc-build.base.yml(与当前文件同目录,先加载再与当前文件合并)
|
|
2
|
+
|
|
3
|
+
# manifest: 指定 lpk 的 manifest 文件
|
|
4
|
+
manifest: ./lzc-manifest.yml
|
|
5
|
+
|
|
6
|
+
# contentdir: 需要打包进 lpk 的目录
|
|
7
|
+
contentdir: ./
|
|
8
|
+
|
|
9
|
+
# pkgout: lpk 输出目录
|
|
10
|
+
pkgout: ./
|
|
11
|
+
|
|
12
|
+
# icon: 应用图标(png)
|
|
13
|
+
icon: ./lzc-icon.png
|
|
14
|
+
|
|
15
|
+
# images: 构建容器镜像并写入 lpk v2 的 images 目录
|
|
16
|
+
images:
|
|
17
|
+
app-runtime:
|
|
18
|
+
# context: Docker 构建上下文目录
|
|
19
|
+
context: ./
|
|
20
|
+
# dockerfile: 镜像构建文件
|
|
21
|
+
dockerfile: ./Dockerfile
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
flask==3.0.3
|