@lazycatcloud/lzc-cli 2.0.0-pre.0 → 2.0.0-pre.2
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 +46 -7
- package/changelog.md +56 -19
- package/lib/app/apkshell.js +7 -44
- package/lib/app/index.js +5 -1
- package/lib/app/lpk_build.js +266 -56
- package/lib/app/lpk_build_images.js +424 -229
- package/lib/app/lpk_build_images_local.js +425 -0
- package/lib/app/lpk_build_images_pack_local.js +409 -0
- package/lib/app/lpk_create.js +158 -83
- package/lib/app/lpk_create_generator.js +35 -42
- package/lib/app/lpk_devshell.js +6 -2
- package/lib/app/lpk_installer.js +4 -3
- package/lib/app/manifest_build.js +259 -0
- package/lib/app/project_cp.js +5 -10
- package/lib/app/project_deploy.js +80 -11
- package/lib/app/project_exec.js +48 -11
- package/lib/app/project_info.js +59 -59
- package/lib/app/project_log.js +5 -10
- package/lib/app/project_runtime.js +113 -18
- package/lib/app/project_start.js +6 -11
- package/lib/app/project_sync.js +499 -0
- package/lib/appstore/apkshell.js +50 -0
- package/lib/appstore/publish.js +54 -15
- package/lib/build_remote.js +0 -1
- package/lib/config/index.js +1 -1
- package/lib/debug_bridge.js +217 -47
- package/lib/i18n/locales/en/translation.json +262 -262
- package/lib/i18n/locales/zh/translation.json +262 -262
- package/lib/lpk/core.js +2 -1
- 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 +2 -2
- package/lib/utils.js +92 -15
- package/package.json +89 -89
- package/scripts/cli.js +2 -0
- package/scripts/smoke/frontend-dev-entry.mjs +104 -0
- package/scripts/smoke/template-project.mjs +311 -0
- package/template/_lpk/README.md +6 -3
- package/template/_lpk/gui-vnc.manifest.yml.in +0 -9
- package/template/_lpk/hello-vue.manifest.yml.in +38 -0
- package/template/_lpk/manifest.yml.in +0 -9
- package/template/_lpk/package.yml.in +7 -0
- package/template/_lpk/todolist-golang.manifest.yml.in +23 -9
- package/template/_lpk/todolist-java.manifest.yml.in +23 -9
- package/template/_lpk/todolist-python.manifest.yml.in +31 -9
- package/template/_lpk/todolist-serverless.manifest.yml.in +38 -0
- package/template/blank/lzc-build.dev.yml +4 -0
- package/template/blank/lzc-build.yml +0 -2
- package/template/blank/lzc-manifest.yml +3 -12
- package/template/blank/package.yml +7 -0
- package/template/golang/Dockerfile +1 -1
- package/template/golang/Dockerfile.dev +20 -0
- package/template/golang/README.md +22 -11
- package/template/golang/_lzcdevignore +21 -0
- package/template/golang/lzc-build.dev.yml +12 -0
- package/template/golang/lzc-build.yml +0 -5
- package/template/golang/main.go +1 -1
- package/template/golang/manifest.dev.page.js +24 -0
- package/template/golang/run.sh +7 -0
- package/template/gui-vnc/README.md +5 -1
- package/template/gui-vnc/lzc-build.dev.yml +4 -0
- package/template/gui-vnc/lzc-build.yml +0 -5
- package/template/python/Dockerfile +2 -2
- package/template/python/Dockerfile.dev +18 -0
- package/template/python/README.md +28 -11
- package/template/python/_lzcdevignore +21 -0
- package/template/python/app.py +1 -1
- package/template/python/lzc-build.dev.yml +12 -0
- package/template/python/lzc-build.yml +0 -5
- package/template/python/manifest.dev.page.js +25 -0
- package/template/python/run.sh +12 -1
- package/template/springboot/Dockerfile +1 -1
- package/template/springboot/Dockerfile.dev +20 -0
- package/template/springboot/README.md +22 -11
- package/template/springboot/_lzcdevignore +21 -0
- package/template/springboot/lzc-build.dev.yml +12 -0
- package/template/springboot/lzc-build.yml +0 -5
- package/template/springboot/manifest.dev.page.js +24 -0
- package/template/springboot/run.sh +7 -0
- package/template/vue/README.md +14 -27
- package/template/vue/_gitignore +0 -1
- package/template/vue/lzc-build.dev.yml +7 -0
- package/template/vue/lzc-build.yml +0 -2
- package/template/vue/manifest.dev.page.js +50 -0
- package/template/vue/src/App.vue +1 -1
- package/template/vue-minidb/README.md +11 -19
- package/template/vue-minidb/_gitignore +0 -1
- package/template/vue-minidb/lzc-build.dev.yml +7 -0
- package/template/vue-minidb/lzc-build.yml +0 -2
- package/template/vue-minidb/manifest.dev.page.js +50 -0
- package/template/blank/_gitignore +0 -1
|
@@ -7,12 +7,52 @@ import { isUserApp } from '../utils.js';
|
|
|
7
7
|
import { resolveBuildRemoteFromFile, DEFAULT_BUILD_CONFIG_FILE } from '../build_remote.js';
|
|
8
8
|
|
|
9
9
|
export const DEFAULT_DEPLOY_BUILD_CONFIG_FILE = 'lzc-build.dev.yml';
|
|
10
|
-
export const DEFAULT_RELEASE_BUILD_CONFIG_FILE =
|
|
10
|
+
export const DEFAULT_RELEASE_BUILD_CONFIG_FILE = DEFAULT_BUILD_CONFIG_FILE;
|
|
11
11
|
|
|
12
12
|
function isFile(pathname) {
|
|
13
13
|
return fs.existsSync(pathname) && fs.statSync(pathname).isFile();
|
|
14
14
|
}
|
|
15
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
|
+
|
|
16
56
|
export function resolveBuildConfigPath(startDir = process.cwd(), buildConfigFile = DEFAULT_BUILD_CONFIG_FILE) {
|
|
17
57
|
const configName = String(buildConfigFile ?? DEFAULT_BUILD_CONFIG_FILE).trim() || DEFAULT_BUILD_CONFIG_FILE;
|
|
18
58
|
if (path.isAbsolute(configName)) {
|
|
@@ -43,16 +83,62 @@ export function resolveBuildConfigPath(startDir = process.cwd(), buildConfigFile
|
|
|
43
83
|
return '';
|
|
44
84
|
}
|
|
45
85
|
|
|
46
|
-
|
|
47
|
-
|
|
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 '';
|
|
48
116
|
}
|
|
49
117
|
|
|
50
|
-
export function
|
|
51
|
-
|
|
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;
|
|
52
134
|
}
|
|
53
135
|
|
|
54
|
-
function
|
|
55
|
-
const
|
|
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;
|
|
56
142
|
if (explicitConfig) {
|
|
57
143
|
const explicitPath = resolveBuildConfigPath(startDir, explicitConfig);
|
|
58
144
|
if (!explicitPath) {
|
|
@@ -61,23 +147,32 @@ function resolvePreferredBuildConfigPath(startDir, buildConfigFile, preferredCon
|
|
|
61
147
|
return explicitPath;
|
|
62
148
|
}
|
|
63
149
|
|
|
64
|
-
const
|
|
65
|
-
if (preferredConfigPath) {
|
|
66
|
-
return preferredConfigPath;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const defaultConfigPath = resolveBuildConfigPath(startDir, DEFAULT_BUILD_CONFIG_FILE);
|
|
150
|
+
const defaultConfigPath = resolveBuildConfigPath(startDir, DEFAULT_RELEASE_BUILD_CONFIG_FILE);
|
|
70
151
|
if (!defaultConfigPath) {
|
|
71
|
-
throw new Error(`Build config file not found: ${
|
|
152
|
+
throw new Error(`Build config file not found: ${DEFAULT_RELEASE_BUILD_CONFIG_FILE}`);
|
|
72
153
|
}
|
|
73
154
|
return defaultConfigPath;
|
|
74
155
|
}
|
|
75
156
|
|
|
76
|
-
export
|
|
77
|
-
const
|
|
78
|
-
if (
|
|
79
|
-
|
|
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;
|
|
80
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);
|
|
81
176
|
|
|
82
177
|
const projectCwd = path.dirname(configPath);
|
|
83
178
|
const configName = path.basename(configPath);
|
package/lib/app/project_start.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import logger from 'loglevel';
|
|
2
|
-
import { DEFAULT_BUILD_CONFIG_FILE } from '../build_remote.js';
|
|
3
2
|
import { LpkBuild } from './lpk_build.js';
|
|
4
|
-
import { resolveProjectRuntime, getProjectDeployInfo, getProjectComposePs } from './project_runtime.js';
|
|
3
|
+
import { addProjectTargetOptions, resolveProjectRuntime, getProjectDeployInfo, getProjectComposePs } from './project_runtime.js';
|
|
5
4
|
|
|
6
5
|
function sleep(ms) {
|
|
7
6
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
@@ -32,7 +31,7 @@ async function waitProjectNotRunning(runtime, timeoutMs = 30000) {
|
|
|
32
31
|
}
|
|
33
32
|
|
|
34
33
|
async function deployCurrentVersion(runtime) {
|
|
35
|
-
const lpkBuild = await new LpkBuild(runtime.projectCwd, runtime.configName).init();
|
|
34
|
+
const lpkBuild = await new LpkBuild(runtime.projectCwd, runtime.configName, { forceV2: true }).init();
|
|
36
35
|
lpkBuild.onBeforeBuildPackage(async (options) => {
|
|
37
36
|
delete options['devshell'];
|
|
38
37
|
return options;
|
|
@@ -47,20 +46,16 @@ export function projectStartCommand() {
|
|
|
47
46
|
command: 'start',
|
|
48
47
|
desc: 'Start project app',
|
|
49
48
|
builder: (args) => {
|
|
50
|
-
args
|
|
51
|
-
alias: 'config',
|
|
52
|
-
describe: 'Build config file name',
|
|
53
|
-
type: 'string',
|
|
54
|
-
default: DEFAULT_BUILD_CONFIG_FILE,
|
|
55
|
-
});
|
|
49
|
+
addProjectTargetOptions(args);
|
|
56
50
|
args.option('restart', {
|
|
57
51
|
describe: 'Force restart app instance',
|
|
58
52
|
type: 'boolean',
|
|
59
53
|
default: false,
|
|
60
54
|
});
|
|
61
55
|
},
|
|
62
|
-
handler: async ({ config, restart }) => {
|
|
63
|
-
const runtime = await resolveProjectRuntime(process.cwd(), config);
|
|
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}`);
|
|
64
59
|
let deploy = await getProjectDeployInfo(runtime);
|
|
65
60
|
|
|
66
61
|
if (!deploy.currentVersionDeployed) {
|
|
@@ -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
|
+
}
|