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.
@@ -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;
@@ -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(context);
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 axios from 'axios';
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": "4.7.3",
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": "4.7.3",
36
- "@lowdefy/helpers": "4.7.3",
37
- "@lowdefy/logger": "4.7.3",
38
- "@lowdefy/node-utils": "4.7.3",
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.1.63",
51
- "@swc/core": "1.3.99",
52
- "@swc/jest": "0.2.29",
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 --delete-dir-on-start --copy-files",
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"