@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.
- package/README.md +47 -7
- package/changelog.md +14 -0
- package/lib/app/apkshell.js +7 -44
- package/lib/app/index.js +178 -64
- package/lib/app/lpk_build.js +446 -61
- package/lib/app/lpk_build_images.js +749 -0
- package/lib/app/lpk_create.js +192 -45
- package/lib/app/lpk_create_generator.js +141 -13
- package/lib/app/lpk_devshell.js +33 -19
- package/lib/app/lpk_embed_images.js +257 -0
- package/lib/app/lpk_installer.js +17 -9
- package/lib/app/manifest_build.js +259 -0
- package/lib/app/project_cp.js +59 -0
- package/lib/app/project_deploy.js +58 -0
- package/lib/app/project_exec.js +82 -0
- package/lib/app/project_info.js +106 -0
- package/lib/app/project_log.js +62 -0
- package/lib/app/project_runtime.js +356 -0
- package/lib/app/project_start.js +95 -0
- package/lib/app/project_sync.js +499 -0
- package/lib/appstore/apkshell.js +50 -0
- package/lib/box/index.js +101 -4
- package/lib/box/ssh_remote.js +259 -0
- package/lib/build_remote.js +21 -0
- package/lib/debug_bridge.js +891 -83
- package/lib/docker/index.js +30 -10
- package/lib/i18n/locales/en/translation.json +262 -255
- package/lib/i18n/locales/zh/translation.json +262 -255
- package/lib/lpk/core.js +488 -0
- package/lib/lpk/index.js +210 -0
- package/lib/migrate/index.js +52 -0
- package/lib/package_info.js +135 -0
- package/lib/shellapi.js +35 -1
- package/lib/sig/core.js +254 -0
- package/lib/sig/index.js +88 -0
- package/lib/utils.js +94 -15
- package/package.json +3 -3
- package/scripts/cli.js +6 -0
- package/scripts/smoke/frontend-dev-entry.mjs +104 -0
- package/scripts/smoke/template-project.mjs +311 -0
- package/template/_lpk/README.md +15 -4
- package/template/_lpk/gui-vnc.manifest.yml.in +18 -0
- package/template/_lpk/hello-vue.manifest.yml.in +38 -0
- package/template/_lpk/manifest.yml.in +4 -11
- package/template/_lpk/package.yml.in +7 -0
- package/template/_lpk/todolist-golang.manifest.yml.in +30 -0
- package/template/_lpk/todolist-java.manifest.yml.in +29 -0
- package/template/_lpk/todolist-python.manifest.yml.in +37 -0
- package/template/_lpk/todolist-serverless.manifest.yml.in +38 -0
- package/template/_lpk/vue.lzc-build.yml.in +0 -44
- package/template/blank/lzc-build.dev.yml +4 -0
- package/template/blank/lzc-build.yml +24 -41
- package/template/blank/lzc-manifest.yml +7 -9
- package/template/blank/package.yml +7 -0
- package/template/golang/Dockerfile +19 -0
- package/template/golang/Dockerfile.dev +20 -0
- package/template/golang/README.md +44 -0
- package/template/golang/_gitignore +3 -0
- package/template/golang/_lzcdevignore +21 -0
- package/template/golang/go.mod +3 -0
- package/template/golang/lzc-build.dev.yml +12 -0
- package/template/golang/lzc-build.yml +16 -0
- package/template/golang/lzc-icon.png +0 -0
- package/template/golang/main.go +252 -0
- package/template/golang/manifest.dev.page.js +24 -0
- package/template/golang/run.sh +10 -0
- package/template/golang/web/index.html +238 -0
- package/template/gui-vnc/README.md +23 -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.dev.yml +4 -0
- package/template/gui-vnc/lzc-build.yml +18 -0
- package/template/gui-vnc/lzc-icon.png +0 -0
- package/template/python/Dockerfile +15 -0
- package/template/python/Dockerfile.dev +18 -0
- package/template/python/README.md +50 -0
- package/template/python/_gitignore +3 -0
- package/template/python/_lzcdevignore +21 -0
- package/template/python/app.py +110 -0
- package/template/python/lzc-build.dev.yml +12 -0
- package/template/python/lzc-build.yml +16 -0
- package/template/python/lzc-icon.png +0 -0
- package/template/python/manifest.dev.page.js +25 -0
- package/template/python/requirements.txt +1 -0
- package/template/python/run.sh +14 -0
- package/template/python/web/index.html +238 -0
- package/template/springboot/Dockerfile +20 -0
- package/template/springboot/Dockerfile.dev +20 -0
- package/template/springboot/README.md +44 -0
- package/template/springboot/_gitignore +3 -0
- package/template/springboot/_lzcdevignore +21 -0
- package/template/springboot/lzc-build.dev.yml +12 -0
- package/template/springboot/lzc-build.yml +16 -0
- package/template/springboot/lzc-icon.png +0 -0
- package/template/springboot/manifest.dev.page.js +24 -0
- package/template/springboot/pom.xml +38 -0
- package/template/springboot/run.sh +10 -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 +18 -21
- package/template/vue/lzc-build.dev.yml +7 -0
- package/template/vue/lzc-build.yml +30 -43
- package/template/vue/manifest.dev.page.js +50 -0
- package/template/vue/src/App.vue +36 -25
- package/template/vue/src/style.css +106 -49
- package/template/vue-minidb/README.md +26 -0
- package/template/vue-minidb/_gitignore +25 -0
- package/template/vue-minidb/index.html +13 -0
- package/template/vue-minidb/lzc-build.dev.yml +7 -0
- package/template/vue-minidb/lzc-build.yml +46 -0
- package/template/vue-minidb/lzc-icon.png +0 -0
- package/template/vue-minidb/manifest.dev.page.js +50 -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,499 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import chokidar from 'chokidar';
|
|
4
|
+
import debounce from 'lodash.debounce';
|
|
5
|
+
import ignore from 'ignore';
|
|
6
|
+
import spawn from 'cross-spawn';
|
|
7
|
+
import logger from 'loglevel';
|
|
8
|
+
import { checkRsync, contextDirname, isDebugMode, isWindows, resolveDomain } from '../utils.js';
|
|
9
|
+
import { addProjectTargetOptions, resolveProjectRuntime, getProjectDeployInfo } from './project_runtime.js';
|
|
10
|
+
|
|
11
|
+
export const DEFAULT_PROJECT_SYNC_TARGET = '/lzcapp/cache/project-mirror';
|
|
12
|
+
const PROJECT_SYNC_CACHE_ROOT = '/lzcapp/cache';
|
|
13
|
+
const RSYNC_PASSWORD = 'fakefakefake';
|
|
14
|
+
const RSYNC_PORT = '874';
|
|
15
|
+
const LZCDEVIGNORE_FILE = '.lzcdevignore';
|
|
16
|
+
const GITIGNORE_FILE = '.gitignore';
|
|
17
|
+
const DEFAULT_LZCDEVIGNORE_RULES = [
|
|
18
|
+
'.git',
|
|
19
|
+
'.git/**',
|
|
20
|
+
'node_modules',
|
|
21
|
+
'node_modules/**',
|
|
22
|
+
'.venv',
|
|
23
|
+
'.venv/**',
|
|
24
|
+
'dist',
|
|
25
|
+
'dist/**',
|
|
26
|
+
'build',
|
|
27
|
+
'build/**',
|
|
28
|
+
'__pycache__',
|
|
29
|
+
'__pycache__/**',
|
|
30
|
+
'.idea',
|
|
31
|
+
'.idea/**',
|
|
32
|
+
'.vscode',
|
|
33
|
+
'.vscode/**',
|
|
34
|
+
'.DS_Store',
|
|
35
|
+
'.lzc-cli-*',
|
|
36
|
+
'.lzc-cli-*/**',
|
|
37
|
+
'*.lpk',
|
|
38
|
+
'*.lpk.tar',
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
function normalizePosixPath(value) {
|
|
42
|
+
return String(value ?? '')
|
|
43
|
+
.replace(/\\/g, '/')
|
|
44
|
+
.replace(/^\.\//, '')
|
|
45
|
+
.replace(/^\/+/, '')
|
|
46
|
+
.trim();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function loadIgnoreFile(rootDir, filename) {
|
|
50
|
+
const filePath = path.join(rootDir, filename);
|
|
51
|
+
if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) {
|
|
52
|
+
return [];
|
|
53
|
+
}
|
|
54
|
+
return fs
|
|
55
|
+
.readFileSync(filePath, 'utf8')
|
|
56
|
+
.split(/\r?\n/)
|
|
57
|
+
.map((line) => line.trim())
|
|
58
|
+
.filter((line) => line && !line.startsWith('#'));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function createIgnoreMatcher(rootDir) {
|
|
62
|
+
const ig = ignore();
|
|
63
|
+
ig.add(loadIgnoreFile(rootDir, LZCDEVIGNORE_FILE));
|
|
64
|
+
return ig;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function createWatchIgnored(rootDir) {
|
|
68
|
+
const ig = createIgnoreMatcher(rootDir);
|
|
69
|
+
return (candidatePath) => {
|
|
70
|
+
const relPath = normalizePosixPath(path.relative(rootDir, candidatePath));
|
|
71
|
+
if (!relPath || relPath.startsWith('..')) {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
return ig.ignores(relPath);
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function ensureLzcdevignore(rootDir) {
|
|
79
|
+
const filePath = path.join(rootDir, LZCDEVIGNORE_FILE);
|
|
80
|
+
if (fs.existsSync(filePath)) {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
const mergedRules = [];
|
|
84
|
+
const seen = new Set();
|
|
85
|
+
for (const rule of [...DEFAULT_LZCDEVIGNORE_RULES, ...loadIgnoreFile(rootDir, GITIGNORE_FILE)]) {
|
|
86
|
+
const normalized = String(rule ?? '').trim();
|
|
87
|
+
if (!normalized || seen.has(normalized)) {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
seen.add(normalized);
|
|
91
|
+
mergedRules.push(normalized);
|
|
92
|
+
}
|
|
93
|
+
const body = `${mergedRules.join('\n')}\n`;
|
|
94
|
+
fs.writeFileSync(filePath, body);
|
|
95
|
+
logger.warn(`Created ${LZCDEVIGNORE_FILE} from built-in defaults${fs.existsSync(path.join(rootDir, GITIGNORE_FILE)) ? ` and ${GITIGNORE_FILE}` : ''}: ${filePath}`);
|
|
96
|
+
logger.warn(`Review ${LZCDEVIGNORE_FILE} and adjust sync rules there if needed.`);
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function resolveSyncCacheSubpath(targetDir) {
|
|
101
|
+
const normalized = String(targetDir || DEFAULT_PROJECT_SYNC_TARGET)
|
|
102
|
+
.replace(/\\/g, '/')
|
|
103
|
+
.replace(/\/+$/, '')
|
|
104
|
+
.trim();
|
|
105
|
+
if (!normalized) {
|
|
106
|
+
throw new Error('project sync target is empty');
|
|
107
|
+
}
|
|
108
|
+
if (normalized === PROJECT_SYNC_CACHE_ROOT) {
|
|
109
|
+
return '';
|
|
110
|
+
}
|
|
111
|
+
if (!normalized.startsWith(`${PROJECT_SYNC_CACHE_ROOT}/`)) {
|
|
112
|
+
throw new Error(`project sync target must stay under ${PROJECT_SYNC_CACHE_ROOT}`);
|
|
113
|
+
}
|
|
114
|
+
return normalizePosixPath(path.posix.relative(PROJECT_SYNC_CACHE_ROOT, normalized));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function resolveRsyncCommand() {
|
|
118
|
+
if (isWindows) {
|
|
119
|
+
return path.join(contextDirname(import.meta.url), '..', '..', 'template', '_lpk', 'win-rsync', 'rsync.exe');
|
|
120
|
+
}
|
|
121
|
+
return 'rsync';
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function formatRsyncHost(host) {
|
|
125
|
+
return String(host).includes(':') ? `[${host}]` : String(host);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function resolveRsyncHost(runtime) {
|
|
129
|
+
if (runtime.bridge.isBuildRemoteMode()) {
|
|
130
|
+
return runtime.bridge.buildRemote.sshHost;
|
|
131
|
+
}
|
|
132
|
+
return resolveDomain(runtime.bridge.domain);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function resolveRsyncUID(runtime) {
|
|
136
|
+
return runtime.bridge.resolveCurrentUID();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function buildRsyncModulePath(runtime, uid, targetDir) {
|
|
140
|
+
let modulePath = runtime.pkgId;
|
|
141
|
+
if (runtime.userApp) {
|
|
142
|
+
modulePath += `/${uid}`;
|
|
143
|
+
}
|
|
144
|
+
const syncSubpath = resolveSyncCacheSubpath(targetDir);
|
|
145
|
+
if (syncSubpath) {
|
|
146
|
+
modulePath += `/${syncSubpath}`;
|
|
147
|
+
}
|
|
148
|
+
return modulePath;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function buildRsyncArgs(runtime, uid, host, rootDir, targetDir, sourceDir, deleteMode, dryRun) {
|
|
152
|
+
const args = ['--recursive', '--links', '--times', '--perms', '--omit-dir-times', '--human-readable', '--itemize-changes', '--compress'];
|
|
153
|
+
if (isDebugMode()) {
|
|
154
|
+
args.push('-P');
|
|
155
|
+
}
|
|
156
|
+
if (deleteMode) {
|
|
157
|
+
args.push('--delete');
|
|
158
|
+
}
|
|
159
|
+
if (dryRun) {
|
|
160
|
+
args.push('--dry-run');
|
|
161
|
+
}
|
|
162
|
+
const ignoreFile = path.join(rootDir, LZCDEVIGNORE_FILE);
|
|
163
|
+
if (fs.existsSync(ignoreFile)) {
|
|
164
|
+
args.push(`--exclude-from=${ignoreFile}`);
|
|
165
|
+
if (deleteMode) {
|
|
166
|
+
args.push('--delete-excluded');
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if (sourceDir) {
|
|
170
|
+
args.push('--relative');
|
|
171
|
+
}
|
|
172
|
+
const modulePath = buildRsyncModulePath(runtime, uid, targetDir);
|
|
173
|
+
const dest = `rsync://${uid}@${formatRsyncHost(host)}:${RSYNC_PORT}/lzcapp_cache/${modulePath}/`;
|
|
174
|
+
const sourceArg = sourceDir ? `./${sourceDir}/` : './';
|
|
175
|
+
args.push(sourceArg, dest);
|
|
176
|
+
return args;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function formatRsyncLine(line, dryRun) {
|
|
180
|
+
const text = String(line ?? '').trim();
|
|
181
|
+
if (!text) {
|
|
182
|
+
return '';
|
|
183
|
+
}
|
|
184
|
+
if (text === 'sending incremental file list') {
|
|
185
|
+
return '';
|
|
186
|
+
}
|
|
187
|
+
if (text.startsWith('sent ') || text.startsWith('total size is ')) {
|
|
188
|
+
return '';
|
|
189
|
+
}
|
|
190
|
+
if (text.startsWith('*deleting ')) {
|
|
191
|
+
const target = text.slice('*deleting '.length).trim();
|
|
192
|
+
return `${dryRun ? 'Would delete' : 'Delete'} ${target}`;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const match = text.match(/^(\S+)\s+(.+)$/);
|
|
196
|
+
if (!match) {
|
|
197
|
+
return text;
|
|
198
|
+
}
|
|
199
|
+
const [, flags, target] = match;
|
|
200
|
+
const pathText = target.trim();
|
|
201
|
+
const actionPrefix = dryRun ? 'Would ' : '';
|
|
202
|
+
if (flags.startsWith('cd')) {
|
|
203
|
+
return `${actionPrefix}create directory ${pathText}`;
|
|
204
|
+
}
|
|
205
|
+
if (flags.startsWith('<') || flags.startsWith('>') || flags.startsWith('c')) {
|
|
206
|
+
return `${actionPrefix}upload ${pathText}`;
|
|
207
|
+
}
|
|
208
|
+
if (flags.startsWith('.')) {
|
|
209
|
+
return `${actionPrefix}update ${pathText}`;
|
|
210
|
+
}
|
|
211
|
+
return `${actionPrefix}sync ${pathText}`;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function runRsyncProcess(command, args, cwd, dryRun) {
|
|
215
|
+
return new Promise((resolve, reject) => {
|
|
216
|
+
const child = spawn(command, args, {
|
|
217
|
+
cwd,
|
|
218
|
+
env: { ...process.env, RSYNC_PASSWORD },
|
|
219
|
+
shell: false,
|
|
220
|
+
stdio: ['ignore', 'pipe', 'inherit'],
|
|
221
|
+
});
|
|
222
|
+
let stdoutBuffer = '';
|
|
223
|
+
let hasChanges = false;
|
|
224
|
+
const flushLines = (force = false) => {
|
|
225
|
+
const lines = stdoutBuffer.split(/\r?\n/);
|
|
226
|
+
if (!force) {
|
|
227
|
+
stdoutBuffer = lines.pop() ?? '';
|
|
228
|
+
} else {
|
|
229
|
+
stdoutBuffer = '';
|
|
230
|
+
}
|
|
231
|
+
for (const line of lines) {
|
|
232
|
+
const formatted = formatRsyncLine(line, dryRun);
|
|
233
|
+
if (!formatted) {
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
hasChanges = true;
|
|
237
|
+
logger.info(formatted);
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
child.stdout?.on('data', (chunk) => {
|
|
241
|
+
stdoutBuffer += String(chunk ?? '');
|
|
242
|
+
flushLines(false);
|
|
243
|
+
});
|
|
244
|
+
child.on('error', (error) => reject(error));
|
|
245
|
+
child.on('close', (code) => {
|
|
246
|
+
flushLines(true);
|
|
247
|
+
if (code === 0) {
|
|
248
|
+
resolve(hasChanges);
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
reject(new Error(`rsync exited with code ${code}`));
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
async function ensureProjectDeployed(runtime) {
|
|
257
|
+
const deploy = await getProjectDeployInfo(runtime);
|
|
258
|
+
if (!deploy.deployed) {
|
|
259
|
+
throw new Error('Project app is not deployed. Run "lzc-cli project deploy" first.');
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function dirCovers(ancestor, target) {
|
|
264
|
+
if (ancestor === '') {
|
|
265
|
+
return true;
|
|
266
|
+
}
|
|
267
|
+
return target === ancestor || target.startsWith(`${ancestor}/`);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function compressDirs(dirs) {
|
|
271
|
+
const normalized = [...new Set(dirs.map((item) => normalizePosixPath(item)).filter((item) => item || item === ''))].sort((a, b) => {
|
|
272
|
+
if (a.length !== b.length) {
|
|
273
|
+
return a.length - b.length;
|
|
274
|
+
}
|
|
275
|
+
return a.localeCompare(b);
|
|
276
|
+
});
|
|
277
|
+
const result = [];
|
|
278
|
+
for (const dir of normalized) {
|
|
279
|
+
if (result.some((item) => dirCovers(item, dir))) {
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
result.push(dir);
|
|
283
|
+
}
|
|
284
|
+
return result;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function parentDirOf(relPath) {
|
|
288
|
+
const dir = path.posix.dirname(relPath);
|
|
289
|
+
return dir === '.' ? '' : normalizePosixPath(dir);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function collectWatchChange(state, eventName, relPath, deleteEnabled) {
|
|
293
|
+
if (!relPath) {
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
if (relPath === LZCDEVIGNORE_FILE) {
|
|
297
|
+
state.fullSyncRequested = true;
|
|
298
|
+
state.reloadWatcherRequested = true;
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
const parentDir = parentDirOf(relPath);
|
|
302
|
+
if ((eventName === 'unlink' || eventName === 'unlinkDir') && deleteEnabled) {
|
|
303
|
+
state.deleteDirs.add(parentDir);
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
if (eventName === 'add' || eventName === 'change' || eventName === 'addDir') {
|
|
307
|
+
state.syncDirs.add(parentDir);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
async function runSyncPath(runtime, rootDir, options, sourceDir, deleteMode) {
|
|
312
|
+
const [uid, host] = await Promise.all([resolveRsyncUID(runtime), resolveRsyncHost(runtime)]);
|
|
313
|
+
const rsyncCmd = resolveRsyncCommand();
|
|
314
|
+
const rsyncArgs = buildRsyncArgs(runtime, uid, host, rootDir, options.target, sourceDir, deleteMode, options.dryRun);
|
|
315
|
+
logger.debug('project sync rsync:', rsyncCmd, rsyncArgs.join(' '));
|
|
316
|
+
return runRsyncProcess(rsyncCmd, rsyncArgs, rootDir, options.dryRun);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
async function runInitialSync(runtime, rootDir, options) {
|
|
320
|
+
await ensureProjectDeployed(runtime);
|
|
321
|
+
const hasChanges = await runSyncPath(runtime, rootDir, options, '', options.delete);
|
|
322
|
+
if (!hasChanges) {
|
|
323
|
+
logger.info('project sync: no changes');
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
async function runDirtySync(runtime, rootDir, options, state) {
|
|
328
|
+
await ensureProjectDeployed(runtime);
|
|
329
|
+
if (state.fullSyncRequested) {
|
|
330
|
+
state.fullSyncRequested = false;
|
|
331
|
+
const hasChanges = await runSyncPath(runtime, rootDir, options, '', options.delete);
|
|
332
|
+
if (!hasChanges) {
|
|
333
|
+
logger.info('project sync: no changes');
|
|
334
|
+
}
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
const pendingDeleteDirs = options.delete ? compressDirs([...state.deleteDirs]) : [];
|
|
338
|
+
const pendingSyncDirs = compressDirs([...state.syncDirs]).filter((item) => !pendingDeleteDirs.some((dir) => dirCovers(dir, item)));
|
|
339
|
+
state.deleteDirs.clear();
|
|
340
|
+
state.syncDirs.clear();
|
|
341
|
+
let hasChanges = false;
|
|
342
|
+
for (const dir of pendingDeleteDirs) {
|
|
343
|
+
hasChanges = (await runSyncPath(runtime, rootDir, options, dir, true)) || hasChanges;
|
|
344
|
+
}
|
|
345
|
+
for (const dir of pendingSyncDirs) {
|
|
346
|
+
hasChanges = (await runSyncPath(runtime, rootDir, options, dir, false)) || hasChanges;
|
|
347
|
+
}
|
|
348
|
+
if (!hasChanges && pendingDeleteDirs.length === 0 && pendingSyncDirs.length === 0) {
|
|
349
|
+
logger.info('project sync: no changes');
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function createWatchState() {
|
|
354
|
+
return {
|
|
355
|
+
syncDirs: new Set(),
|
|
356
|
+
deleteDirs: new Set(),
|
|
357
|
+
fullSyncRequested: false,
|
|
358
|
+
reloadWatcherRequested: false,
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function hasPendingChanges(state) {
|
|
363
|
+
return state.fullSyncRequested || state.syncDirs.size > 0 || state.deleteDirs.size > 0;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function waitWatcherReady(watcher) {
|
|
367
|
+
return new Promise((resolve, reject) => {
|
|
368
|
+
watcher.once('ready', resolve);
|
|
369
|
+
watcher.once('error', reject);
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function attachWatchHandlers(watcher, rootDir, options, state, trigger) {
|
|
374
|
+
watcher.on('all', (eventName, filePath) => {
|
|
375
|
+
const relPath = normalizePosixPath(path.relative(rootDir, filePath));
|
|
376
|
+
if (!relPath || relPath.startsWith('..')) {
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
collectWatchChange(state, eventName, relPath, options.delete);
|
|
380
|
+
trigger();
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
export function projectSyncCommand() {
|
|
385
|
+
return {
|
|
386
|
+
command: 'sync',
|
|
387
|
+
desc: 'Mirror local project files into project app cache',
|
|
388
|
+
builder: (args) => {
|
|
389
|
+
addProjectTargetOptions(args);
|
|
390
|
+
args.option('watch', {
|
|
391
|
+
describe: 'Watch local files and keep mirroring changes',
|
|
392
|
+
type: 'boolean',
|
|
393
|
+
default: false,
|
|
394
|
+
});
|
|
395
|
+
args.option('delete', {
|
|
396
|
+
describe: 'Mirror target directory by deleting files removed locally',
|
|
397
|
+
type: 'boolean',
|
|
398
|
+
default: true,
|
|
399
|
+
});
|
|
400
|
+
args.option('target', {
|
|
401
|
+
describe: 'Target directory inside app cache',
|
|
402
|
+
type: 'string',
|
|
403
|
+
default: DEFAULT_PROJECT_SYNC_TARGET,
|
|
404
|
+
});
|
|
405
|
+
args.option('dry-run', {
|
|
406
|
+
describe: 'Show sync plan without changing remote files',
|
|
407
|
+
type: 'boolean',
|
|
408
|
+
default: false,
|
|
409
|
+
});
|
|
410
|
+
},
|
|
411
|
+
handler: async ({ config, dev, release, watch, delete: shouldDelete, target, dryRun }) => {
|
|
412
|
+
await checkRsync();
|
|
413
|
+
const runtime = await resolveProjectRuntime(process.cwd(), { config, dev, release, command: 'lzc-cli project sync' });
|
|
414
|
+
logger.info(`Build config: ${runtime.configPath}`);
|
|
415
|
+
const rootDir = runtime.projectCwd;
|
|
416
|
+
const options = {
|
|
417
|
+
target: String(target || DEFAULT_PROJECT_SYNC_TARGET),
|
|
418
|
+
delete: !!shouldDelete,
|
|
419
|
+
dryRun: !!dryRun,
|
|
420
|
+
};
|
|
421
|
+
const state = createWatchState();
|
|
422
|
+
|
|
423
|
+
ensureLzcdevignore(rootDir);
|
|
424
|
+
resolveSyncCacheSubpath(options.target);
|
|
425
|
+
logger.info(`Sync root: ${rootDir}`);
|
|
426
|
+
logger.info(`Sync target: ${options.target}`);
|
|
427
|
+
|
|
428
|
+
if (!watch) {
|
|
429
|
+
await runInitialSync(runtime, rootDir, options);
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
let watcher = null;
|
|
434
|
+
let running = false;
|
|
435
|
+
let pending = false;
|
|
436
|
+
const triggerSync = async () => {
|
|
437
|
+
if (running) {
|
|
438
|
+
pending = true;
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
running = true;
|
|
442
|
+
try {
|
|
443
|
+
await runDirtySync(runtime, rootDir, options, state);
|
|
444
|
+
if (state.reloadWatcherRequested) {
|
|
445
|
+
state.reloadWatcherRequested = false;
|
|
446
|
+
await watcher.close();
|
|
447
|
+
watcher = createWatcher();
|
|
448
|
+
await waitWatcherReady(watcher);
|
|
449
|
+
logger.info('project sync watch reloaded after .lzcdevignore change.');
|
|
450
|
+
}
|
|
451
|
+
} catch (error) {
|
|
452
|
+
logger.error(`project sync failed: ${error.message}`);
|
|
453
|
+
} finally {
|
|
454
|
+
running = false;
|
|
455
|
+
if (pending) {
|
|
456
|
+
pending = false;
|
|
457
|
+
await triggerSync();
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
};
|
|
461
|
+
|
|
462
|
+
const debouncedTrigger = debounce(() => {
|
|
463
|
+
void triggerSync();
|
|
464
|
+
}, 300);
|
|
465
|
+
|
|
466
|
+
const createWatcher = () => {
|
|
467
|
+
const instance = chokidar.watch(rootDir, {
|
|
468
|
+
ignoreInitial: true,
|
|
469
|
+
ignored: createWatchIgnored(rootDir),
|
|
470
|
+
awaitWriteFinish: {
|
|
471
|
+
stabilityThreshold: 150,
|
|
472
|
+
pollInterval: 50,
|
|
473
|
+
},
|
|
474
|
+
});
|
|
475
|
+
attachWatchHandlers(instance, rootDir, options, state, debouncedTrigger);
|
|
476
|
+
return instance;
|
|
477
|
+
};
|
|
478
|
+
|
|
479
|
+
watcher = createWatcher();
|
|
480
|
+
await waitWatcherReady(watcher);
|
|
481
|
+
await runInitialSync(runtime, rootDir, options);
|
|
482
|
+
if (hasPendingChanges(state)) {
|
|
483
|
+
await runDirtySync(runtime, rootDir, options, state);
|
|
484
|
+
}
|
|
485
|
+
logger.info('project sync watch started. Press Ctrl+C to stop.');
|
|
486
|
+
await new Promise((resolve) => {
|
|
487
|
+
const shutdown = async () => {
|
|
488
|
+
debouncedTrigger.cancel();
|
|
489
|
+
if (watcher) {
|
|
490
|
+
await watcher.close();
|
|
491
|
+
}
|
|
492
|
+
resolve();
|
|
493
|
+
};
|
|
494
|
+
process.once('SIGINT', shutdown);
|
|
495
|
+
process.once('SIGTERM', shutdown);
|
|
496
|
+
});
|
|
497
|
+
},
|
|
498
|
+
};
|
|
499
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { Blob } from 'node:buffer';
|
|
4
|
+
import logger from 'loglevel';
|
|
5
|
+
import fetch, { FormData } from 'node-fetch';
|
|
6
|
+
import { t } from '../i18n/index.js';
|
|
7
|
+
import { appStoreServerUrl } from './env.js';
|
|
8
|
+
|
|
9
|
+
// axios@1.7.7 依赖的 form-data 使用了 util.isArray 导致 node 会输出弃用警告 (所以将逻辑迁移避免依赖)
|
|
10
|
+
export async function triggerApk(id, name, iconPath) {
|
|
11
|
+
if (!id) {
|
|
12
|
+
logger.error(t('lzc_cli.lib.appstore.apkshell.trigger_apk_empty_appid', 'Appid 为必填项!'));
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const appName = name || t('lzc_cli.lib.appstore.apkshell.trigger_apk_default_app_name', '懒猫应用');
|
|
17
|
+
const form = new FormData();
|
|
18
|
+
form.append('app_id', id);
|
|
19
|
+
form.append('app_name', appName);
|
|
20
|
+
|
|
21
|
+
if (iconPath && fs.existsSync(iconPath)) {
|
|
22
|
+
const iconBuffer = fs.readFileSync(iconPath);
|
|
23
|
+
form.append('app_icon', new Blob([iconBuffer]), path.basename(iconPath));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const controller = new AbortController();
|
|
27
|
+
const timer = setTimeout(() => controller.abort(), 5000);
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
const resp = await fetch(`${appStoreServerUrl}/api/trigger_latest_for_app`, {
|
|
31
|
+
method: 'POST',
|
|
32
|
+
body: form,
|
|
33
|
+
signal: controller.signal,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
logger.debug('triggerApk resp:', resp);
|
|
37
|
+
if (resp.status == 304) {
|
|
38
|
+
logger.debug(t('lzc_cli.lib.appstore.apkshell.trigger_apk_build_tips', `APK构建任务已创建成功,如需使用安卓端,请耐心等待1分钟左右`));
|
|
39
|
+
} else if (resp.status <= 201) {
|
|
40
|
+
logger.info(t('lzc_cli.lib.appstore.apkshell.trigger_apk_build_ok_tips', `APK构建任务已创建成功,如需使用安卓端,请耐心等待1分钟左右`));
|
|
41
|
+
} else if (resp.status >= 400) {
|
|
42
|
+
logger.debug(t('lzc_cli.lib.appstore.apkshell.trigger_apk_build_failed', '请求按钮应用出错:'), await resp.text());
|
|
43
|
+
throw t('lzc_cli.lib.appstore.apkshell.trigger_apk_build_failed_tips', `请求生成应用出错! 使用 --apk=n 停止生成APK`);
|
|
44
|
+
}
|
|
45
|
+
} catch (error) {
|
|
46
|
+
logger.debug(error);
|
|
47
|
+
} finally {
|
|
48
|
+
clearTimeout(timer);
|
|
49
|
+
}
|
|
50
|
+
}
|
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',
|