@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.
- package/README.md +30 -5
- package/changelog.md +4 -0
- package/lib/app/index.js +174 -58
- package/lib/app/lpk_build.js +192 -17
- package/lib/app/lpk_build_images.js +728 -0
- package/lib/app/lpk_create.js +93 -21
- package/lib/app/lpk_create_generator.js +144 -9
- package/lib/app/lpk_devshell.js +33 -19
- package/lib/app/lpk_embed_images.js +257 -0
- package/lib/app/lpk_installer.js +14 -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/box/index.js +101 -4
- package/lib/box/ssh_remote.js +259 -0
- package/lib/build_remote.js +22 -0
- package/lib/config/index.js +1 -1
- package/lib/debug_bridge.js +837 -46
- package/lib/docker/index.js +30 -10
- package/lib/i18n/index.js +1 -0
- package/lib/i18n/locales/en/translation.json +17 -5
- package/lib/i18n/locales/zh/translation.json +16 -4
- package/lib/lpk/core.js +487 -0
- package/lib/lpk/index.js +210 -0
- package/lib/sig/core.js +254 -0
- package/lib/sig/index.js +88 -0
- package/lib/utils.js +3 -1
- package/package.json +2 -1
- 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,261 @@
|
|
|
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 = 'lzc-build.release.yml';
|
|
11
|
+
|
|
12
|
+
function isFile(pathname) {
|
|
13
|
+
return fs.existsSync(pathname) && fs.statSync(pathname).isFile();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function resolveBuildConfigPath(startDir = process.cwd(), buildConfigFile = DEFAULT_BUILD_CONFIG_FILE) {
|
|
17
|
+
const configName = String(buildConfigFile ?? DEFAULT_BUILD_CONFIG_FILE).trim() || DEFAULT_BUILD_CONFIG_FILE;
|
|
18
|
+
if (path.isAbsolute(configName)) {
|
|
19
|
+
return isFile(configName) ? configName : '';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const direct = path.resolve(startDir, configName);
|
|
23
|
+
if (isFile(direct)) {
|
|
24
|
+
return direct;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (configName.includes('/') || configName.includes('\\')) {
|
|
28
|
+
return '';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
let current = path.resolve(startDir);
|
|
32
|
+
while (true) {
|
|
33
|
+
const candidate = path.join(current, configName);
|
|
34
|
+
if (isFile(candidate)) {
|
|
35
|
+
return candidate;
|
|
36
|
+
}
|
|
37
|
+
const parent = path.dirname(current);
|
|
38
|
+
if (parent === current) {
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
current = parent;
|
|
42
|
+
}
|
|
43
|
+
return '';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function resolveProjectDeployConfigPath(startDir = process.cwd(), buildConfigFile = '') {
|
|
47
|
+
return resolvePreferredBuildConfigPath(startDir, buildConfigFile, DEFAULT_DEPLOY_BUILD_CONFIG_FILE);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function resolveProjectReleaseConfigPath(startDir = process.cwd(), buildConfigFile = '') {
|
|
51
|
+
return resolvePreferredBuildConfigPath(startDir, buildConfigFile, DEFAULT_RELEASE_BUILD_CONFIG_FILE);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function resolvePreferredBuildConfigPath(startDir, buildConfigFile, preferredConfigFile) {
|
|
55
|
+
const explicitConfig = String(buildConfigFile ?? '').trim();
|
|
56
|
+
if (explicitConfig) {
|
|
57
|
+
const explicitPath = resolveBuildConfigPath(startDir, explicitConfig);
|
|
58
|
+
if (!explicitPath) {
|
|
59
|
+
throw new Error(`Build config file not found: ${explicitConfig}`);
|
|
60
|
+
}
|
|
61
|
+
return explicitPath;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const preferredConfigPath = resolveBuildConfigPath(startDir, preferredConfigFile);
|
|
65
|
+
if (preferredConfigPath) {
|
|
66
|
+
return preferredConfigPath;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const defaultConfigPath = resolveBuildConfigPath(startDir, DEFAULT_BUILD_CONFIG_FILE);
|
|
70
|
+
if (!defaultConfigPath) {
|
|
71
|
+
throw new Error(`Build config file not found: ${preferredConfigFile} or ${DEFAULT_BUILD_CONFIG_FILE}`);
|
|
72
|
+
}
|
|
73
|
+
return defaultConfigPath;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export async function resolveProjectRuntime(startDir = process.cwd(), buildConfigFile = DEFAULT_BUILD_CONFIG_FILE) {
|
|
77
|
+
const configPath = resolveBuildConfigPath(startDir, buildConfigFile);
|
|
78
|
+
if (!configPath) {
|
|
79
|
+
throw new Error(`Build config file not found: ${buildConfigFile}`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const projectCwd = path.dirname(configPath);
|
|
83
|
+
const configName = path.basename(configPath);
|
|
84
|
+
|
|
85
|
+
const buildRemote = resolveBuildRemoteFromFile(projectCwd, configName);
|
|
86
|
+
if (!buildRemote) {
|
|
87
|
+
await shellApi.init();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const lpkBuild = await new LpkBuild(projectCwd, configName).init();
|
|
91
|
+
const manifest = await lpkBuild.getManifest();
|
|
92
|
+
const pkgId = String(manifest?.package ?? '').trim();
|
|
93
|
+
if (!pkgId) {
|
|
94
|
+
throw new Error('Manifest package is empty');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const bridge = new DebugBridge(projectCwd, buildRemote);
|
|
98
|
+
await bridge.init();
|
|
99
|
+
|
|
100
|
+
const userApp = isUserApp(manifest);
|
|
101
|
+
const composeProjectName = pkgId.replaceAll('.', '');
|
|
102
|
+
const localVersion = String(manifest?.version ?? '').trim();
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
projectCwd,
|
|
106
|
+
configName,
|
|
107
|
+
configPath,
|
|
108
|
+
manifest,
|
|
109
|
+
pkgId,
|
|
110
|
+
userApp,
|
|
111
|
+
bridge,
|
|
112
|
+
composeProjectName,
|
|
113
|
+
localVersion,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function firstNonEmptyLine(text) {
|
|
118
|
+
return String(text ?? '')
|
|
119
|
+
.split(/\r?\n/)
|
|
120
|
+
.map((line) => line.trim())
|
|
121
|
+
.find((line) => line.length > 0);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function parseComposeProjectList(raw) {
|
|
125
|
+
const text = String(raw ?? '').trim();
|
|
126
|
+
if (!text) {
|
|
127
|
+
return [];
|
|
128
|
+
}
|
|
129
|
+
try {
|
|
130
|
+
const parsed = JSON.parse(text);
|
|
131
|
+
if (!Array.isArray(parsed)) {
|
|
132
|
+
return [];
|
|
133
|
+
}
|
|
134
|
+
return parsed
|
|
135
|
+
.map((item) => ({
|
|
136
|
+
name: String(item?.Name ?? '').trim(),
|
|
137
|
+
status: String(item?.Status ?? '').trim(),
|
|
138
|
+
configFiles: String(item?.ConfigFiles ?? '').trim(),
|
|
139
|
+
}))
|
|
140
|
+
.filter((item) => item.name.length > 0);
|
|
141
|
+
} catch {
|
|
142
|
+
return [];
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export async function listComposeProjects(runtime) {
|
|
147
|
+
try {
|
|
148
|
+
const output = await runtime.bridge.lzcDockerComposeCapture(['ls', '--format', 'json']);
|
|
149
|
+
return parseComposeProjectList(output);
|
|
150
|
+
} catch {
|
|
151
|
+
return [];
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export async function getComposeProject(runtime) {
|
|
156
|
+
const projects = await listComposeProjects(runtime);
|
|
157
|
+
return projects.find((item) => item.name === runtime.composeProjectName) || null;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export async function getProjectDeployInfo(runtime) {
|
|
161
|
+
const remoteInfo = await runtime.bridge.info(runtime.pkgId);
|
|
162
|
+
const deployId = String(remoteInfo?.deploy_id ?? '').trim();
|
|
163
|
+
const deployedVersion = String(remoteInfo?.version ?? '').trim();
|
|
164
|
+
const domain = String(remoteInfo?.domain ?? '').trim();
|
|
165
|
+
const appStatus = String(remoteInfo?.status ?? '').trim();
|
|
166
|
+
const instanceStatus = String(remoteInfo?.instance_status ?? '').trim();
|
|
167
|
+
const errorReason = String(remoteInfo?.error_reason ?? '').trim();
|
|
168
|
+
const deployed = appStatus !== '' && appStatus !== 'NotInstalled';
|
|
169
|
+
const currentVersionDeployed = deployed && runtime.localVersion !== '' && deployedVersion === runtime.localVersion;
|
|
170
|
+
return {
|
|
171
|
+
...remoteInfo,
|
|
172
|
+
deployId,
|
|
173
|
+
localVersion: runtime.localVersion,
|
|
174
|
+
deployedVersion,
|
|
175
|
+
domain,
|
|
176
|
+
appStatus,
|
|
177
|
+
instanceStatus,
|
|
178
|
+
errorReason,
|
|
179
|
+
deployed,
|
|
180
|
+
currentVersionDeployed,
|
|
181
|
+
isRunning: instanceStatus === 'Status_Running',
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export async function getProjectErrmsgByDeployId(runtime, deployId) {
|
|
186
|
+
const targetDeployId = String(deployId ?? '').trim();
|
|
187
|
+
if (!targetDeployId) {
|
|
188
|
+
return '';
|
|
189
|
+
}
|
|
190
|
+
try {
|
|
191
|
+
const errmsgPath = `/data/system/pkgm/run/${targetDeployId}/errmsg`;
|
|
192
|
+
const raw = await runtime.bridge.hostReadFile(errmsgPath);
|
|
193
|
+
return String(raw ?? '').trim();
|
|
194
|
+
} catch {
|
|
195
|
+
return '';
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export async function ensureProjectRunning(runtime) {
|
|
200
|
+
const deploy = await getProjectDeployInfo(runtime);
|
|
201
|
+
if (!deploy.isRunning) {
|
|
202
|
+
throw new Error('Project app is not running. Run "lzc-cli project start" first.');
|
|
203
|
+
}
|
|
204
|
+
return deploy;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export async function getProjectComposePs(runtime) {
|
|
208
|
+
try {
|
|
209
|
+
return await runtime.bridge.lzcDockerComposeCapture(['-p', runtime.composeProjectName, 'ps']);
|
|
210
|
+
} catch {
|
|
211
|
+
return '';
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export async function findProjectServiceContainer(runtime, service = 'app') {
|
|
216
|
+
const targetService = String(service || 'app');
|
|
217
|
+
try {
|
|
218
|
+
const output = await runtime.bridge.lzcDockerComposeCapture(['-p', runtime.composeProjectName, 'ps', '--status', 'running', '-q', targetService]);
|
|
219
|
+
return firstNonEmptyLine(output) || '';
|
|
220
|
+
} catch {
|
|
221
|
+
return '';
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export async function ensureProjectServiceRunning(runtime, service = 'app') {
|
|
226
|
+
const targetService = String(service || 'app');
|
|
227
|
+
const composeProject = await getComposeProject(runtime);
|
|
228
|
+
if (!composeProject || !composeProject.status.startsWith('running(')) {
|
|
229
|
+
throw new Error('Project app is not running. Run "lzc-cli project start" first.');
|
|
230
|
+
}
|
|
231
|
+
const containerId = await findProjectServiceContainer(runtime, targetService);
|
|
232
|
+
if (!containerId) {
|
|
233
|
+
throw new Error(`Service \"${targetService}\" is not running. Run \"lzc-cli project start\" first.`);
|
|
234
|
+
}
|
|
235
|
+
return containerId;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export async function getProjectServiceEnv(runtime, service = 'app') {
|
|
239
|
+
const containerId = await ensureProjectServiceRunning(runtime, service);
|
|
240
|
+
const output = await runtime.bridge.lzcDockerCapture(['inspect', containerId]);
|
|
241
|
+
const inspectList = JSON.parse(String(output ?? '').trim());
|
|
242
|
+
if (!Array.isArray(inspectList) || inspectList.length === 0 || typeof inspectList[0] !== 'object' || !inspectList[0]) {
|
|
243
|
+
throw new Error(`Invalid inspect output for service "${String(service || 'app')}"`);
|
|
244
|
+
}
|
|
245
|
+
const envList = inspectList[0]?.Config?.Env;
|
|
246
|
+
if (!Array.isArray(envList)) {
|
|
247
|
+
throw new Error(`Invalid env list for service "${String(service || 'app')}"`);
|
|
248
|
+
}
|
|
249
|
+
const envMap = {};
|
|
250
|
+
for (const item of envList) {
|
|
251
|
+
const pair = String(item ?? '');
|
|
252
|
+
const idx = pair.indexOf('=');
|
|
253
|
+
if (idx <= 0) {
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
const key = pair.slice(0, idx);
|
|
257
|
+
const value = pair.slice(idx + 1);
|
|
258
|
+
envMap[key] = value;
|
|
259
|
+
}
|
|
260
|
+
return envMap;
|
|
261
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import logger from 'loglevel';
|
|
2
|
+
import { DEFAULT_BUILD_CONFIG_FILE } from '../build_remote.js';
|
|
3
|
+
import { LpkBuild } from './lpk_build.js';
|
|
4
|
+
import { resolveProjectRuntime, getProjectDeployInfo, getProjectComposePs } from './project_runtime.js';
|
|
5
|
+
|
|
6
|
+
function sleep(ms) {
|
|
7
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
async function waitProjectRunning(runtime, timeoutMs = 90000) {
|
|
11
|
+
const startedAt = Date.now();
|
|
12
|
+
while (Date.now() - startedAt <= timeoutMs) {
|
|
13
|
+
const deploy = await getProjectDeployInfo(runtime);
|
|
14
|
+
if (deploy.isRunning) {
|
|
15
|
+
return deploy;
|
|
16
|
+
}
|
|
17
|
+
await sleep(1000);
|
|
18
|
+
}
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function waitProjectNotRunning(runtime, timeoutMs = 30000) {
|
|
23
|
+
const startedAt = Date.now();
|
|
24
|
+
while (Date.now() - startedAt <= timeoutMs) {
|
|
25
|
+
const deploy = await getProjectDeployInfo(runtime);
|
|
26
|
+
if (!deploy.isRunning) {
|
|
27
|
+
return deploy;
|
|
28
|
+
}
|
|
29
|
+
await sleep(1000);
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function deployCurrentVersion(runtime) {
|
|
35
|
+
const lpkBuild = await new LpkBuild(runtime.projectCwd, runtime.configName).init();
|
|
36
|
+
lpkBuild.onBeforeBuildPackage(async (options) => {
|
|
37
|
+
delete options['devshell'];
|
|
38
|
+
return options;
|
|
39
|
+
});
|
|
40
|
+
const pkgPath = await lpkBuild.exec();
|
|
41
|
+
logger.info(`Install package: ${pkgPath}`);
|
|
42
|
+
await runtime.bridge.install(pkgPath, runtime.pkgId);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function projectStartCommand() {
|
|
46
|
+
return {
|
|
47
|
+
command: 'start',
|
|
48
|
+
desc: 'Start project app',
|
|
49
|
+
builder: (args) => {
|
|
50
|
+
args.option('c', {
|
|
51
|
+
alias: 'config',
|
|
52
|
+
describe: 'Build config file name',
|
|
53
|
+
type: 'string',
|
|
54
|
+
default: DEFAULT_BUILD_CONFIG_FILE,
|
|
55
|
+
});
|
|
56
|
+
args.option('restart', {
|
|
57
|
+
describe: 'Force restart app instance',
|
|
58
|
+
type: 'boolean',
|
|
59
|
+
default: false,
|
|
60
|
+
});
|
|
61
|
+
},
|
|
62
|
+
handler: async ({ config, restart }) => {
|
|
63
|
+
const runtime = await resolveProjectRuntime(process.cwd(), config);
|
|
64
|
+
let deploy = await getProjectDeployInfo(runtime);
|
|
65
|
+
|
|
66
|
+
if (!deploy.currentVersionDeployed) {
|
|
67
|
+
logger.info('Current project version is not deployed. Build and install current version.');
|
|
68
|
+
await deployCurrentVersion(runtime);
|
|
69
|
+
deploy = await getProjectDeployInfo(runtime);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (restart) {
|
|
73
|
+
if (deploy.isRunning) {
|
|
74
|
+
logger.info('Restart requested. Pause app instance first.');
|
|
75
|
+
await runtime.bridge.pause(runtime.pkgId);
|
|
76
|
+
deploy = await waitProjectNotRunning(runtime);
|
|
77
|
+
if (!deploy) {
|
|
78
|
+
throw new Error('Project app is still running after pause. Please check app status and try again.');
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
logger.info('Restart requested. Resume app instance.');
|
|
82
|
+
await runtime.bridge.resume(runtime.pkgId);
|
|
83
|
+
} else if (!deploy.isRunning) {
|
|
84
|
+
logger.info('App is not running. Resume app instance.');
|
|
85
|
+
await runtime.bridge.resume(runtime.pkgId);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
deploy = await waitProjectRunning(runtime);
|
|
89
|
+
if (!deploy) {
|
|
90
|
+
throw new Error('Project app is still not running. Please check app status and try again.');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
logger.info('Project app is running.');
|
|
94
|
+
const psOutput = await getProjectComposePs(runtime);
|
|
95
|
+
if (psOutput) {
|
|
96
|
+
console.log(psOutput);
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
}
|
package/lib/box/index.js
CHANGED
|
@@ -1,7 +1,42 @@
|
|
|
1
1
|
import logger from 'loglevel';
|
|
2
2
|
import shellapi from '../shellapi.js';
|
|
3
|
-
import
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import os from 'node:os';
|
|
6
|
+
import { findSshPublicKey, selectSshPublicKey, isMacOs, isWindows } from '../utils.js';
|
|
4
7
|
import { t } from '../i18n/index.js';
|
|
8
|
+
import { addSSHBox, listSSHBoxes, getDefaultSSHBox, setDefaultSSHBox, clearDefaultSSHBox, removeSSHBox } from './ssh_remote.js';
|
|
9
|
+
|
|
10
|
+
function getShellAPIConfigDir() {
|
|
11
|
+
const home = os.homedir();
|
|
12
|
+
let suffix = '/.config/hportal-client';
|
|
13
|
+
if (isMacOs) {
|
|
14
|
+
suffix = '/Library/Application Support/hportal-client';
|
|
15
|
+
} else if (isWindows) {
|
|
16
|
+
suffix = '\\AppData\\Roaming\\hportal-client';
|
|
17
|
+
}
|
|
18
|
+
return path.join(home, suffix);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function canUseShellApi() {
|
|
22
|
+
const configDir = getShellAPIConfigDir();
|
|
23
|
+
const addrFile = path.resolve(configDir, 'shellapi_addr');
|
|
24
|
+
const credFile = path.resolve(configDir, 'shellapi_cred');
|
|
25
|
+
return fs.existsSync(addrFile) && fs.existsSync(credFile);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function fetchShellBoxes() {
|
|
29
|
+
if (!canUseShellApi()) {
|
|
30
|
+
return [];
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
await shellapi.init();
|
|
34
|
+
return await shellapi.boxList();
|
|
35
|
+
} catch (error) {
|
|
36
|
+
logger.debug('query shell boxes failed', error);
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
}
|
|
5
40
|
|
|
6
41
|
export function boxCommand(box) {
|
|
7
42
|
let subCommands = [
|
|
@@ -9,14 +44,39 @@ export function boxCommand(box) {
|
|
|
9
44
|
command: 'switch <boxname>',
|
|
10
45
|
desc: t('lzc_cli.lib.box.index.switch_cmd_desc', '设置默认的盒子'),
|
|
11
46
|
handler: async ({ boxname }) => {
|
|
47
|
+
const target = String(boxname ?? '').trim();
|
|
48
|
+
if (!target) {
|
|
49
|
+
throw new Error('boxname is required');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const sshBoxes = listSSHBoxes();
|
|
53
|
+
if (sshBoxes.some((item) => item.box_name === target)) {
|
|
54
|
+
setDefaultSSHBox(target);
|
|
55
|
+
logger.info(`Default box switched: ${target}`);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (!canUseShellApi()) {
|
|
60
|
+
throw new Error(`Box "${target}" not found in SSH boxes and shellapi is unavailable`);
|
|
61
|
+
}
|
|
12
62
|
await shellapi.init();
|
|
13
|
-
await shellapi.setDefaultBox(
|
|
63
|
+
await shellapi.setDefaultBox(target);
|
|
64
|
+
clearDefaultSSHBox();
|
|
65
|
+
logger.info(`Default box switched: ${target}`);
|
|
14
66
|
},
|
|
15
67
|
},
|
|
16
68
|
{
|
|
17
69
|
command: 'default',
|
|
18
70
|
desc: t('lzc_cli.lib.box.index.default_cmd_desc', '输出当前默认的盒子名'),
|
|
19
71
|
handler: async () => {
|
|
72
|
+
const sshDefault = getDefaultSSHBox();
|
|
73
|
+
if (sshDefault) {
|
|
74
|
+
console.log(sshDefault.box_name);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (!canUseShellApi()) {
|
|
78
|
+
throw new Error('Default box not found. Run "lzc-cli box add-by-ssh <loginUser> <address>" first.');
|
|
79
|
+
}
|
|
20
80
|
await shellapi.init();
|
|
21
81
|
console.log(shellapi.boxname);
|
|
22
82
|
},
|
|
@@ -32,8 +92,17 @@ export function boxCommand(box) {
|
|
|
32
92
|
});
|
|
33
93
|
},
|
|
34
94
|
handler: async ({ verbose }) => {
|
|
35
|
-
await
|
|
36
|
-
const
|
|
95
|
+
const shellBoxes = await fetchShellBoxes();
|
|
96
|
+
const sshBoxes = listSSHBoxes();
|
|
97
|
+
const sshDefault = sshBoxes.find((item) => item.is_default_box);
|
|
98
|
+
const boxes = [
|
|
99
|
+
...shellBoxes.map((item) => ({
|
|
100
|
+
...item,
|
|
101
|
+
is_default_box: sshDefault ? false : item.is_default_box,
|
|
102
|
+
box_type: 'hclient',
|
|
103
|
+
})),
|
|
104
|
+
...sshBoxes,
|
|
105
|
+
];
|
|
37
106
|
if (boxes.length === 0) {
|
|
38
107
|
console.log(t('lzc_cli.lib.box.index.list_cmd_box_not_exist_tips', '没有找到任何盒子,赶紧添加一个吧!'));
|
|
39
108
|
return;
|
|
@@ -45,6 +114,7 @@ export function boxCommand(box) {
|
|
|
45
114
|
|
|
46
115
|
const list = boxes.map((b) => {
|
|
47
116
|
const info = {};
|
|
117
|
+
info[`${t('lzc_cli.lib.box.index.list_cmd_table_box_type', 'Type')}`] = b.box_type ?? 'hclient';
|
|
48
118
|
info[`${t('lzc_cli.lib.box.index.list_cmd_table_box_name', '名称')}`] = b.box_name;
|
|
49
119
|
info[`${t('lzc_cli.lib.box.index.list_cmd_table_status', '状态')}`] = b.status;
|
|
50
120
|
info[`${t('lzc_cli.lib.box.index.list_cmd_table_login_user', '登录用户')}`] = b.login_user;
|
|
@@ -59,6 +129,9 @@ export function boxCommand(box) {
|
|
|
59
129
|
command: 'add-public-key',
|
|
60
130
|
desc: t('lzc_cli.lib.box.index.add_public_key_cmd_desc', '添加public-key到开发者工具中'),
|
|
61
131
|
handler: async () => {
|
|
132
|
+
if (!canUseShellApi()) {
|
|
133
|
+
throw new Error('add-public-key requires shellapi connection');
|
|
134
|
+
}
|
|
62
135
|
await shellapi.init();
|
|
63
136
|
const keys = await findSshPublicKey();
|
|
64
137
|
const sshInfo = await selectSshPublicKey(keys);
|
|
@@ -75,6 +148,30 @@ export function boxCommand(box) {
|
|
|
75
148
|
);
|
|
76
149
|
},
|
|
77
150
|
},
|
|
151
|
+
{
|
|
152
|
+
command: 'add-by-ssh <loginUser> <address>',
|
|
153
|
+
desc: t('lzc_cli.lib.box.index.add_by_ssh_cmd_desc', '通过 ssh 配置远端直连目标'),
|
|
154
|
+
handler: async ({ loginUser, address }) => {
|
|
155
|
+
const remote = addSSHBox(loginUser, address);
|
|
156
|
+
logger.info(`SSH box added: ${remote.box_name} (${remote.ssh_target})`);
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
command: 'del <boxname>',
|
|
161
|
+
aliases: ['delete', 'rm', 'remove'],
|
|
162
|
+
desc: 'Delete ssh box config by name',
|
|
163
|
+
handler: async ({ boxname }) => {
|
|
164
|
+
const target = String(boxname ?? '').trim();
|
|
165
|
+
if (!target) {
|
|
166
|
+
throw new Error('boxname is required');
|
|
167
|
+
}
|
|
168
|
+
const removed = removeSSHBox(target);
|
|
169
|
+
if (!removed) {
|
|
170
|
+
throw new Error(`SSH box not found: ${target}`);
|
|
171
|
+
}
|
|
172
|
+
logger.info(`SSH box deleted: ${target}`);
|
|
173
|
+
},
|
|
174
|
+
},
|
|
78
175
|
];
|
|
79
176
|
box.command({
|
|
80
177
|
command: 'box',
|