@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
@@ -1,6 +1,6 @@
1
1
  import logger from 'loglevel';
2
2
  import inquirer from 'inquirer';
3
- import { contextDirname, loadFromYaml, envTemplateFile, ensureDir, ensureDirectoryExists, dumpToYaml, isFileExist, isValidAppId, fakeLoadManifestYml } from '../utils.js';
3
+ import { contextDirname, ensureDir, ensureDirectoryExists, dumpToYaml, isFileExist, isValidAppId, envsubstr } from '../utils.js';
4
4
  import path from 'node:path';
5
5
  import { TemplateConfig } from './lpk_create_generator.js';
6
6
  import fs from 'node:fs';
@@ -8,38 +8,113 @@ import yaml from 'js-yaml';
8
8
  import chalk from 'chalk';
9
9
  import { t } from '../i18n/index.js';
10
10
 
11
- let fsPromises = fs.promises;
12
11
 
13
- export class LpkManifest {
14
- constructor(defaultAppID) {
12
+ function fakeLoadManifestText(text) {
13
+ let obj = {
14
+ application: {
15
+ subdomain: undefined,
16
+ },
17
+ };
18
+
19
+ const normalizeValue = (value) => {
20
+ const commentIndex = value.indexOf(' #');
21
+ if (commentIndex >= 0) {
22
+ value = value.slice(0, commentIndex);
23
+ }
24
+ return value.trim();
25
+ };
26
+
27
+ String(text ?? '').split('\n').forEach((rawLine) => {
28
+ const line = rawLine.trim();
29
+ const arr = line.split(':');
30
+ if (arr.length !== 2) {
31
+ return;
32
+ }
33
+ let [key, value] = arr;
34
+ value = normalizeValue(value);
35
+ if (!obj.package && key === 'package') {
36
+ obj.package = value;
37
+ }
38
+ if (!obj.application.subdomain && key === 'subdomain') {
39
+ obj.application.subdomain = value;
40
+ }
41
+ if (!obj.version && key === 'version') {
42
+ obj.version = value;
43
+ }
44
+ });
45
+ return obj;
46
+ }
47
+
48
+ async function renderTemplateText(templateFilePath, templateVars) {
49
+ const templateText = fs.readFileSync(templateFilePath, 'utf-8');
50
+ return await envsubstr(templateText, {
51
+ options: {
52
+ envs: Object.entries(templateVars).map(([key, value]) => ({
53
+ name: key.replace(/-/g, '_'),
54
+ value,
55
+ })),
56
+ syntax: 'default',
57
+ protect: false,
58
+ },
59
+ });
60
+ }
61
+
62
+ function loadTemplateYaml(text, fallbackLoader) {
63
+ try {
64
+ return {
65
+ data: yaml.load(text),
66
+ excp: false,
67
+ };
68
+ } catch {
69
+ return {
70
+ data: fallbackLoader(text),
71
+ excp: true,
72
+ };
73
+ }
74
+ }
75
+
76
+ export class LpkTemplateFile {
77
+ constructor(defaultAppID, outputFileName, fallbackLoader = (text) => yaml.load(text)) {
15
78
  this.pwd = contextDirname(import.meta.url);
16
- this.manifest = {};
17
79
  this.defaultAppID = defaultAppID;
80
+ this.outputFileName = outputFileName;
81
+ this.fallbackLoader = fallbackLoader;
82
+ this.data = {};
83
+ this.text = '';
84
+ this.excpTemplate = false;
18
85
  }
19
86
 
20
- async init(manifestFilePath, askInfo = false) {
21
- if (!manifestFilePath) {
22
- manifestFilePath = path.join(this.pwd, '..', '..', 'template', '_lpk', 'manifest.yml.in');
87
+ async init(templateFilePath, askInfo = false, templateVars = null) {
88
+ if (!templateFilePath) {
89
+ throw new Error(`template file is required for ${this.outputFileName}`);
23
90
  }
24
91
 
92
+ let vars = templateVars;
25
93
  if (askInfo) {
26
- let answer = await this.askLpkInfo();
27
- this.manifest = yaml.load(await envTemplateFile(manifestFilePath, answer));
28
- } else {
29
- try {
30
- this.manifest = loadFromYaml(manifestFilePath);
31
- } catch (err) {
32
- this.manifest = fakeLoadManifestYml(manifestFilePath);
33
- this.excpManifest = true;
34
- }
94
+ vars = vars || (await this.askLpkInfo());
95
+ this.text = await renderTemplateText(templateFilePath, vars);
96
+ const loaded = loadTemplateYaml(this.text, this.fallbackLoader);
97
+ this.data = loaded.data;
98
+ this.excpTemplate = loaded.excp;
99
+ return vars;
35
100
  }
101
+
102
+ const templateText = fs.readFileSync(templateFilePath, 'utf-8');
103
+ const loaded = loadTemplateYaml(templateText, this.fallbackLoader);
104
+ this.data = loaded.data;
105
+ this.excpTemplate = loaded.excp;
106
+ return vars;
36
107
  }
37
108
 
38
109
  async create(outputDir) {
39
- let outputFilePath = path.join(outputDir, 'lzc-manifest.yml');
110
+ let outputFilePath = path.join(outputDir, this.outputFileName);
40
111
  logger.debug(`create file ${outputFilePath}`);
41
112
  ensureDir(outputFilePath);
42
- return dumpToYaml(this.manifest, outputFilePath);
113
+ if (this.text) {
114
+ fs.writeFileSync(outputFilePath, this.text);
115
+ return;
116
+ }
117
+ return dumpToYaml(this.data, outputFilePath);
43
118
  }
44
119
 
45
120
  async askLpkInfo() {
@@ -61,61 +136,133 @@ export class LpkManifest {
61
136
  },
62
137
  },
63
138
  ]);
64
- const answer = {
139
+ return {
65
140
  name: appId,
66
141
  package: `cloud.lazycat.app.${appId}`,
67
142
  subdomain: appId,
68
143
  };
69
- return answer;
70
144
  }
71
145
  }
72
146
 
147
+ export class LpkManifest extends LpkTemplateFile {
148
+ constructor(defaultAppID) {
149
+ super(defaultAppID, 'lzc-manifest.yml', fakeLoadManifestText);
150
+ }
151
+ }
152
+
153
+ export class LpkPackageFile extends LpkTemplateFile {
154
+ constructor(defaultAppID) {
155
+ super(defaultAppID, 'package.yml');
156
+ }
157
+ }
158
+
159
+ const PROJECT_TEMPLATE_OPTIONS = [
160
+ {
161
+ i18nKey: 'lzc_cli.lib.app.lpk_create.template_option_hello_vue',
162
+ defaultText: 'hello-vue (Vue基础模板)',
163
+ value: 'hello-vue',
164
+ },
165
+ {
166
+ i18nKey: 'lzc_cli.lib.app.lpk_create.template_option_todolist_java',
167
+ defaultText: 'todolist-java (Java Todo示例)',
168
+ value: 'todolist-java',
169
+ },
170
+ {
171
+ i18nKey: 'lzc_cli.lib.app.lpk_create.template_option_todolist_python',
172
+ defaultText: 'todolist-python (Python Todo示例)',
173
+ value: 'todolist-python',
174
+ },
175
+ {
176
+ i18nKey: 'lzc_cli.lib.app.lpk_create.template_option_todolist_golang',
177
+ defaultText: 'todolist-golang (Golang Todo示例)',
178
+ value: 'todolist-golang',
179
+ },
180
+ {
181
+ i18nKey: 'lzc_cli.lib.app.lpk_create.template_option_gui_vnc',
182
+ defaultText: 'gui-vnc (GUI VNC Embed示例)',
183
+ value: 'gui-vnc',
184
+ },
185
+ {
186
+ i18nKey: 'lzc_cli.lib.app.lpk_create.template_option_todolist_serverless',
187
+ defaultText: 'todolist-serverless (Serverless Todo示例)',
188
+ value: 'todolist-serverless',
189
+ },
190
+ ];
191
+
192
+ export function listProjectTemplateValues() {
193
+ return PROJECT_TEMPLATE_OPTIONS.map((item) => item.value);
194
+ }
195
+
196
+ export function normalizeTemplateType(rawType) {
197
+ const type = String(rawType ?? '').trim();
198
+ if (!type) {
199
+ return '';
200
+ }
201
+ const allowedTypes = listProjectTemplateValues();
202
+ if (!allowedTypes.includes(type)) {
203
+ throw new Error(`Unsupported template "${type}". Available templates: ${allowedTypes.join(', ')}`);
204
+ }
205
+ return type;
206
+ }
207
+
73
208
  export class TemplateInit {
74
209
  constructor(cwd, name, type) {
75
210
  this.cwd = cwd;
76
211
  this.name = name;
77
- this.type = type;
212
+ this.type = normalizeTemplateType(type);
78
213
  this.lpkManifest = new LpkManifest(name);
214
+ this.lpkPackageFile = new LpkPackageFile(name);
79
215
  }
80
216
 
81
217
  async init() {
82
- if (!this.type) {
83
- this.type = await chooseTemplate();
84
- }
218
+ this.type = await chooseTemplate(this.type);
85
219
 
86
- // 优先使用模板项目自定义的模板文件
87
- const typeTemplatePath = path.join(contextDirname(import.meta.url), '..', '..', 'template', '_lpk', `${this.type}.manifest.yml.in`);
88
- const templatePath = isFileExist(typeTemplatePath) ? typeTemplatePath : '';
89
- await this.lpkManifest.init(templatePath, true);
220
+ const templateRoot = path.join(contextDirname(import.meta.url), '..', '..', 'template', '_lpk');
221
+ const typeTemplatePath = path.join(templateRoot, `${this.type}.manifest.yml.in`);
222
+ const manifestTemplatePath = isFileExist(typeTemplatePath) ? typeTemplatePath : path.join(templateRoot, 'manifest.yml.in');
223
+ const packageTemplatePath = path.join(templateRoot, 'package.yml.in');
224
+ const templateVars = await this.lpkManifest.init(manifestTemplatePath, true);
225
+ await this.lpkPackageFile.init(packageTemplatePath, true, templateVars);
90
226
  }
91
227
 
92
228
  async create() {
93
- await this.lpkManifest.create(path.resolve(this.cwd, this.name));
229
+ const outputDir = path.resolve(this.cwd, this.name);
230
+ await this.lpkManifest.create(outputDir);
231
+ await this.lpkPackageFile.create(outputDir);
94
232
  }
95
233
  }
96
234
 
97
- export async function chooseTemplate() {
235
+ export async function chooseTemplate(preselectedType = '') {
236
+ const fixedType = normalizeTemplateType(preselectedType);
237
+ if (fixedType) {
238
+ return fixedType;
239
+ }
240
+
98
241
  return (
99
- await inquirer.prompt([
100
- {
101
- name: 'type',
102
- message: t('lzc_cli.lib.app.lpk_create.choose_template_message', '选择项目构建模板'),
103
- type: 'list',
104
- choices: ['vue3'],
105
- },
106
- ])
107
- )['type'];
242
+ await inquirer.prompt([
243
+ {
244
+ name: 'type',
245
+ message: t('lzc_cli.lib.app.lpk_create.choose_template_message', '选择项目构建模板'),
246
+ type: 'list',
247
+ choices: PROJECT_TEMPLATE_OPTIONS.map((item) => ({
248
+ name: t(item.i18nKey, item.defaultText),
249
+ value: item.value,
250
+ })),
251
+ },
252
+ ])
253
+ )['type'];
108
254
  }
109
255
 
110
256
  // 创建一个应用
111
- // - 先选择模板信息(golang, vue3)
257
+ // - 先选择模板信息(todolist-golang, hello-vue)
112
258
  // - 根据 lpk manifest 模板中所需要的字段填充manifest
113
259
  // - 调用模板的hook
114
260
  class LpkCreate {
115
- constructor(name, cwd = process.cwd()) {
261
+ constructor(name, cwd = process.cwd(), type = '') {
116
262
  this.name = name;
117
263
  this.cwd = cwd ?? process.cwd();
118
264
  this.appDir = path.resolve(this.cwd, this.name);
265
+ this.type = normalizeTemplateType(type);
119
266
  }
120
267
 
121
268
  async init() {
@@ -123,7 +270,7 @@ class LpkCreate {
123
270
  }
124
271
 
125
272
  async exec() {
126
- const type = await chooseTemplate();
273
+ const type = await chooseTemplate(this.type);
127
274
  const template = new TemplateInit(this.cwd, this.name, type);
128
275
  await template.init();
129
276
 
@@ -148,7 +295,7 @@ class LpkCreate {
148
295
  }
149
296
  }
150
297
 
151
- export default async (name, cwd = process.cwd()) => {
152
- const lpk = await new LpkCreate(name, cwd).init();
298
+ export default async (name, cwd = process.cwd(), type = '') => {
299
+ const lpk = await new LpkCreate(name, cwd, type).init();
153
300
  return await lpk.exec();
154
301
  };
@@ -4,7 +4,6 @@ import fs from 'fs';
4
4
  import { isBinaryFileSync } from 'isbinaryfile';
5
5
  import { contextDirname } from '../utils.js';
6
6
  import chalk from 'chalk';
7
- // import spawn from "cross-spawn"
8
7
  import logger from 'loglevel';
9
8
  import { t } from '../i18n/index.js';
10
9
 
@@ -27,7 +26,6 @@ async function loadFiles(dirPath, prefix = '') {
27
26
  } else {
28
27
  content = fs.readFileSync(sourcePath, 'utf8');
29
28
  }
30
- // change _ prefix file into dot file
31
29
  if (p.startsWith('_')) {
32
30
  p = p.replace('_', '.');
33
31
  }
@@ -56,19 +54,25 @@ export async function generate(type, to, opts = {}) {
56
54
  writeFileTree(to, files);
57
55
  }
58
56
 
59
- function appCreateSuccessTip(name) {
57
+ function appCreateSuccessTipByCommands(name, installCommand = '') {
58
+ const installBlock = installCommand ? `\n ${installCommand}` : '';
60
59
  console.log(
61
60
  chalk.green(
62
61
  t(
63
- 'lzc_cli.lib.app.lpk_create_generator.app_create_success_tip',
62
+ 'lzc_cli.lib.app.lpk_create_generator.app_create_success_tip_common',
64
63
  `
65
64
  ✨ 懒猫微服应用已创建成功 !
66
- 进行下面步骤后即可进入容器开发
67
- cd {{ name }}
68
- lzc-cli project devshell
65
+ First deploy and open the app once
66
+ cd {{ name }}{{ installBlock }}
67
+ lzc-cli project deploy
68
+ lzc-cli project info
69
+ ℹ By default, project commands use lzc-build.dev.yml when it exists.
70
+ Each command prints the active Build config.
71
+ Use --release if you want to operate on lzc-build.yml.
69
72
  `,
70
73
  {
71
74
  name,
75
+ installBlock,
72
76
  interpolation: { escapeValue: false }
73
77
  },
74
78
  ).trim(),
@@ -77,22 +81,146 @@ function appCreateSuccessTip(name) {
77
81
  }
78
82
 
79
83
  export const TemplateConfig = {
80
- vue3: {
84
+ 'hello-vue': {
81
85
  build: async (to, option) => {
82
86
  await generate('vue', to, option);
83
87
  },
84
88
  after: async function (name) {
85
- appCreateSuccessTip(name);
89
+ appCreateSuccessTipByCommands(name);
86
90
  console.log(
87
91
  chalk.green(
88
92
  t(
89
93
  'lzc_cli.lib.app.lpk_create_generator.template_config_vue3_green',
90
94
  `
91
- ⚙️ 进入应用容器后执行下面命令:
92
- npm install
95
+ 🛠 Open the app first.
96
+ If the frontend dev server is not running yet, the app page will show the expected port and next step.
97
+ Then start local frontend dev:
93
98
  npm run dev
94
- 🚀 启动应用:
95
- 进入微服客户端启动器页面点击应用图标来测试应用
99
+ The command output prints the active Build config.
100
+ 🔎 For troubleshooting:
101
+ lzc-cli project log -f
102
+ `,
103
+ ).trim(),
104
+ ),
105
+ );
106
+ },
107
+ },
108
+ 'todolist-java': {
109
+ build: async (to, option) => {
110
+ await generate('springboot', to, option);
111
+ },
112
+ after: async function (name) {
113
+ appCreateSuccessTipByCommands(name);
114
+ console.log(
115
+ chalk.green(
116
+ t(
117
+ 'lzc_cli.lib.app.lpk_create_generator.template_config_springboot_green',
118
+ `
119
+ 🛠 Open the app first.
120
+ If the backend dev service is not ready yet, the app page will tell you how to sync or copy code and how to start the service.
121
+ Recommended backend dev loop:
122
+ lzc-cli project sync --watch
123
+ lzc-cli project exec /bin/sh
124
+ The command output prints the active Build config.
125
+ 🔎 For troubleshooting:
126
+ lzc-cli project log -f
127
+ `,
128
+ ).trim(),
129
+ ),
130
+ );
131
+ },
132
+ },
133
+ 'todolist-python': {
134
+ build: async (to, option) => {
135
+ await generate('python', to, option);
136
+ },
137
+ after: async function (name) {
138
+ appCreateSuccessTipByCommands(name);
139
+ console.log(
140
+ chalk.green(
141
+ t(
142
+ 'lzc_cli.lib.app.lpk_create_generator.template_config_python_green',
143
+ `
144
+ 🛠 Open the app first.
145
+ If the backend dev service is not ready yet, the app page will tell you how to sync or copy code and how to start the service.
146
+ Recommended backend dev loop:
147
+ lzc-cli project sync --watch
148
+ lzc-cli project exec /bin/sh
149
+ The command output prints the active Build config.
150
+ 🔎 For troubleshooting:
151
+ lzc-cli project log -f
152
+ `,
153
+ ).trim(),
154
+ ),
155
+ );
156
+ },
157
+ },
158
+ 'todolist-golang': {
159
+ build: async (to, option) => {
160
+ await generate('golang', to, option);
161
+ },
162
+ after: async function (name) {
163
+ appCreateSuccessTipByCommands(name);
164
+ console.log(
165
+ chalk.green(
166
+ t(
167
+ 'lzc_cli.lib.app.lpk_create_generator.template_config_golang_green',
168
+ `
169
+ 🛠 Open the app first.
170
+ If the backend dev service is not ready yet, the app page will tell you how to sync or copy code and how to start the service.
171
+ Recommended backend dev loop:
172
+ lzc-cli project sync --watch
173
+ lzc-cli project exec /bin/sh
174
+ The command output prints the active Build config.
175
+ 🔎 For troubleshooting:
176
+ lzc-cli project log -f
177
+ `,
178
+ ).trim(),
179
+ ),
180
+ );
181
+ },
182
+ },
183
+ 'gui-vnc': {
184
+ build: async (to, option) => {
185
+ await generate('gui-vnc', to, option);
186
+ },
187
+ after: async function (name) {
188
+ appCreateSuccessTipByCommands(name);
189
+ console.log(
190
+ chalk.green(
191
+ t(
192
+ 'lzc_cli.lib.app.lpk_create_generator.template_config_gui_vnc_green',
193
+ `
194
+ 🛠 GUI VNC app (embedded image template):
195
+ lzc-cli project deploy
196
+ The command output prints the active Build config.
197
+ 🔎 For troubleshooting:
198
+ lzc-cli project log -f
199
+ lzc-cli project exec -s desktop /bin/sh
200
+ `,
201
+ ).trim(),
202
+ ),
203
+ );
204
+ },
205
+ },
206
+ 'todolist-serverless': {
207
+ build: async (to, option) => {
208
+ await generate('vue-minidb', to, option);
209
+ },
210
+ after: async function (name) {
211
+ appCreateSuccessTipByCommands(name);
212
+ console.log(
213
+ chalk.green(
214
+ t(
215
+ 'lzc_cli.lib.app.lpk_create_generator.template_config_vue_minidb_green',
216
+ `
217
+ 🛠 Open the app first.
218
+ If the frontend dev server is not running yet, the app page will show the expected port and next step.
219
+ Then start local frontend dev:
220
+ npm run dev
221
+ The command output prints the active Build config.
222
+ 🔎 For troubleshooting:
223
+ lzc-cli project log -f
96
224
  `,
97
225
  ).trim(),
98
226
  ),
@@ -30,6 +30,7 @@ import { DebugBridge } from '../debug_bridge.js';
30
30
  import shellApi from '../shellapi.js';
31
31
  import { t } from '../i18n/index.js';
32
32
  import { collectContextFromDockerFile } from './lpk_devshell_docker.js';
33
+ import { resolveBuildRemoteFromOptions } from '../build_remote.js';
33
34
 
34
35
  // 判断是否需要重新构建
35
36
  // - 先判断 lzc-build.yml 是否发生改变
@@ -44,6 +45,7 @@ class AppDevShellMonitor {
44
45
 
45
46
  this.optionsFilePath = path.join(this.pwd, buildConfigFile);
46
47
  this.options = loadFromYaml(this.optionsFilePath);
48
+ this.buildRemote = resolveBuildRemoteFromOptions(this.options, `file:${this.optionsFilePath}`);
47
49
 
48
50
  this.manifestFilePath = this.options['manifest'] ? path.join(this.pwd, this.options['manifest']) : path.join(this.pwd, 'lzc-manifest.yml');
49
51
 
@@ -54,7 +56,7 @@ class AppDevShellMonitor {
54
56
  this.cacheFilePath = undefined;
55
57
  this.oldHash = undefined;
56
58
  this.newHash = undefined;
57
- this.bridge = new DebugBridge();
59
+ this.bridge = new DebugBridge(this.pwd, this.buildRemote);
58
60
  }
59
61
 
60
62
  async init() {
@@ -101,6 +103,7 @@ export class AppDevShell {
101
103
  this.buildConfigFile = buildConfigFile;
102
104
  this.isUserApp = false;
103
105
  this.monitor = undefined;
106
+ this.buildRemote = resolveBuildRemoteFromOptions(this.lpkBuild.options, `file:${this.lpkBuild.optionsFilePath}`);
104
107
  }
105
108
 
106
109
  async init() {
@@ -225,7 +228,6 @@ export class AppDevShell {
225
228
  // 避免应用永远处于 unhealth 导致状态卡在 starting
226
229
  this.lpkBuild.onBeforeDumpYaml(async (manifest, options) => {
227
230
  logger.debug('merge lzc-build.yml devshell services\n', options);
228
- const userapp = this.isUserApp ? shellApi.uid + '.' : '';
229
231
  const devshell = {
230
232
  application: {
231
233
  devshell: options['devshell'],
@@ -263,7 +265,7 @@ export class AppDevShell {
263
265
  logger.debug(t('lzc_cli.lib.app.lpk_devshell.devshell_build_image_for_box_tips', `开始在盒子中构建 {{ label }} 镜像 from {{ tempDir }}`, { label, tempDir, interpolation: { escapeValue: false } }));
264
266
 
265
267
  const contextTar = await collectContextFromDockerFile(tempDir, path.resolve(tempDir, 'Dockerfile'));
266
- const bridge = new DebugBridge();
268
+ const bridge = new DebugBridge(this.cwd, this.buildRemote);
267
269
  await bridge.init();
268
270
  const tag = await bridge.buildImage(label, contextTar);
269
271
  delete manifest['application']['devshell'];
@@ -291,7 +293,7 @@ export class AppDevShell {
291
293
 
292
294
  const contextTar = await collectContextFromDockerFile(process.cwd(), path.resolve(process.cwd(), config['build'], 'Dockerfile'));
293
295
 
294
- const bridge = new DebugBridge();
296
+ const bridge = new DebugBridge(this.cwd, this.buildRemote);
295
297
  await bridge.init();
296
298
  const tag = await bridge.buildImage(label, contextTar);
297
299
  delete manifest['application']['devshell'];
@@ -323,15 +325,9 @@ export class AppDevShell {
323
325
  return manifest;
324
326
  });
325
327
 
326
- // devshell 模式下,默认打开后台常驻
327
- this.lpkBuild.onBeforeDumpYaml(async (manifest) => {
328
- manifest['application']['background_task'] = true;
329
- return manifest;
330
- });
331
-
332
- // 添加一个 devshell 的标记在 lpk 中,标记当前 lpk 为一个 debug 版本
333
- this.lpkBuild.onBeforeDumpLpk(async (options, cwd, destDir) => {
334
- fs.writeFileSync(path.resolve(destDir, 'devshell'), '');
328
+ // 添加一个 devshell 的标记在 lpk 中,标记当前 lpk 为一个 debug 版本
329
+ this.lpkBuild.onBeforeDumpLpk(async (options, cwd, destDir) => {
330
+ fs.writeFileSync(path.resolve(destDir, 'devshell'), '');
335
331
  });
336
332
 
337
333
  // 在构建生成 lpk 包后,调用 deploy 进行部署
@@ -343,7 +339,7 @@ export class AppDevShell {
343
339
  async rsyncShell() {
344
340
  const manifest = await this.lpkBuild.getManifest();
345
341
  const pkgId = manifest['package'];
346
- const devshell = new DevShell(pkgId, this.isUserApp);
342
+ const devshell = new DevShell(pkgId, this.isUserApp, this.cwd, this.buildRemote);
347
343
  try {
348
344
  await devshell.shell();
349
345
  } catch (e) {
@@ -357,16 +353,34 @@ export class AppDevShell {
357
353
  }
358
354
 
359
355
  class DevShell {
360
- constructor(appId, isUserApp) {
356
+ constructor(appId, isUserApp, cwd = process.cwd(), buildRemote = null) {
361
357
  this.appId = appId;
362
358
  this.isUserApp = isUserApp;
359
+ this.cwd = cwd;
360
+ this.buildRemote = buildRemote;
361
+ this.syncUID = '';
362
+ }
363
+
364
+ async resolveUID() {
365
+ if (!this.buildRemote) {
366
+ return shellApi.uid;
367
+ }
368
+ if (this.syncUID) {
369
+ return this.syncUID;
370
+ }
371
+ const bridge = new DebugBridge(this.cwd, this.buildRemote);
372
+ await bridge.init();
373
+ this.syncUID = await bridge.resolveCurrentUID();
374
+ return this.syncUID;
363
375
  }
364
376
 
365
377
  async syncProject(appId) {
366
- const resolvedIp = await resolveDomain(`dev.${shellApi.boxname}.heiyu.space`);
378
+ const uid = await this.resolveUID();
379
+ const resolvedIp = this.buildRemote ? this.buildRemote.sshHost : await resolveDomain(`dev.${shellApi.boxname}.heiyu.space`);
367
380
  const rsyncDebug = isDebugMode() ? '-P' : '';
368
- const destDir = `${appId}${this.isUserApp ? '/' + shellApi.uid : ''}`;
369
- const dest = `rsync://${shellApi.uid}@[${resolvedIp}]:874/lzcapp_cache/${destDir}/devshell`;
381
+ const destDir = `${appId}${this.isUserApp ? '/' + uid : ''}`;
382
+ const rsyncHost = resolvedIp.includes(':') ? `[${resolvedIp}]` : resolvedIp;
383
+ const dest = `rsync://${uid}@${rsyncHost}:874/lzcapp_cache/${destDir}/devshell`;
370
384
  const rsyncCmd = isWindows ? path.join(contextDirname(import.meta.url), '..', '..', 'template', '_lpk', 'win-rsync', 'rsync.exe') : `rsync`;
371
385
 
372
386
  try {
@@ -454,7 +468,7 @@ class DevShell {
454
468
  }
455
469
 
456
470
  async connectShell(onconnect = null) {
457
- const bridge = new DebugBridge();
471
+ const bridge = new DebugBridge(this.cwd, this.buildRemote);
458
472
  await bridge.init();
459
473
  await bridge.devshell(this.appId, this.isUserApp, onconnect);
460
474
  }