lowdefy 4.7.3 → 5.0.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/dist/commands/upgrade/executePhase.js +92 -0
- package/dist/commands/upgrade/getCodemods.js +70 -0
- package/dist/commands/upgrade/handlePrompt.js +99 -0
- package/dist/commands/upgrade/resolveChain.js +41 -0
- package/dist/commands/upgrade/runUpgrade.js +89 -0
- package/dist/commands/upgrade/upgrade.js +136 -0
- package/dist/commands/upgrade/upgradeState.js +43 -0
- package/dist/program.js +5 -0
- package/dist/utils/startUp.js +4 -1
- package/dist/utils/validateVersion.js +24 -3
- package/package.json +9 -9
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/ import fs from 'fs';
|
|
16
|
+
import path from 'path';
|
|
17
|
+
import { execSync } from 'child_process';
|
|
18
|
+
import handlePrompt from './handlePrompt.js';
|
|
19
|
+
import { askQuestion } from './handlePrompt.js';
|
|
20
|
+
function updateLowdefyVersion(configDirectory, version) {
|
|
21
|
+
const yamlPath = path.join(configDirectory, 'lowdefy.yaml');
|
|
22
|
+
if (!fs.existsSync(yamlPath)) return;
|
|
23
|
+
let content = fs.readFileSync(yamlPath, 'utf8');
|
|
24
|
+
// Match both quoted and unquoted version values
|
|
25
|
+
content = content.replace(/^(lowdefy:\s*)(['"]?)[\d.]+(?:-[\w.-]+)?(\2)\s*$/m, `$1$2${version}$3`);
|
|
26
|
+
fs.writeFileSync(yamlPath, content);
|
|
27
|
+
}
|
|
28
|
+
function isGitRepo(directory) {
|
|
29
|
+
try {
|
|
30
|
+
execSync('git rev-parse --is-inside-work-tree', {
|
|
31
|
+
cwd: directory,
|
|
32
|
+
stdio: 'pipe'
|
|
33
|
+
});
|
|
34
|
+
return true;
|
|
35
|
+
} catch {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
async function executePhase({ phase, phaseIndex, totalPhases, targetDirectory, codemodsDirectory, logger }) {
|
|
40
|
+
const phaseLabel = `[${phaseIndex + 1}/${totalPhases}]`;
|
|
41
|
+
logger.info(`\n${phaseLabel} Upgrading to v${phase.version} — ${phase.description}`);
|
|
42
|
+
const results = [];
|
|
43
|
+
const totalCodemods = phase.codemods.length;
|
|
44
|
+
for(let j = 0; j < phase.codemods.length; j++){
|
|
45
|
+
const codemod = phase.codemods[j];
|
|
46
|
+
const stepLabel = `${phaseLabel} [${j + 1}/${totalCodemods}]`;
|
|
47
|
+
const label = codemod.description;
|
|
48
|
+
if (!codemod.path) {
|
|
49
|
+
logger.warn(` ${stepLabel} ${label} — no path defined, skipping.`);
|
|
50
|
+
results.push({
|
|
51
|
+
id: codemod.id,
|
|
52
|
+
status: 'skipped'
|
|
53
|
+
});
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
const codemodPath = path.join(codemodsDirectory, codemod.path);
|
|
57
|
+
logger.info(` ${stepLabel} ${label}`);
|
|
58
|
+
const result = await handlePrompt({
|
|
59
|
+
path: codemodPath,
|
|
60
|
+
codemodId: codemod.id,
|
|
61
|
+
stepLabel,
|
|
62
|
+
logger
|
|
63
|
+
});
|
|
64
|
+
results.push({
|
|
65
|
+
id: codemod.id,
|
|
66
|
+
status: result.status
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
updateLowdefyVersion(targetDirectory, phase.version);
|
|
70
|
+
logger.info(`\n Updated lowdefy.yaml: lowdefy: '${phase.version}'`);
|
|
71
|
+
if (isGitRepo(targetDirectory)) {
|
|
72
|
+
const answer = await askQuestion(` Commit this phase? [Y/n] `);
|
|
73
|
+
if (answer === '' || answer.toLowerCase() === 'y') {
|
|
74
|
+
try {
|
|
75
|
+
execSync(`git add -A && git commit -m "chore: upgrade to lowdefy v${phase.version}"`, {
|
|
76
|
+
cwd: targetDirectory,
|
|
77
|
+
stdio: 'inherit'
|
|
78
|
+
});
|
|
79
|
+
} catch {
|
|
80
|
+
logger.warn(' Git commit failed. You can commit manually.');
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
logger.info(` ✓ Phase complete.`);
|
|
85
|
+
return {
|
|
86
|
+
version: phase.version,
|
|
87
|
+
status: 'completed',
|
|
88
|
+
codemods: results
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
export default executePhase;
|
|
92
|
+
export { updateLowdefyVersion };
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/ import fs from 'fs';
|
|
16
|
+
import path from 'path';
|
|
17
|
+
import axios from 'axios';
|
|
18
|
+
import { cleanDirectory, readFile } from '@lowdefy/node-utils';
|
|
19
|
+
import fetchNpmTarball from '../../utils/fetchNpmTarball.js';
|
|
20
|
+
async function getCodemods({ directory, logger }) {
|
|
21
|
+
const packageJsonPath = path.join(directory, 'package.json');
|
|
22
|
+
// Query npm for the latest version
|
|
23
|
+
let latestVersion;
|
|
24
|
+
try {
|
|
25
|
+
const packageInfo = await axios.get('https://registry.npmjs.org/@lowdefy/codemods');
|
|
26
|
+
latestVersion = packageInfo.data['dist-tags']?.latest;
|
|
27
|
+
} catch (error) {
|
|
28
|
+
if (error.response?.status === 404) {
|
|
29
|
+
throw new Error('No @lowdefy/codemods package found on npm. Upgrade manually using the migration guide.');
|
|
30
|
+
}
|
|
31
|
+
throw error;
|
|
32
|
+
}
|
|
33
|
+
if (!latestVersion) {
|
|
34
|
+
throw new Error('Could not determine latest @lowdefy/codemods version.');
|
|
35
|
+
}
|
|
36
|
+
// Check if cached version matches
|
|
37
|
+
let fetchCodemods = false;
|
|
38
|
+
const exists = fs.existsSync(packageJsonPath);
|
|
39
|
+
if (!exists) {
|
|
40
|
+
fetchCodemods = true;
|
|
41
|
+
} else {
|
|
42
|
+
const cachedConfig = JSON.parse(await readFile(packageJsonPath));
|
|
43
|
+
if (cachedConfig.version !== latestVersion) {
|
|
44
|
+
logger.info(`Updating codemods package from ${cachedConfig.version} to ${latestVersion}.`);
|
|
45
|
+
await cleanDirectory(directory);
|
|
46
|
+
fetchCodemods = true;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (fetchCodemods) {
|
|
50
|
+
logger.info({
|
|
51
|
+
spin: 'start'
|
|
52
|
+
}, 'Fetching @lowdefy/codemods from npm.');
|
|
53
|
+
await fetchNpmTarball({
|
|
54
|
+
packageName: '@lowdefy/codemods',
|
|
55
|
+
version: latestVersion,
|
|
56
|
+
directory
|
|
57
|
+
});
|
|
58
|
+
logger.info('Fetched @lowdefy/codemods from npm.');
|
|
59
|
+
}
|
|
60
|
+
// Read and return registry
|
|
61
|
+
const registryPath = path.join(directory, 'registry.json');
|
|
62
|
+
if (!fs.existsSync(registryPath)) {
|
|
63
|
+
throw new Error('Codemods package missing registry.json.');
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
registry: JSON.parse(fs.readFileSync(registryPath, 'utf8')),
|
|
67
|
+
directory
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
export default getCodemods;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/ import fs from 'fs';
|
|
16
|
+
import { execSync } from 'child_process';
|
|
17
|
+
import readline from 'readline';
|
|
18
|
+
function detectAiTool() {
|
|
19
|
+
if (process.env.CLAUDE_CODE === '1' || fs.existsSync(`${process.env.HOME}/.claude`)) {
|
|
20
|
+
return 'Claude Code';
|
|
21
|
+
}
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
function askQuestion(question) {
|
|
25
|
+
const rl = readline.createInterface({
|
|
26
|
+
input: process.stdin,
|
|
27
|
+
output: process.stdout
|
|
28
|
+
});
|
|
29
|
+
return new Promise((resolve)=>{
|
|
30
|
+
rl.question(question, (answer)=>{
|
|
31
|
+
rl.close();
|
|
32
|
+
resolve(answer.trim());
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
function copyToClipboard(text) {
|
|
37
|
+
try {
|
|
38
|
+
if (process.platform === 'darwin') {
|
|
39
|
+
execSync('pbcopy', {
|
|
40
|
+
input: text
|
|
41
|
+
});
|
|
42
|
+
} else if (process.platform === 'linux') {
|
|
43
|
+
execSync('xclip -selection clipboard', {
|
|
44
|
+
input: text
|
|
45
|
+
});
|
|
46
|
+
} else if (process.platform === 'win32') {
|
|
47
|
+
execSync('clip', {
|
|
48
|
+
input: text
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
return true;
|
|
52
|
+
} catch {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
async function handlePrompt({ path: filePath, codemodId, stepLabel, logger }) {
|
|
57
|
+
const prefix = stepLabel ? ` ${stepLabel}` : ' ';
|
|
58
|
+
const aiTool = detectAiTool();
|
|
59
|
+
if (!filePath || !fs.existsSync(filePath)) {
|
|
60
|
+
logger.warn(`${prefix} No prompt/guide found for ${codemodId}. Skipping.`);
|
|
61
|
+
return {
|
|
62
|
+
status: 'skipped'
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
66
|
+
if (aiTool) {
|
|
67
|
+
logger.info(`${prefix} ${aiTool} detected.`);
|
|
68
|
+
}
|
|
69
|
+
logger.info(`${prefix} [1] Copy migration prompt to clipboard`);
|
|
70
|
+
logger.info(`${prefix} [2] View migration guide`);
|
|
71
|
+
logger.info(`${prefix} [3] Skip for now`);
|
|
72
|
+
const answer = await askQuestion(`${prefix} > `);
|
|
73
|
+
const choice = parseInt(answer, 10);
|
|
74
|
+
if (choice === 1) {
|
|
75
|
+
const copied = copyToClipboard(content);
|
|
76
|
+
if (copied) {
|
|
77
|
+
logger.info(`${prefix} Prompt copied to clipboard. Paste into your AI tool, then press Enter when done.`);
|
|
78
|
+
} else {
|
|
79
|
+
logger.info(`${prefix} Could not copy to clipboard. Content:`);
|
|
80
|
+
logger.info(content);
|
|
81
|
+
}
|
|
82
|
+
await askQuestion(`${prefix} Press Enter when done...`);
|
|
83
|
+
return {
|
|
84
|
+
status: 'completed'
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
if (choice === 2) {
|
|
88
|
+
logger.info(content);
|
|
89
|
+
await askQuestion(`${prefix} Press Enter when done...`);
|
|
90
|
+
return {
|
|
91
|
+
status: 'completed'
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
status: 'skipped'
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
export { detectAiTool, askQuestion, copyToClipboard };
|
|
99
|
+
export default handlePrompt;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/ import semver from 'semver';
|
|
16
|
+
function resolveChain({ registry, currentVersion, targetVersion }) {
|
|
17
|
+
if (semver.gte(currentVersion, targetVersion)) {
|
|
18
|
+
return [];
|
|
19
|
+
}
|
|
20
|
+
const candidates = (registry.versions ?? []).filter((entry)=>semver.valid(entry.version) && semver.lte(entry.version, targetVersion)).sort((a, b)=>semver.compare(a.version, b.version));
|
|
21
|
+
const chain = [];
|
|
22
|
+
let cursor = currentVersion;
|
|
23
|
+
while(semver.lt(cursor, targetVersion)){
|
|
24
|
+
const match = candidates.find((entry)=>semver.gt(entry.version, cursor) && semver.satisfies(cursor, entry.from));
|
|
25
|
+
if (match) {
|
|
26
|
+
chain.push(match);
|
|
27
|
+
cursor = match.version;
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
// Gap handling: skip to next entry with version > cursor
|
|
31
|
+
const next = candidates.find((entry)=>semver.gt(entry.version, cursor));
|
|
32
|
+
if (next) {
|
|
33
|
+
chain.push(next);
|
|
34
|
+
cursor = next.version;
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
return chain;
|
|
40
|
+
}
|
|
41
|
+
export default resolveChain;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/ import { readUpgradeState, writeUpgradeState, clearUpgradeState } from './upgradeState.js';
|
|
16
|
+
import executePhase from './executePhase.js';
|
|
17
|
+
async function runUpgrade({ chain, targetDirectory, codemodsDirectory, logger, resume }) {
|
|
18
|
+
let phases;
|
|
19
|
+
if (resume) {
|
|
20
|
+
const state = readUpgradeState(targetDirectory);
|
|
21
|
+
if (!state) {
|
|
22
|
+
logger.warn('No upgrade state found. Starting fresh.');
|
|
23
|
+
phases = chain;
|
|
24
|
+
} else {
|
|
25
|
+
const completedVersions = new Set(state.phases.filter((p)=>p.status === 'completed').map((p)=>p.version));
|
|
26
|
+
phases = chain.filter((entry)=>!completedVersions.has(entry.version));
|
|
27
|
+
if (phases.length === 0) {
|
|
28
|
+
logger.info('All phases already completed.');
|
|
29
|
+
clearUpgradeState(targetDirectory);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
logger.info(`Resuming upgrade — ${phases.length} phase(s) remaining.`);
|
|
33
|
+
}
|
|
34
|
+
} else {
|
|
35
|
+
phases = chain;
|
|
36
|
+
}
|
|
37
|
+
// Write initial state
|
|
38
|
+
const state = {
|
|
39
|
+
startedAt: new Date().toISOString(),
|
|
40
|
+
fromVersion: chain[0]?.version ?? 'unknown',
|
|
41
|
+
toVersion: chain[chain.length - 1]?.version ?? 'unknown',
|
|
42
|
+
currentPhase: 0,
|
|
43
|
+
phases: chain.map((entry)=>({
|
|
44
|
+
version: entry.version,
|
|
45
|
+
status: 'pending',
|
|
46
|
+
codemods: entry.codemods.map((c)=>({
|
|
47
|
+
id: c.id,
|
|
48
|
+
status: 'pending'
|
|
49
|
+
}))
|
|
50
|
+
}))
|
|
51
|
+
};
|
|
52
|
+
writeUpgradeState(targetDirectory, state);
|
|
53
|
+
let skippedCount = 0;
|
|
54
|
+
for(let i = 0; i < phases.length; i++){
|
|
55
|
+
const phase = phases[i];
|
|
56
|
+
// Update state to in-progress
|
|
57
|
+
const statePhase = state.phases.find((p)=>p.version === phase.version);
|
|
58
|
+
if (statePhase) statePhase.status = 'in-progress';
|
|
59
|
+
state.currentPhase = i;
|
|
60
|
+
writeUpgradeState(targetDirectory, state);
|
|
61
|
+
const result = await executePhase({
|
|
62
|
+
phase,
|
|
63
|
+
phaseIndex: i,
|
|
64
|
+
totalPhases: phases.length,
|
|
65
|
+
targetDirectory,
|
|
66
|
+
codemodsDirectory,
|
|
67
|
+
logger
|
|
68
|
+
});
|
|
69
|
+
// Update state to completed
|
|
70
|
+
if (statePhase) {
|
|
71
|
+
statePhase.status = 'completed';
|
|
72
|
+
statePhase.codemods = result.codemods;
|
|
73
|
+
}
|
|
74
|
+
writeUpgradeState(targetDirectory, state);
|
|
75
|
+
skippedCount += result.codemods.filter((c)=>c.status === 'skipped').length;
|
|
76
|
+
}
|
|
77
|
+
if (skippedCount > 0) {
|
|
78
|
+
logger.info(`\n⚠ ${skippedCount} codemod(s) were skipped. Run "npx lowdefy upgrade --resume" to complete them.`);
|
|
79
|
+
} else {
|
|
80
|
+
clearUpgradeState(targetDirectory);
|
|
81
|
+
}
|
|
82
|
+
const targetVersion = chain[chain.length - 1]?.version ?? 'unknown';
|
|
83
|
+
logger.info(`\n✓ Upgrade complete! Your app is now on Lowdefy ${targetVersion}.`);
|
|
84
|
+
logger.info(`\n Next steps:`);
|
|
85
|
+
logger.info(` - Review changes: git diff`);
|
|
86
|
+
logger.info(` - Run your app: lowdefy dev`);
|
|
87
|
+
logger.info(` - Run your tests`);
|
|
88
|
+
}
|
|
89
|
+
export default runUpgrade;
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/ import path from 'path';
|
|
16
|
+
import axios from 'axios';
|
|
17
|
+
import semver from 'semver';
|
|
18
|
+
import { updateLowdefyVersion } from './executePhase.js';
|
|
19
|
+
import getCodemods from './getCodemods.js';
|
|
20
|
+
import resolveChain from './resolveChain.js';
|
|
21
|
+
import runUpgrade from './runUpgrade.js';
|
|
22
|
+
import { readUpgradeState } from './upgradeState.js';
|
|
23
|
+
import { askQuestion } from './handlePrompt.js';
|
|
24
|
+
async function upgrade({ context }) {
|
|
25
|
+
const currentVersion = context.lowdefyVersion;
|
|
26
|
+
const logger = context.logger;
|
|
27
|
+
const configDirectory = context.directories.config;
|
|
28
|
+
const codemodsDir = path.join(configDirectory, '.lowdefy', 'codemods');
|
|
29
|
+
if (!semver.valid(currentVersion)) {
|
|
30
|
+
throw new Error(`Current version "${currentVersion}" is not a valid semver version.`);
|
|
31
|
+
}
|
|
32
|
+
logger.info(`Current version: ${currentVersion} (from lowdefy.yaml)`);
|
|
33
|
+
// Determine target version
|
|
34
|
+
let targetVersion = context.options.to;
|
|
35
|
+
if (!targetVersion) {
|
|
36
|
+
logger.info({
|
|
37
|
+
spin: 'start'
|
|
38
|
+
}, 'Checking latest Lowdefy version...');
|
|
39
|
+
try {
|
|
40
|
+
const packageInfo = await axios.get('https://registry.npmjs.org/lowdefy');
|
|
41
|
+
targetVersion = packageInfo.data['dist-tags'].latest;
|
|
42
|
+
} catch {
|
|
43
|
+
throw new Error('Could not reach npm registry to determine latest version.');
|
|
44
|
+
}
|
|
45
|
+
logger.info(`Latest stable version: ${targetVersion}`);
|
|
46
|
+
}
|
|
47
|
+
if (!semver.valid(targetVersion)) {
|
|
48
|
+
throw new Error(`Target version "${targetVersion}" is not a valid semver version.`);
|
|
49
|
+
}
|
|
50
|
+
const targetIsPrerelease = semver.prerelease(targetVersion) !== null;
|
|
51
|
+
if (!targetIsPrerelease && semver.gt(currentVersion, targetVersion)) {
|
|
52
|
+
logger.info(`Already up to date (${currentVersion}).`);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (!targetIsPrerelease && semver.eq(currentVersion, targetVersion)) {
|
|
56
|
+
logger.info(`Version ${currentVersion} is already set in lowdefy.yaml.`);
|
|
57
|
+
const runCodemods = await askQuestion('Still run codemods for this version? [y/N] ');
|
|
58
|
+
if (!runCodemods || runCodemods.toLowerCase() !== 'y') {
|
|
59
|
+
logger.info('Upgrade skipped.');
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (targetIsPrerelease) {
|
|
64
|
+
logger.warn(`
|
|
65
|
+
---------------------------------------------------
|
|
66
|
+
Upgrading to prerelease version ${targetVersion}.
|
|
67
|
+
Features may change at any time.
|
|
68
|
+
---------------------------------------------------`);
|
|
69
|
+
updateLowdefyVersion(configDirectory, targetVersion);
|
|
70
|
+
logger.info(`Updated lowdefy.yaml to version ${targetVersion}.`);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
logger.info(`\nUpgrade path: ${currentVersion} → ${targetVersion}\n`);
|
|
74
|
+
// Fetch codemods package
|
|
75
|
+
const { registry, directory: codemodsDirectory } = await getCodemods({
|
|
76
|
+
directory: codemodsDir,
|
|
77
|
+
logger
|
|
78
|
+
});
|
|
79
|
+
// Resolve chain
|
|
80
|
+
const chain = resolveChain({
|
|
81
|
+
registry,
|
|
82
|
+
currentVersion,
|
|
83
|
+
targetVersion
|
|
84
|
+
});
|
|
85
|
+
if (chain.length === 0) {
|
|
86
|
+
logger.info('No codemods needed for this upgrade. Update lowdefy.yaml manually.');
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
// Handle resume
|
|
90
|
+
const resume = context.options.resume;
|
|
91
|
+
if (resume) {
|
|
92
|
+
const state = readUpgradeState(configDirectory);
|
|
93
|
+
if (!state) {
|
|
94
|
+
logger.warn('No upgrade in progress. Starting fresh.');
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
// Print plan
|
|
98
|
+
chain.forEach((phase, i)=>{
|
|
99
|
+
const codemodCount = phase.codemods.length;
|
|
100
|
+
logger.info(` Phase ${i + 1}: v${phase.version} — ${codemodCount} codemod(s)`);
|
|
101
|
+
phase.codemods.forEach((c)=>{
|
|
102
|
+
logger.info(` • ${c.description}`);
|
|
103
|
+
});
|
|
104
|
+
logger.info('');
|
|
105
|
+
});
|
|
106
|
+
if (context.options.plan) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
// Confirm
|
|
110
|
+
const answer = await askQuestion('Proceed? [Y/n] ');
|
|
111
|
+
if (answer && answer.toLowerCase() !== 'y') {
|
|
112
|
+
logger.info('Upgrade cancelled.');
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
// Execute
|
|
116
|
+
await runUpgrade({
|
|
117
|
+
chain,
|
|
118
|
+
targetDirectory: configDirectory,
|
|
119
|
+
codemodsDirectory,
|
|
120
|
+
logger,
|
|
121
|
+
resume
|
|
122
|
+
});
|
|
123
|
+
// Telemetry
|
|
124
|
+
if (context.sendTelemetry) {
|
|
125
|
+
await context.sendTelemetry({
|
|
126
|
+
data: {
|
|
127
|
+
upgrade: {
|
|
128
|
+
from: currentVersion,
|
|
129
|
+
to: targetVersion,
|
|
130
|
+
phases: chain.length
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
export default upgrade;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/ import fs from 'fs';
|
|
16
|
+
import path from 'path';
|
|
17
|
+
function getStatePath(directory) {
|
|
18
|
+
return path.join(directory, '.lowdefy', 'upgrade-state.json');
|
|
19
|
+
}
|
|
20
|
+
function readUpgradeState(directory) {
|
|
21
|
+
const statePath = getStatePath(directory);
|
|
22
|
+
if (!fs.existsSync(statePath)) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
return JSON.parse(fs.readFileSync(statePath, 'utf8'));
|
|
26
|
+
}
|
|
27
|
+
function writeUpgradeState(directory, state) {
|
|
28
|
+
const statePath = getStatePath(directory);
|
|
29
|
+
const dir = path.dirname(statePath);
|
|
30
|
+
if (!fs.existsSync(dir)) {
|
|
31
|
+
fs.mkdirSync(dir, {
|
|
32
|
+
recursive: true
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
fs.writeFileSync(statePath, JSON.stringify(state, null, 2) + '\n');
|
|
36
|
+
}
|
|
37
|
+
function clearUpgradeState(directory) {
|
|
38
|
+
const statePath = getStatePath(directory);
|
|
39
|
+
if (fs.existsSync(statePath)) {
|
|
40
|
+
fs.unlinkSync(statePath);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export { readUpgradeState, writeUpgradeState, clearUpgradeState };
|
package/dist/program.js
CHANGED
|
@@ -20,6 +20,7 @@ import init from './commands/init/init.js';
|
|
|
20
20
|
import initDocker from './commands/init-docker/initDocker.js';
|
|
21
21
|
import initVercel from './commands/init-vercel/initVercel.js';
|
|
22
22
|
import start from './commands/start/start.js';
|
|
23
|
+
import upgrade from './commands/upgrade/upgrade.js';
|
|
23
24
|
import runCommand from './utils/runCommand.js';
|
|
24
25
|
const require = createRequire(import.meta.url);
|
|
25
26
|
const packageJson = require('../package.json');
|
|
@@ -68,4 +69,8 @@ program.command('start').description('Start a Lowdefy production app.').usage('[
|
|
|
68
69
|
cliVersion,
|
|
69
70
|
handler: start
|
|
70
71
|
}));
|
|
72
|
+
program.command('upgrade').description('Upgrade a Lowdefy app to a newer version, applying codemods.').usage('[options]').addOption(options.configDirectory).addOption(options.disableTelemetry).addOption(options.logLevel).addOption(new Option('--to <version>', 'Target version. Default: latest stable.')).addOption(new Option('--plan', 'Show upgrade plan without executing.')).addOption(new Option('--resume', 'Resume a previously interrupted upgrade.')).action(runCommand({
|
|
73
|
+
cliVersion,
|
|
74
|
+
handler: upgrade
|
|
75
|
+
}));
|
|
71
76
|
export default program;
|
package/dist/utils/startUp.js
CHANGED
|
@@ -47,7 +47,10 @@ async function startUp({ context, options = {}, command }) {
|
|
|
47
47
|
logger: context.logger,
|
|
48
48
|
pnpmCmd: context.pnpmCmd
|
|
49
49
|
});
|
|
50
|
-
await validateVersion(
|
|
50
|
+
await validateVersion({
|
|
51
|
+
...context,
|
|
52
|
+
configDirectory: context.configDirectory
|
|
53
|
+
});
|
|
51
54
|
context.sendTelemetry = getSendTelemetry(context);
|
|
52
55
|
if (type.isNone(lowdefyVersion)) {
|
|
53
56
|
context.logger.info(`Running 'lowdefy ${context.command}'.`);
|
|
@@ -12,9 +12,11 @@
|
|
|
12
12
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
13
|
See the License for the specific language governing permissions and
|
|
14
14
|
limitations under the License.
|
|
15
|
-
*/ import
|
|
15
|
+
*/ import fs from 'fs';
|
|
16
|
+
import path from 'path';
|
|
17
|
+
import axios from 'axios';
|
|
16
18
|
import semver from 'semver';
|
|
17
|
-
async function validateVersion({ cliVersion, lowdefyVersion, logger, requiresLowdefyYaml }) {
|
|
19
|
+
async function validateVersion({ cliVersion, lowdefyVersion, logger, requiresLowdefyYaml, configDirectory }) {
|
|
18
20
|
const ui = logger?.ui ?? logger ?? {
|
|
19
21
|
warn: (message)=>console.warn(message)
|
|
20
22
|
};
|
|
@@ -73,8 +75,27 @@ async function validateVersion({ cliVersion, lowdefyVersion, logger, requiresLow
|
|
|
73
75
|
} catch (error) {
|
|
74
76
|
ui.warn('Failed to check for latest Lowdefy version.');
|
|
75
77
|
}
|
|
78
|
+
// Check for pending codemods from an interrupted upgrade
|
|
79
|
+
if (configDirectory) {
|
|
80
|
+
try {
|
|
81
|
+
const upgradeStatePath = path.join(configDirectory, '.lowdefy', 'upgrade-state.json');
|
|
82
|
+
if (fs.existsSync(upgradeStatePath)) {
|
|
83
|
+
const state = JSON.parse(fs.readFileSync(upgradeStatePath, 'utf8'));
|
|
84
|
+
const skipped = state.phases.flatMap((p)=>p.codemods).filter((c)=>c.status === 'skipped');
|
|
85
|
+
if (skipped.length > 0) {
|
|
86
|
+
ui.warn(`
|
|
87
|
+
-------------------------------------------------------------
|
|
88
|
+
${skipped.length} codemod(s) were skipped during upgrade.
|
|
89
|
+
Run "npx lowdefy upgrade --resume" to complete them.
|
|
90
|
+
-------------------------------------------------------------`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
} catch {
|
|
94
|
+
// Ignore errors reading upgrade state
|
|
95
|
+
}
|
|
96
|
+
}
|
|
76
97
|
}
|
|
77
98
|
function isExperimentalVersion(version) {
|
|
78
|
-
return version.includes('alpha') || version.includes('beta') || version.includes('rc');
|
|
99
|
+
return version.includes('alpha') || version.includes('beta') || version.includes('rc') || version.includes('experimental');
|
|
79
100
|
}
|
|
80
101
|
export default validateVersion;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lowdefy",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "5.0.0",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"description": "Lowdefy CLI",
|
|
6
6
|
"homepage": "https://lowdefy.com",
|
|
@@ -32,10 +32,10 @@
|
|
|
32
32
|
],
|
|
33
33
|
"exports": "./dist/index.js",
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@lowdefy/errors": "
|
|
36
|
-
"@lowdefy/helpers": "
|
|
37
|
-
"@lowdefy/logger": "
|
|
38
|
-
"@lowdefy/node-utils": "
|
|
35
|
+
"@lowdefy/errors": "5.0.0",
|
|
36
|
+
"@lowdefy/helpers": "5.0.0",
|
|
37
|
+
"@lowdefy/logger": "5.0.0",
|
|
38
|
+
"@lowdefy/node-utils": "5.0.0",
|
|
39
39
|
"axios": "1.8.2",
|
|
40
40
|
"commander": "11.1.0",
|
|
41
41
|
"decompress": "4.2.1",
|
|
@@ -47,9 +47,9 @@
|
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
49
49
|
"@jest/globals": "28.1.3",
|
|
50
|
-
"@swc/cli": "0.
|
|
51
|
-
"@swc/core": "1.
|
|
52
|
-
"@swc/jest": "0.2.
|
|
50
|
+
"@swc/cli": "0.8.0",
|
|
51
|
+
"@swc/core": "1.15.18",
|
|
52
|
+
"@swc/jest": "0.2.39",
|
|
53
53
|
"jest": "28.1.3"
|
|
54
54
|
},
|
|
55
55
|
"engines": {
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
"access": "public"
|
|
60
60
|
},
|
|
61
61
|
"scripts": {
|
|
62
|
-
"build": "swc src --out-dir dist --config-file ../../.swcrc --
|
|
62
|
+
"build": "swc src --out-dir dist --config-file ../../.swcrc --cli-config-file ../../.swc-cli.json --copy-files",
|
|
63
63
|
"clean": "rm -rf dist && rm -rf .lowdefy",
|
|
64
64
|
"start": "node ./dist/index.js",
|
|
65
65
|
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js"
|