@lazycatcloud/lzc-cli 1.3.14 → 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.
Files changed (101) hide show
  1. package/README.md +30 -5
  2. package/changelog.md +4 -0
  3. package/lib/app/index.js +174 -58
  4. package/lib/app/lpk_build.js +192 -17
  5. package/lib/app/lpk_build_images.js +728 -0
  6. package/lib/app/lpk_create.js +93 -21
  7. package/lib/app/lpk_create_generator.js +144 -9
  8. package/lib/app/lpk_devshell.js +33 -19
  9. package/lib/app/lpk_embed_images.js +257 -0
  10. package/lib/app/lpk_installer.js +14 -7
  11. package/lib/app/project_cp.js +64 -0
  12. package/lib/app/project_deploy.js +33 -0
  13. package/lib/app/project_exec.js +45 -0
  14. package/lib/app/project_info.js +106 -0
  15. package/lib/app/project_log.js +67 -0
  16. package/lib/app/project_runtime.js +261 -0
  17. package/lib/app/project_start.js +100 -0
  18. package/lib/box/index.js +101 -4
  19. package/lib/box/ssh_remote.js +259 -0
  20. package/lib/build_remote.js +22 -0
  21. package/lib/config/index.js +1 -1
  22. package/lib/debug_bridge.js +837 -46
  23. package/lib/docker/index.js +30 -10
  24. package/lib/i18n/index.js +1 -0
  25. package/lib/i18n/locales/en/translation.json +17 -5
  26. package/lib/i18n/locales/zh/translation.json +16 -4
  27. package/lib/lpk/core.js +487 -0
  28. package/lib/lpk/index.js +210 -0
  29. package/lib/sig/core.js +254 -0
  30. package/lib/sig/index.js +88 -0
  31. package/lib/utils.js +3 -1
  32. package/package.json +2 -1
  33. package/scripts/cli.js +4 -0
  34. package/template/_lpk/README.md +11 -3
  35. package/template/_lpk/gui-vnc.manifest.yml.in +27 -0
  36. package/template/_lpk/manifest.yml.in +4 -2
  37. package/template/_lpk/todolist-golang.manifest.yml.in +16 -0
  38. package/template/_lpk/todolist-java.manifest.yml.in +15 -0
  39. package/template/_lpk/todolist-python.manifest.yml.in +15 -0
  40. package/template/_lpk/vue.lzc-build.yml.in +0 -44
  41. package/template/blank/_gitignore +1 -0
  42. package/template/blank/lzc-build.yml +25 -40
  43. package/template/blank/lzc-manifest.yml +14 -7
  44. package/template/golang/Dockerfile +19 -0
  45. package/template/golang/README.md +33 -0
  46. package/template/golang/_gitignore +3 -0
  47. package/template/golang/go.mod +3 -0
  48. package/template/golang/lzc-build.yml +21 -0
  49. package/template/golang/lzc-icon.png +0 -0
  50. package/template/golang/main.go +252 -0
  51. package/template/golang/run.sh +3 -0
  52. package/template/golang/web/index.html +238 -0
  53. package/template/gui-vnc/README.md +19 -0
  54. package/template/gui-vnc/_gitignore +2 -0
  55. package/template/gui-vnc/images/Dockerfile +30 -0
  56. package/template/gui-vnc/images/kasmvnc.yaml +33 -0
  57. package/template/gui-vnc/images/startup-script.desktop +9 -0
  58. package/template/gui-vnc/images/startup-script.sh +6 -0
  59. package/template/gui-vnc/lzc-build.yml +23 -0
  60. package/template/gui-vnc/lzc-icon.png +0 -0
  61. package/template/python/Dockerfile +15 -0
  62. package/template/python/README.md +33 -0
  63. package/template/python/_gitignore +3 -0
  64. package/template/python/app.py +110 -0
  65. package/template/python/lzc-build.yml +21 -0
  66. package/template/python/lzc-icon.png +0 -0
  67. package/template/python/requirements.txt +1 -0
  68. package/template/python/run.sh +3 -0
  69. package/template/python/web/index.html +238 -0
  70. package/template/springboot/Dockerfile +20 -0
  71. package/template/springboot/README.md +33 -0
  72. package/template/springboot/_gitignore +3 -0
  73. package/template/springboot/lzc-build.yml +21 -0
  74. package/template/springboot/lzc-icon.png +0 -0
  75. package/template/springboot/pom.xml +38 -0
  76. package/template/springboot/run.sh +3 -0
  77. package/template/springboot/src/main/java/cloud/lazycat/app/Application.java +132 -0
  78. package/template/springboot/src/main/resources/application.properties +1 -0
  79. package/template/springboot/src/main/resources/static/index.html +238 -0
  80. package/template/vue/README.md +17 -7
  81. package/template/vue/_gitignore +1 -0
  82. package/template/vue/lzc-build.yml +31 -42
  83. package/template/vue/src/App.vue +36 -25
  84. package/template/vue/src/style.css +106 -49
  85. package/template/vue-minidb/README.md +34 -0
  86. package/template/vue-minidb/_gitignore +26 -0
  87. package/template/vue-minidb/index.html +13 -0
  88. package/template/vue-minidb/lzc-build.yml +48 -0
  89. package/template/vue-minidb/lzc-icon.png +0 -0
  90. package/template/vue-minidb/package.json +21 -0
  91. package/template/vue-minidb/public/vite.svg +1 -0
  92. package/template/vue-minidb/src/App.vue +206 -0
  93. package/template/vue-minidb/src/assets/vue.svg +1 -0
  94. package/template/vue-minidb/src/main.ts +5 -0
  95. package/template/vue-minidb/src/style.css +136 -0
  96. package/template/vue-minidb/src/vite-env.d.ts +1 -0
  97. package/template/vue-minidb/tsconfig.app.json +24 -0
  98. package/template/vue-minidb/tsconfig.json +7 -0
  99. package/template/vue-minidb/tsconfig.node.json +22 -0
  100. package/template/vue-minidb/vite.config.ts +10 -0
  101. /package/template/{vue → vue-minidb}/src/components/HelloWorld.vue +0 -0
@@ -0,0 +1,16 @@
1
+ name: ${name} # app名称
2
+ package: ${package} # app的唯一标识符
3
+ version: 0.0.1 # app的版本
4
+ description: # app描述
5
+
6
+ license: https://choosealicense.com/licenses/mit/
7
+ homepage: # 出现bug时候提交反馈的地方
8
+ author: # app author
9
+
10
+ application:
11
+ subdomain: ${subdomain} # 默认访问子域名前缀
12
+ image: embed:app-runtime # 引用 lzc-build.yml 里的 images.app-runtime
13
+ routes:
14
+ # exec:// 路由会执行 /app/run.sh,并把请求转发到 3000 端口
15
+ # 适合「一个容器同时承载前端+后端」的入门场景
16
+ - /=exec://3000,/app/run.sh
@@ -0,0 +1,15 @@
1
+ name: ${name} # app名称
2
+ package: ${package} # app的唯一标识符
3
+ version: 0.0.1 # app的版本
4
+ description: # app描述
5
+
6
+ license: https://choosealicense.com/licenses/mit/
7
+ homepage: # 出现bug时候提交反馈的地方
8
+ author: # app author
9
+
10
+ application:
11
+ subdomain: ${subdomain} # 默认访问子域名前缀
12
+ image: embed:app-runtime # 引用 lzc-build.yml 里的 images.app-runtime
13
+ routes:
14
+ # exec:// 路由会执行 /app/run.sh,并把请求转发到 8080 端口
15
+ - /=exec://8080,/app/run.sh
@@ -0,0 +1,15 @@
1
+ name: ${name} # app名称
2
+ package: ${package} # app的唯一标识符
3
+ version: 0.0.1 # app的版本
4
+ description: # app描述
5
+
6
+ license: https://choosealicense.com/licenses/mit/
7
+ homepage: # 出现bug时候提交反馈的地方
8
+ author: # app author
9
+
10
+ application:
11
+ subdomain: ${subdomain} # 默认访问子域名前缀
12
+ image: embed:app-runtime # 引用 lzc-build.yml 里的 images.app-runtime
13
+ routes:
14
+ # exec:// 路由会执行 /app/run.sh,并把请求转发到 3000 端口
15
+ - /=exec://3000,/app/run.sh
@@ -1,5 +1,3 @@
1
- # 整个文件中,可以通过 ${var} 的方式,使用 manifest 字段指定的文件定义的值
2
-
3
1
  # buildscript
4
2
  # - 可以为构建脚本的路径地址
5
3
  # - 如果构建命令简单,也可以直接写 sh 的命令
@@ -20,45 +18,3 @@ icon: ./lazycat.png
20
18
 
21
19
  # deploy_params 指定用户运行时输入的模板字段
22
20
  # deploy_params: ./lzc-deploy-params.yml
23
-
24
- # devshell 自定义应用的开发容器环境
25
- # - routes 指定应用容器的访问路由
26
-
27
- # devshell 没有指定 image 的情况,将会默认使用 registry.lazycat.cloud/lzc-cli/devshell:v0.0.5
28
- # devshell:
29
- # routes:
30
- # - /=http://127.0.0.1:8080
31
-
32
- # devshell 指定 image 的情况
33
- # devshell:
34
- # routes:
35
- # - /=http://127.0.0.1:3000
36
- # image: registry.lazycat.cloud/lzc-cli/devshell:v0.0.5
37
-
38
- # devshell 指定构建Dockerfile
39
- # image 字段如果没有定义,将默认使用 ${package}-devshell:${version}
40
- # devshell:
41
- # routes:
42
- # - /=http://127.0.0.1:3000
43
- # image: ${package}-devshell:${version}
44
- # pull_policy: build
45
- # build: .
46
-
47
- # dvshell 指定开发依赖的情况
48
- # 这种情况下,选用 alpine:latest 作为基础镜像,在 dependencies 中添加所需要的开发依赖即可
49
- # 如果 dependencies 和 build 同时存在,将会优先使用 dependencies
50
- devshell:
51
- routes:
52
- - /=http://127.0.0.1:3000
53
- dependencies:
54
- - nodejs
55
- - vim
56
- - npm
57
- # setupscript 每次进入到app container后都会执行的配置脚本
58
- # - 可以为脚本的路径地址
59
- # - 如果构建命令简单,也可以直接写 sh 的命令
60
- # setupscript: export GOPROXY=https://goproxy.cn
61
- # setupscript: ./setupscript.sh
62
- setupscript: |
63
- export GOPROXY=https://goproxy.cn
64
- export npm_config_registry=https://registry.npmmirror.com
@@ -0,0 +1 @@
1
+ lzc-build.base.yml
@@ -1,4 +1,4 @@
1
- # 整个文件中,可以通过 ${var} 的方式,使用 manifest 字段指定的文件定义的值
1
+ # 可选基础文件: lzc-build.base.yml(与当前文件同目录,先加载再与当前文件合并)
2
2
 
3
3
  # buildscript
4
4
  # - 可以为构建脚本的路径地址
@@ -11,48 +11,33 @@ manifest: ./lzc-manifest.yml
11
11
  # contentdir: 指定打包的内容,将会打包到 lpk 中
12
12
  # contentdir: ./
13
13
 
14
+ # images: 在盒子侧构建镜像并写入 lpk v2 images 目录
15
+ # - key 是 image alias
16
+ # - 在 lzc-manifest.yml 中通过 embed:alias 引用
17
+ # - dockerfile 与 dockerfile-content 二选一
18
+ # - context 可选,默认是 dockerfile 所在目录
19
+ # images:
20
+ # my-nginx:
21
+ # context: ./image
22
+ # dockerfile: ./image/Dockerfile
23
+ # # 可选,默认 registry.lazycat.cloud
24
+ # upstream-match: registry.lazycat.cloud
25
+ # other-image:
26
+ # dockerfile: ./other/Dockerfile
27
+ # inline-image:
28
+ # context: ./
29
+ # dockerfile-content: |
30
+ # FROM alpine:3.20
31
+ # RUN echo "hello"
32
+ #
33
+ # 构建时默认是混合模式:
34
+ # - 先沿父镜像链向上找 upstream
35
+ # - 找到 upstream 后,仅 embed 非 upstream 的 layers
36
+ # - 如果未匹配到 upstream,则自动全量 embed
37
+
14
38
  # pkgout: lpk 包的输出路径
15
39
  pkgout: ./
16
40
 
17
41
  # icon 指定 lpk 包 icon 的路径路径,如果不指定将会警告
18
42
  # icon 仅仅允许 png 后缀的文件
19
43
  icon: ./lzc-icon.png
20
- # devshell 自定义应用的开发容器环境
21
- # - routes 指定应用容器的访问路由
22
-
23
- # devshell 没有指定 image 的情况,将会默认使用 registry.lazycat.cloud/lzc-cli/devshell:v0.0.5
24
- # devshell:
25
- # routes:
26
- # - /=http://127.0.0.1:8080
27
-
28
- # devshell 指定 image 的情况
29
- # devshell:
30
- # routes:
31
- # - /=http://127.0.0.1:3000
32
- # image: registry.lazycat.cloud/lzc-cli/devshell:v0.0.5
33
-
34
- # devshell 指定构建Dockerfile
35
- # image 字段如果没有定义,将默认使用 ${package}-devshell:${version}
36
- # devshell:
37
- # routes:
38
- # - /=http://127.0.0.1:3000
39
- # image: ${package}-devshell:${version}
40
- # pull_policy: build
41
- # build: .
42
-
43
- # dvshell 指定开发依赖的情况
44
- # 这种情况下,选用 alpine:latest 作为基础镜像,在 dependencies 中添加所需要的开发依赖即可
45
- # 如果 dependencies 和 build 同时存在,将会优先使用 dependencies
46
- # devshell:
47
- # routes:
48
- # - /=http://127.0.0.1:3000
49
- # dependencies:
50
- # - nodejs
51
- # - npm
52
- # # setupscript 每次进入到app container后都会执行的配置脚本
53
- # # - 可以为脚本的路径地址
54
- # # - 如果构建命令简单,也可以直接写 sh 的命令
55
- # # setupscript: export GOPROXY=https://goproxy.cn
56
- # # setupscript: ./setupscript.sh
57
- # setupscript: |
58
- # export npm_config_registry=https://registry.npmmirror.com
@@ -1,11 +1,18 @@
1
- name: helloworld
2
- package: cloud.lazycat.app.helloworld
3
- version: 0.0.1
4
- description:
1
+ name: helloworld # 应用名称(启动器里显示)
2
+ package: cloud.lazycat.app.helloworld # 应用唯一ID,建议使用反向域名格式
3
+ version: 0.0.1 # 应用版本,建议遵循 semver
4
+ description: # 应用描述
5
5
  license: https://choosealicense.com/licenses/mit/
6
- homepage:
7
- author:
6
+ homepage: # 项目主页或文档地址
7
+ author: # 作者信息
8
+
9
+ # application 是默认前台服务,对应固定 service 名 app
8
10
  application:
9
- subdomain: helloworld
11
+ subdomain: helloworld # 默认访问子域名前缀
10
12
  routes:
13
+ # 路由示例:把首页转发到一个外部站点
14
+ # 常见写法:
15
+ # - /=file:///lzcapp/pkg/content/dist (静态前端)
16
+ # - /=exec://3000,/app/run.sh (执行命令并转发到端口)
17
+ # - /api/=http://backend:3000/api/ (转发到其他 service)
11
18
  - /=https://developer.lazycat.cloud/
@@ -0,0 +1,19 @@
1
+ FROM registry.lazycat.cloud/lzc/lzcapp:3.20.3 AS builder
2
+
3
+ RUN apk add --no-cache go
4
+ WORKDIR /workspace
5
+
6
+ COPY go.mod ./
7
+ COPY main.go ./
8
+ RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o /workspace/app ./main.go
9
+
10
+ FROM registry.lazycat.cloud/lzc/lzcapp:3.20.3
11
+
12
+ WORKDIR /app
13
+ COPY --from=builder /workspace/app /app/app
14
+ COPY run.sh /app/run.sh
15
+ RUN chmod +x /app/run.sh /app/app
16
+ COPY web /lzcapp/pkg/content/web
17
+
18
+ EXPOSE 3000
19
+ CMD ["/app/run.sh"]
@@ -0,0 +1,33 @@
1
+ # Lazycat Golang 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 golang-app.lpk
28
+ ```
29
+
30
+ ## Install
31
+ ```bash
32
+ lzc-cli lpk install golang-app.lpk
33
+ ```
@@ -0,0 +1,3 @@
1
+ /bin/
2
+ *.lpk
3
+ /cloud.lazycat.app.golang-template
@@ -0,0 +1,3 @@
1
+ module cloud.lazycat.app.golang-template
2
+
3
+ go 1.22
@@ -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,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("/lzcapp/pkg/content/web")))
251
+ _ = http.ListenAndServe(":3000", mux)
252
+ }
@@ -0,0 +1,3 @@
1
+ #!/bin/sh
2
+ set -e
3
+ exec /app/app