@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,82 @@
1
+ import logger from 'loglevel';
2
+ import { addProjectTargetOptions, resolveProjectRuntime, ensureProjectServiceRunning } from './project_runtime.js';
3
+ import { DEFAULT_PROJECT_SYNC_TARGET } from './project_sync.js';
4
+
5
+ export async function ensureWorkdir(runtime, service, workdir) {
6
+ const normalized = String(workdir || '').trim();
7
+ if (!normalized) {
8
+ return '';
9
+ }
10
+ await runtime.bridge.lzcDockerComposeCapture([
11
+ '-p',
12
+ runtime.composeProjectName,
13
+ 'exec',
14
+ '-T',
15
+ String(service || 'app'),
16
+ 'mkdir',
17
+ '-p',
18
+ normalized,
19
+ ]);
20
+ return normalized;
21
+ }
22
+
23
+ export function resolveProjectExecCommandArgs(cmd, passthrough) {
24
+ if (Array.isArray(cmd) && cmd.length > 0) {
25
+ return cmd.map((item) => String(item));
26
+ }
27
+ if (Array.isArray(passthrough) && passthrough.length > 0) {
28
+ return passthrough.map((item) => String(item));
29
+ }
30
+ return ['/bin/sh'];
31
+ }
32
+
33
+ export function projectExecCommand() {
34
+ return {
35
+ command: 'exec [cmd..]',
36
+ desc: 'Execute command in a project service container',
37
+ builder: (args) => {
38
+ args.option('s', {
39
+ alias: 'service',
40
+ describe: 'Service name in docker compose project',
41
+ type: 'string',
42
+ default: 'app',
43
+ });
44
+ addProjectTargetOptions(args);
45
+ args.option('t', {
46
+ alias: 'tty',
47
+ describe: 'Allocate tty for exec',
48
+ type: 'boolean',
49
+ default: true,
50
+ });
51
+ args.option('w', {
52
+ alias: 'workdir',
53
+ describe: 'Working directory inside container',
54
+ type: 'string',
55
+ default: DEFAULT_PROJECT_SYNC_TARGET,
56
+ });
57
+ args.parserConfiguration({
58
+ 'populate--': true,
59
+ });
60
+ },
61
+ handler: async ({ cmd, '--': passthrough, service, config, dev, release, tty, workdir }) => {
62
+ const runtime = await resolveProjectRuntime(process.cwd(), { config, dev, release, command: 'lzc-cli project exec' });
63
+ logger.info(`Build config: ${runtime.configPath}`);
64
+ const targetService = String(service || 'app');
65
+ await ensureProjectServiceRunning(runtime, targetService);
66
+ const normalizedWorkdir = await ensureWorkdir(runtime, targetService, workdir);
67
+ const execCommand = resolveProjectExecCommandArgs(cmd, passthrough);
68
+
69
+ const composeArgs = ['-p', runtime.composeProjectName, 'exec'];
70
+ if (normalizedWorkdir) {
71
+ composeArgs.push('--workdir', normalizedWorkdir);
72
+ }
73
+ if (!tty) {
74
+ composeArgs.push('-T');
75
+ }
76
+ composeArgs.push(targetService, ...execCommand);
77
+
78
+ logger.debug('project exec:', composeArgs.join(' '));
79
+ await runtime.bridge.lzcDockerCompose(composeArgs);
80
+ },
81
+ };
82
+ }
@@ -0,0 +1,106 @@
1
+ import logger from 'loglevel';
2
+ import { addProjectTargetOptions, resolveProjectRuntime, getProjectDeployInfo, getProjectComposePs, getProjectErrmsgByDeployId } from './project_runtime.js';
3
+
4
+ function takeFirstLines(text, maxLines = 50) {
5
+ const lines = String(text ?? '').split(/\r?\n/);
6
+ const limit = Number.isInteger(maxLines) && maxLines > 0 ? maxLines : 50;
7
+ if (lines.length <= limit) {
8
+ return {
9
+ text: lines.join('\n'),
10
+ truncated: false,
11
+ };
12
+ }
13
+ return {
14
+ text: lines.slice(0, limit).join('\n'),
15
+ truncated: true,
16
+ };
17
+ }
18
+
19
+ function trimErrmsgByDockerConfigs(text) {
20
+ const raw = String(text ?? '');
21
+ const marker = '---------------docker-configs:-----------';
22
+ const idx = raw.indexOf(marker);
23
+ if (idx >= 0) {
24
+ return raw.slice(0, idx).trim();
25
+ }
26
+ return raw.trim();
27
+ }
28
+
29
+ function resolveTargetUrl(domain) {
30
+ const rawDomain = String(domain ?? '').trim();
31
+ if (!rawDomain) {
32
+ return '';
33
+ }
34
+ if (rawDomain.startsWith('http://') || rawDomain.startsWith('https://')) {
35
+ return rawDomain;
36
+ }
37
+ return `https://${rawDomain}`;
38
+ }
39
+
40
+ export async function printProjectInfo(runtime) {
41
+ const deploy = await getProjectDeployInfo(runtime);
42
+ const instanceStatus = String(deploy.instanceStatus ?? '').trim();
43
+ const instanceStatusLower = instanceStatus.toLowerCase();
44
+ const targetMode = runtime.bridge.isBuildRemoteMode() ? 'build-remote' : 'box-shell';
45
+ const targetUrl = resolveTargetUrl(deploy.domain);
46
+
47
+ logger.info(`Target mode: ${targetMode}`);
48
+ logger.info(`Target box: ${runtime.bridge.boxname}`);
49
+ if (targetUrl) {
50
+ logger.info(`Target URL: ${targetUrl}`);
51
+ }
52
+ logger.info(`Project package: ${runtime.pkgId}`);
53
+ logger.info(`Local version: ${deploy.localVersion || '(empty)'}`);
54
+ logger.info(`Deployed status: ${deploy.appStatus || '(unknown)'}`);
55
+ logger.info(`Instance status: ${deploy.instanceStatus || '(unknown)'}`);
56
+ logger.info(`Deployed version: ${deploy.deployedVersion || '(empty)'}`);
57
+ logger.info(`Current version deployed: ${deploy.currentVersionDeployed ? 'yes' : 'no'}`);
58
+ if (deploy.deployId) {
59
+ logger.info(`Deploy DIR: /lzcsys/data/system/pkgm/run/${deploy.deployId}`);
60
+ }
61
+
62
+ if (deploy.isRunning) {
63
+ logger.info('Project app is running.');
64
+ const psOutput = await getProjectComposePs(runtime);
65
+ if (psOutput) {
66
+ console.log(psOutput);
67
+ }
68
+ return deploy;
69
+ }
70
+ if (instanceStatusLower.includes('starting')) {
71
+ logger.info('Project app is starting. Please wait and run "lzc-cli project info" again.');
72
+ return deploy;
73
+ }
74
+ if (instanceStatusLower.includes('error')) {
75
+ logger.info('Project app is in error state.');
76
+ const errmsg = await getProjectErrmsgByDeployId(runtime, deploy.deployId);
77
+ const cleanedErrmsg = trimErrmsgByDockerConfigs(errmsg);
78
+ if (cleanedErrmsg) {
79
+ const { text: partialErrmsg, truncated } = takeFirstLines(cleanedErrmsg, 50);
80
+ logger.info(`Error message (first 50 lines):\n${partialErrmsg}`);
81
+ if (truncated) {
82
+ logger.info('Need more logs? Run "lzc-cli project log" for full output.');
83
+ }
84
+ } else {
85
+ logger.info('Error message: (empty)');
86
+ }
87
+ return deploy;
88
+ }
89
+ logger.info('Project app is not running. Run "lzc-cli project start" first.');
90
+ return deploy;
91
+ }
92
+
93
+ export function projectInfoCommand() {
94
+ return {
95
+ command: 'info',
96
+ desc: 'Show project deployment and runtime info',
97
+ builder: (args) => {
98
+ addProjectTargetOptions(args);
99
+ },
100
+ handler: async ({ config, dev, release }) => {
101
+ const runtime = await resolveProjectRuntime(process.cwd(), { config, dev, release, command: 'lzc-cli project info' });
102
+ logger.info(`Build config: ${runtime.configPath}`);
103
+ await printProjectInfo(runtime);
104
+ },
105
+ };
106
+ }
@@ -0,0 +1,62 @@
1
+ import logger from 'loglevel';
2
+ import { addProjectTargetOptions, resolveProjectRuntime, ensureProjectRunning, ensureProjectServiceRunning, getComposeProject } from './project_runtime.js';
3
+
4
+ export function projectLogCommand() {
5
+ return {
6
+ command: 'log',
7
+ desc: 'Show logs of project containers',
8
+ builder: (args) => {
9
+ args.option('s', {
10
+ alias: 'service',
11
+ describe: 'Service name in docker compose project',
12
+ type: 'string',
13
+ });
14
+ addProjectTargetOptions(args);
15
+ args.option('f', {
16
+ alias: 'follow',
17
+ describe: 'Follow log output',
18
+ type: 'boolean',
19
+ default: true,
20
+ });
21
+ args.option('tail', {
22
+ describe: 'Number of lines to show from the end of logs',
23
+ type: 'number',
24
+ default: 200,
25
+ });
26
+ args.option('since', {
27
+ describe: 'Show logs since timestamp or relative duration',
28
+ type: 'string',
29
+ });
30
+ },
31
+ handler: async ({ service, config, dev, release, follow, tail, since }) => {
32
+ const runtime = await resolveProjectRuntime(process.cwd(), { config, dev, release, command: 'lzc-cli project log' });
33
+ logger.info(`Build config: ${runtime.configPath}`);
34
+ const targetService = String(service ?? '').trim();
35
+ if (targetService) {
36
+ await ensureProjectServiceRunning(runtime, targetService);
37
+ } else {
38
+ await ensureProjectRunning(runtime);
39
+ const composeProject = await getComposeProject(runtime);
40
+ if (!composeProject || !composeProject.status.startsWith('running(')) {
41
+ throw new Error('Project app is not running. Run "lzc-cli project start" first.');
42
+ }
43
+ }
44
+ const composeArgs = ['-p', runtime.composeProjectName, 'logs'];
45
+ if (follow) {
46
+ composeArgs.push('-f');
47
+ }
48
+ if (tail !== undefined && tail !== null) {
49
+ composeArgs.push('--tail', String(tail));
50
+ }
51
+ if (since) {
52
+ composeArgs.push('--since', String(since));
53
+ }
54
+ if (targetService) {
55
+ composeArgs.push(targetService);
56
+ }
57
+
58
+ logger.debug('project log:', composeArgs.join(' '));
59
+ await runtime.bridge.lzcDockerCompose(composeArgs);
60
+ },
61
+ };
62
+ }
@@ -0,0 +1,356 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import shellApi from '../shellapi.js';
4
+ import { DebugBridge } from '../debug_bridge.js';
5
+ import { LpkBuild } from './lpk_build.js';
6
+ import { isUserApp } from '../utils.js';
7
+ import { resolveBuildRemoteFromFile, DEFAULT_BUILD_CONFIG_FILE } from '../build_remote.js';
8
+
9
+ export const DEFAULT_DEPLOY_BUILD_CONFIG_FILE = 'lzc-build.dev.yml';
10
+ export const DEFAULT_RELEASE_BUILD_CONFIG_FILE = DEFAULT_BUILD_CONFIG_FILE;
11
+
12
+ function isFile(pathname) {
13
+ return fs.existsSync(pathname) && fs.statSync(pathname).isFile();
14
+ }
15
+
16
+ function normalizeProjectTargetSelection(selection = '') {
17
+ if (typeof selection === 'string') {
18
+ return {
19
+ config: String(selection ?? '').trim(),
20
+ dev: false,
21
+ release: false,
22
+ };
23
+ }
24
+ if (!selection || typeof selection !== 'object') {
25
+ return {
26
+ config: '',
27
+ dev: false,
28
+ release: false,
29
+ };
30
+ }
31
+ return {
32
+ config: String(selection.config ?? '').trim(),
33
+ dev: !!selection.dev,
34
+ release: !!selection.release,
35
+ };
36
+ }
37
+
38
+ export function addProjectTargetOptions(args) {
39
+ args.option('c', {
40
+ alias: 'config',
41
+ describe: 'Build config file name',
42
+ type: 'string',
43
+ });
44
+ args.option('dev', {
45
+ describe: `Use ${DEFAULT_DEPLOY_BUILD_CONFIG_FILE}`,
46
+ type: 'boolean',
47
+ default: false,
48
+ });
49
+ args.option('release', {
50
+ describe: `Use ${DEFAULT_RELEASE_BUILD_CONFIG_FILE}`,
51
+ type: 'boolean',
52
+ default: false,
53
+ });
54
+ }
55
+
56
+ export function resolveBuildConfigPath(startDir = process.cwd(), buildConfigFile = DEFAULT_BUILD_CONFIG_FILE) {
57
+ const configName = String(buildConfigFile ?? DEFAULT_BUILD_CONFIG_FILE).trim() || DEFAULT_BUILD_CONFIG_FILE;
58
+ if (path.isAbsolute(configName)) {
59
+ return isFile(configName) ? configName : '';
60
+ }
61
+
62
+ const direct = path.resolve(startDir, configName);
63
+ if (isFile(direct)) {
64
+ return direct;
65
+ }
66
+
67
+ if (configName.includes('/') || configName.includes('\\')) {
68
+ return '';
69
+ }
70
+
71
+ let current = path.resolve(startDir);
72
+ while (true) {
73
+ const candidate = path.join(current, configName);
74
+ if (isFile(candidate)) {
75
+ return candidate;
76
+ }
77
+ const parent = path.dirname(current);
78
+ if (parent === current) {
79
+ break;
80
+ }
81
+ current = parent;
82
+ }
83
+ return '';
84
+ }
85
+
86
+ function resolveExplicitBuildConfigPath(startDir, selection) {
87
+ const normalized = normalizeProjectTargetSelection(selection);
88
+ if (normalized.dev && normalized.release) {
89
+ throw new Error('Cannot use --dev and --release together.');
90
+ }
91
+ if (normalized.config && (normalized.dev || normalized.release)) {
92
+ throw new Error('Cannot combine --config with --dev or --release.');
93
+ }
94
+ if (normalized.config) {
95
+ const explicitPath = resolveBuildConfigPath(startDir, normalized.config);
96
+ if (!explicitPath) {
97
+ throw new Error(`Build config file not found: ${normalized.config}`);
98
+ }
99
+ return explicitPath;
100
+ }
101
+ if (normalized.dev) {
102
+ const devPath = resolveBuildConfigPath(startDir, DEFAULT_DEPLOY_BUILD_CONFIG_FILE);
103
+ if (!devPath) {
104
+ throw new Error(`Build config file not found: ${DEFAULT_DEPLOY_BUILD_CONFIG_FILE}`);
105
+ }
106
+ return devPath;
107
+ }
108
+ if (normalized.release) {
109
+ const releasePath = resolveBuildConfigPath(startDir, DEFAULT_RELEASE_BUILD_CONFIG_FILE);
110
+ if (!releasePath) {
111
+ throw new Error(`Build config file not found: ${DEFAULT_RELEASE_BUILD_CONFIG_FILE}`);
112
+ }
113
+ return releasePath;
114
+ }
115
+ return '';
116
+ }
117
+
118
+ export function resolveProjectDeployConfigPath(startDir = process.cwd(), selection = '') {
119
+ const explicitPath = resolveExplicitBuildConfigPath(startDir, selection);
120
+ if (explicitPath) {
121
+ return explicitPath;
122
+ }
123
+
124
+ const preferredConfigPath = resolveBuildConfigPath(startDir, DEFAULT_DEPLOY_BUILD_CONFIG_FILE);
125
+ if (preferredConfigPath) {
126
+ return preferredConfigPath;
127
+ }
128
+
129
+ const defaultConfigPath = resolveBuildConfigPath(startDir, DEFAULT_RELEASE_BUILD_CONFIG_FILE);
130
+ if (!defaultConfigPath) {
131
+ throw new Error(`Build config file not found: ${DEFAULT_DEPLOY_BUILD_CONFIG_FILE} or ${DEFAULT_RELEASE_BUILD_CONFIG_FILE}`);
132
+ }
133
+ return defaultConfigPath;
134
+ }
135
+
136
+ export function resolveProjectReleaseConfigPath(startDir = process.cwd(), selection = '') {
137
+ const normalized = normalizeProjectTargetSelection(selection);
138
+ if (normalized.dev) {
139
+ throw new Error('Release command does not support --dev. Use --release or omit the flag.');
140
+ }
141
+ const explicitConfig = normalized.config;
142
+ if (explicitConfig) {
143
+ const explicitPath = resolveBuildConfigPath(startDir, explicitConfig);
144
+ if (!explicitPath) {
145
+ throw new Error(`Build config file not found: ${explicitConfig}`);
146
+ }
147
+ return explicitPath;
148
+ }
149
+
150
+ const defaultConfigPath = resolveBuildConfigPath(startDir, DEFAULT_RELEASE_BUILD_CONFIG_FILE);
151
+ if (!defaultConfigPath) {
152
+ throw new Error(`Build config file not found: ${DEFAULT_RELEASE_BUILD_CONFIG_FILE}`);
153
+ }
154
+ return defaultConfigPath;
155
+ }
156
+
157
+ export function resolveProjectCommandConfigPath(startDir = process.cwd(), selection = '') {
158
+ const explicitPath = resolveExplicitBuildConfigPath(startDir, selection);
159
+ if (explicitPath) {
160
+ return explicitPath;
161
+ }
162
+
163
+ const devPath = resolveBuildConfigPath(startDir, DEFAULT_DEPLOY_BUILD_CONFIG_FILE);
164
+ if (devPath) {
165
+ return devPath;
166
+ }
167
+ const releasePath = resolveBuildConfigPath(startDir, DEFAULT_RELEASE_BUILD_CONFIG_FILE);
168
+ if (releasePath) {
169
+ return releasePath;
170
+ }
171
+ throw new Error(`Build config file not found: ${DEFAULT_DEPLOY_BUILD_CONFIG_FILE} or ${DEFAULT_RELEASE_BUILD_CONFIG_FILE}`);
172
+ }
173
+
174
+ export async function resolveProjectRuntime(startDir = process.cwd(), selection = '') {
175
+ const configPath = resolveProjectCommandConfigPath(startDir, selection);
176
+
177
+ const projectCwd = path.dirname(configPath);
178
+ const configName = path.basename(configPath);
179
+
180
+ const buildRemote = resolveBuildRemoteFromFile(projectCwd, configName);
181
+ if (!buildRemote) {
182
+ await shellApi.init();
183
+ }
184
+
185
+ const lpkBuild = await new LpkBuild(projectCwd, configName).init();
186
+ const manifest = await lpkBuild.getManifest();
187
+ const pkgId = String(manifest?.package ?? '').trim();
188
+ if (!pkgId) {
189
+ throw new Error('Manifest package is empty');
190
+ }
191
+
192
+ const bridge = new DebugBridge(projectCwd, buildRemote);
193
+ await bridge.init();
194
+
195
+ const userApp = isUserApp(manifest);
196
+ const composeProjectName = pkgId.replaceAll('.', '');
197
+ const localVersion = String(manifest?.version ?? '').trim();
198
+
199
+ return {
200
+ projectCwd,
201
+ configName,
202
+ configPath,
203
+ manifest,
204
+ pkgId,
205
+ userApp,
206
+ bridge,
207
+ composeProjectName,
208
+ localVersion,
209
+ };
210
+ }
211
+
212
+ function firstNonEmptyLine(text) {
213
+ return String(text ?? '')
214
+ .split(/\r?\n/)
215
+ .map((line) => line.trim())
216
+ .find((line) => line.length > 0);
217
+ }
218
+
219
+ function parseComposeProjectList(raw) {
220
+ const text = String(raw ?? '').trim();
221
+ if (!text) {
222
+ return [];
223
+ }
224
+ try {
225
+ const parsed = JSON.parse(text);
226
+ if (!Array.isArray(parsed)) {
227
+ return [];
228
+ }
229
+ return parsed
230
+ .map((item) => ({
231
+ name: String(item?.Name ?? '').trim(),
232
+ status: String(item?.Status ?? '').trim(),
233
+ configFiles: String(item?.ConfigFiles ?? '').trim(),
234
+ }))
235
+ .filter((item) => item.name.length > 0);
236
+ } catch {
237
+ return [];
238
+ }
239
+ }
240
+
241
+ export async function listComposeProjects(runtime) {
242
+ try {
243
+ const output = await runtime.bridge.lzcDockerComposeCapture(['ls', '--format', 'json']);
244
+ return parseComposeProjectList(output);
245
+ } catch {
246
+ return [];
247
+ }
248
+ }
249
+
250
+ export async function getComposeProject(runtime) {
251
+ const projects = await listComposeProjects(runtime);
252
+ return projects.find((item) => item.name === runtime.composeProjectName) || null;
253
+ }
254
+
255
+ export async function getProjectDeployInfo(runtime) {
256
+ const remoteInfo = await runtime.bridge.info(runtime.pkgId);
257
+ const deployId = String(remoteInfo?.deploy_id ?? '').trim();
258
+ const deployedVersion = String(remoteInfo?.version ?? '').trim();
259
+ const domain = String(remoteInfo?.domain ?? '').trim();
260
+ const appStatus = String(remoteInfo?.status ?? '').trim();
261
+ const instanceStatus = String(remoteInfo?.instance_status ?? '').trim();
262
+ const errorReason = String(remoteInfo?.error_reason ?? '').trim();
263
+ const deployed = appStatus !== '' && appStatus !== 'NotInstalled';
264
+ const currentVersionDeployed = deployed && runtime.localVersion !== '' && deployedVersion === runtime.localVersion;
265
+ return {
266
+ ...remoteInfo,
267
+ deployId,
268
+ localVersion: runtime.localVersion,
269
+ deployedVersion,
270
+ domain,
271
+ appStatus,
272
+ instanceStatus,
273
+ errorReason,
274
+ deployed,
275
+ currentVersionDeployed,
276
+ isRunning: instanceStatus === 'Status_Running',
277
+ };
278
+ }
279
+
280
+ export async function getProjectErrmsgByDeployId(runtime, deployId) {
281
+ const targetDeployId = String(deployId ?? '').trim();
282
+ if (!targetDeployId) {
283
+ return '';
284
+ }
285
+ try {
286
+ const errmsgPath = `/data/system/pkgm/run/${targetDeployId}/errmsg`;
287
+ const raw = await runtime.bridge.hostReadFile(errmsgPath);
288
+ return String(raw ?? '').trim();
289
+ } catch {
290
+ return '';
291
+ }
292
+ }
293
+
294
+ export async function ensureProjectRunning(runtime) {
295
+ const deploy = await getProjectDeployInfo(runtime);
296
+ if (!deploy.isRunning) {
297
+ throw new Error('Project app is not running. Run "lzc-cli project start" first.');
298
+ }
299
+ return deploy;
300
+ }
301
+
302
+ export async function getProjectComposePs(runtime) {
303
+ try {
304
+ return await runtime.bridge.lzcDockerComposeCapture(['-p', runtime.composeProjectName, 'ps']);
305
+ } catch {
306
+ return '';
307
+ }
308
+ }
309
+
310
+ export async function findProjectServiceContainer(runtime, service = 'app') {
311
+ const targetService = String(service || 'app');
312
+ try {
313
+ const output = await runtime.bridge.lzcDockerComposeCapture(['-p', runtime.composeProjectName, 'ps', '--status', 'running', '-q', targetService]);
314
+ return firstNonEmptyLine(output) || '';
315
+ } catch {
316
+ return '';
317
+ }
318
+ }
319
+
320
+ export async function ensureProjectServiceRunning(runtime, service = 'app') {
321
+ const targetService = String(service || 'app');
322
+ const composeProject = await getComposeProject(runtime);
323
+ if (!composeProject || !composeProject.status.startsWith('running(')) {
324
+ throw new Error('Project app is not running. Run "lzc-cli project start" first.');
325
+ }
326
+ const containerId = await findProjectServiceContainer(runtime, targetService);
327
+ if (!containerId) {
328
+ throw new Error(`Service \"${targetService}\" is not running. Run \"lzc-cli project start\" first.`);
329
+ }
330
+ return containerId;
331
+ }
332
+
333
+ export async function getProjectServiceEnv(runtime, service = 'app') {
334
+ const containerId = await ensureProjectServiceRunning(runtime, service);
335
+ const output = await runtime.bridge.lzcDockerCapture(['inspect', containerId]);
336
+ const inspectList = JSON.parse(String(output ?? '').trim());
337
+ if (!Array.isArray(inspectList) || inspectList.length === 0 || typeof inspectList[0] !== 'object' || !inspectList[0]) {
338
+ throw new Error(`Invalid inspect output for service "${String(service || 'app')}"`);
339
+ }
340
+ const envList = inspectList[0]?.Config?.Env;
341
+ if (!Array.isArray(envList)) {
342
+ throw new Error(`Invalid env list for service "${String(service || 'app')}"`);
343
+ }
344
+ const envMap = {};
345
+ for (const item of envList) {
346
+ const pair = String(item ?? '');
347
+ const idx = pair.indexOf('=');
348
+ if (idx <= 0) {
349
+ continue;
350
+ }
351
+ const key = pair.slice(0, idx);
352
+ const value = pair.slice(idx + 1);
353
+ envMap[key] = value;
354
+ }
355
+ return envMap;
356
+ }
@@ -0,0 +1,95 @@
1
+ import logger from 'loglevel';
2
+ import { LpkBuild } from './lpk_build.js';
3
+ import { addProjectTargetOptions, resolveProjectRuntime, getProjectDeployInfo, getProjectComposePs } from './project_runtime.js';
4
+
5
+ function sleep(ms) {
6
+ return new Promise((resolve) => setTimeout(resolve, ms));
7
+ }
8
+
9
+ async function waitProjectRunning(runtime, timeoutMs = 90000) {
10
+ const startedAt = Date.now();
11
+ while (Date.now() - startedAt <= timeoutMs) {
12
+ const deploy = await getProjectDeployInfo(runtime);
13
+ if (deploy.isRunning) {
14
+ return deploy;
15
+ }
16
+ await sleep(1000);
17
+ }
18
+ return null;
19
+ }
20
+
21
+ async function waitProjectNotRunning(runtime, timeoutMs = 30000) {
22
+ const startedAt = Date.now();
23
+ while (Date.now() - startedAt <= timeoutMs) {
24
+ const deploy = await getProjectDeployInfo(runtime);
25
+ if (!deploy.isRunning) {
26
+ return deploy;
27
+ }
28
+ await sleep(1000);
29
+ }
30
+ return null;
31
+ }
32
+
33
+ async function deployCurrentVersion(runtime) {
34
+ const lpkBuild = await new LpkBuild(runtime.projectCwd, runtime.configName, { forceV2: true }).init();
35
+ lpkBuild.onBeforeBuildPackage(async (options) => {
36
+ delete options['devshell'];
37
+ return options;
38
+ });
39
+ const pkgPath = await lpkBuild.exec();
40
+ logger.info(`Install package: ${pkgPath}`);
41
+ await runtime.bridge.install(pkgPath, runtime.pkgId);
42
+ }
43
+
44
+ export function projectStartCommand() {
45
+ return {
46
+ command: 'start',
47
+ desc: 'Start project app',
48
+ builder: (args) => {
49
+ addProjectTargetOptions(args);
50
+ args.option('restart', {
51
+ describe: 'Force restart app instance',
52
+ type: 'boolean',
53
+ default: false,
54
+ });
55
+ },
56
+ handler: async ({ config, dev, release, restart }) => {
57
+ const runtime = await resolveProjectRuntime(process.cwd(), { config, dev, release, command: 'lzc-cli project start' });
58
+ logger.info(`Build config: ${runtime.configPath}`);
59
+ let deploy = await getProjectDeployInfo(runtime);
60
+
61
+ if (!deploy.currentVersionDeployed) {
62
+ logger.info('Current project version is not deployed. Build and install current version.');
63
+ await deployCurrentVersion(runtime);
64
+ deploy = await getProjectDeployInfo(runtime);
65
+ }
66
+
67
+ if (restart) {
68
+ if (deploy.isRunning) {
69
+ logger.info('Restart requested. Pause app instance first.');
70
+ await runtime.bridge.pause(runtime.pkgId);
71
+ deploy = await waitProjectNotRunning(runtime);
72
+ if (!deploy) {
73
+ throw new Error('Project app is still running after pause. Please check app status and try again.');
74
+ }
75
+ }
76
+ logger.info('Restart requested. Resume app instance.');
77
+ await runtime.bridge.resume(runtime.pkgId);
78
+ } else if (!deploy.isRunning) {
79
+ logger.info('App is not running. Resume app instance.');
80
+ await runtime.bridge.resume(runtime.pkgId);
81
+ }
82
+
83
+ deploy = await waitProjectRunning(runtime);
84
+ if (!deploy) {
85
+ throw new Error('Project app is still not running. Please check app status and try again.');
86
+ }
87
+
88
+ logger.info('Project app is running.');
89
+ const psOutput = await getProjectComposePs(runtime);
90
+ if (psOutput) {
91
+ console.log(psOutput);
92
+ }
93
+ },
94
+ };
95
+ }