Skip to content

Commit 0628cdb

Browse files
authored
Merge pull request #1142 from appwrite/cursor/add-update-command-to-cli-sdk-9202
Add update command to CLI SDK
2 parents ad31e7a + 27bafa9 commit 0628cdb

File tree

5 files changed

+300
-1
lines changed

5 files changed

+300
-1
lines changed

src/SDK/Language/CLI.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,11 @@ public function getFiles(): array
330330
'scope' => 'default',
331331
'destination' => 'lib/commands/types.js',
332332
'template' => 'cli/lib/commands/types.js.twig',
333+
],
334+
[
335+
'scope' => 'default',
336+
'destination' => 'lib/commands/update.js',
337+
'template' => 'cli/lib/commands/update.js.twig',
333338
]
334339
];
335340
}

templates/cli/index.js.twig

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ const chalk = require("chalk");
1010
const { version } = require("./package.json");
1111
const { commandDescriptions, cliConfig } = require("./lib/parser");
1212
const { client } = require("./lib/commands/generic");
13+
const { getLatestVersion, compareVersions } = require("./lib/utils");
1314
const inquirer = require("inquirer");
1415
{% if sdk.test != "true" %}
1516
const { login, logout, whoami, migrate, register } = require("./lib/commands/generic");
@@ -18,6 +19,7 @@ const { types } = require("./lib/commands/types");
1819
const { pull } = require("./lib/commands/pull");
1920
const { run } = require("./lib/commands/run");
2021
const { push, deploy } = require("./lib/commands/push");
22+
const { update } = require("./lib/commands/update");
2123
{% else %}
2224
const { migrate } = require("./lib/commands/generic");
2325
{% endif %}
@@ -27,6 +29,41 @@ const { {{ service.name | caseLower }} } = require("./lib/commands/{{ service.na
2729

2830
inquirer.registerPrompt('search-list', require('inquirer-search-list'));
2931

32+
/**
33+
* Check for updates and show version information
34+
*/
35+
async function checkVersion() {
36+
process.stdout.write(chalk.bold(`{{ language.params.executableName|caseLower }} version ${version}`) + '\n');
37+
38+
try {
39+
const latestVersion = await getLatestVersion();
40+
const comparison = compareVersions(version, latestVersion);
41+
42+
if (comparison > 0) {
43+
// Current version is older than latest
44+
process.stdout.write(chalk.yellow(`\n⚠️ A newer version is available: ${chalk.bold(latestVersion)}`) + '\n');
45+
process.stdout.write(chalk.cyan(`💡 Run '${chalk.bold('{{ language.params.executableName|caseLower }} update')}' to update to the latest version.`) + '\n');
46+
} else if (comparison === 0) {
47+
process.stdout.write(chalk.green('\n✅ You are running the latest version!') + '\n');
48+
} else {
49+
// Current version is newer than latest (pre-release/dev)
50+
process.stdout.write(chalk.blue('\n🚀 You are running a pre-release or development version.') + '\n');
51+
}
52+
} catch (error) {
53+
// Silently fail version check, just show current version
54+
process.stdout.write(chalk.gray('\n(Unable to check for updates)') + '\n');
55+
}
56+
}
57+
58+
// Intercept version flag before Commander.js processes it
59+
if (process.argv.includes('-v') || process.argv.includes('--version')) {
60+
(async () => {
61+
await checkVersion();
62+
process.exit(0);
63+
})();
64+
return;
65+
}
66+
3067
program
3168
.description(commandDescriptions['main'])
3269
.configureHelp({
@@ -72,6 +109,7 @@ program
72109
.addCommand(types)
73110
.addCommand(deploy)
74111
.addCommand(run)
112+
.addCommand(update)
75113
.addCommand(logout)
76114
{% endif %}
77115
{% for service in spec.services %}
Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
const fs = require("fs");
2+
const path = require("path");
3+
const { spawn } = require("child_process");
4+
const { Command } = require("commander");
5+
const { fetch } = require("undici");
6+
const chalk = require("chalk");
7+
const inquirer = require("inquirer");
8+
const { success, log, warn, error, hint, actionRunner, commandDescriptions } = require("../parser");
9+
const { getLatestVersion, compareVersions } = require("../utils");
10+
const { version } = require("../../package.json");
11+
12+
/**
13+
* Check if the CLI was installed via npm
14+
*/
15+
const isInstalledViaNpm = () => {
16+
try {
17+
const scriptPath = process.argv[1];
18+
19+
if (scriptPath.includes('node_modules') && scriptPath.includes('{{ language.params.npmPackage|caseDash }}')) {
20+
return true;
21+
}
22+
23+
if (scriptPath.includes('/usr/local/lib/node_modules/') ||
24+
scriptPath.includes('/opt/homebrew/lib/node_modules/') ||
25+
scriptPath.includes('/.npm-global/') ||
26+
scriptPath.includes('/node_modules/.bin/')) {
27+
return true;
28+
}
29+
30+
return false;
31+
} catch (e) {
32+
return false;
33+
}
34+
};
35+
36+
/**
37+
* Check if the CLI was installed via Homebrew
38+
*/
39+
const isInstalledViaHomebrew = () => {
40+
try {
41+
const scriptPath = process.argv[1];
42+
return scriptPath.includes('/opt/homebrew/') || scriptPath.includes('/usr/local/Cellar/');
43+
} catch (e) {
44+
return false;
45+
}
46+
};
47+
48+
49+
50+
/**
51+
* Execute command and return promise
52+
*/
53+
const execCommand = (command, args = [], options = {}) => {
54+
return new Promise((resolve, reject) => {
55+
const child = spawn(command, args, {
56+
stdio: 'inherit',
57+
shell: true,
58+
...options
59+
});
60+
61+
child.on('close', (code) => {
62+
if (code === 0) {
63+
resolve();
64+
} else {
65+
reject(new Error(`Command failed with exit code ${code}`));
66+
}
67+
});
68+
69+
child.on('error', (err) => {
70+
reject(err);
71+
});
72+
});
73+
};
74+
75+
/**
76+
* Update via npm
77+
*/
78+
const updateViaNpm = async () => {
79+
try {
80+
await execCommand('npm', ['install', '-g', '{{ language.params.npmPackage|caseDash }}@latest']);
81+
console.log("");
82+
success("Updated to latest version via npm!");
83+
hint("Run '{{ language.params.executableName|caseLower }} --version' to verify the new version.");
84+
} catch (e) {
85+
if (e.message.includes('EEXIST') || e.message.includes('file already exists')) {
86+
console.log("");
87+
success("Latest version is already installed via npm!");
88+
hint("The CLI is up to date. Run '{{ language.params.executableName|caseLower }} --version' to verify.");
89+
} else {
90+
console.log("");
91+
error(`Failed to update via npm: ${e.message}`);
92+
hint("Try running: npm install -g {{ language.params.npmPackage|caseDash }}@latest --force");
93+
}
94+
}
95+
};
96+
97+
/**
98+
* Update via Homebrew
99+
*/
100+
const updateViaHomebrew = async () => {
101+
try {
102+
await execCommand('brew', ['upgrade', '{{ language.params.executableName|caseLower }}']);
103+
console.log("");
104+
success("Updated to latest version via Homebrew!");
105+
hint("Run '{{ language.params.executableName|caseLower }} --version' to verify the new version.");
106+
} catch (e) {
107+
if (e.message.includes('already installed') || e.message.includes('up-to-date')) {
108+
console.log("");
109+
success("Latest version is already installed via Homebrew!");
110+
hint("The CLI is up to date. Run '{{ language.params.executableName|caseLower }} --version' to verify.");
111+
} else {
112+
console.log("");
113+
error(`Failed to update via Homebrew: ${e.message}`);
114+
hint("Try running: brew upgrade {{ language.params.executableName|caseLower }}");
115+
}
116+
}
117+
};
118+
119+
/**
120+
* Show manual update instructions
121+
*/
122+
const showManualInstructions = (latestVersion) => {
123+
log("Manual update options:");
124+
console.log("");
125+
126+
log(`${chalk.bold("Option 1: NPM")}`);
127+
console.log(` npm install -g {{ language.params.npmPackage|caseDash }}@latest`);
128+
console.log("");
129+
130+
log(`${chalk.bold("Option 2: Homebrew")}`);
131+
console.log(` brew upgrade {{ language.params.executableName|caseLower }}`);
132+
console.log("");
133+
134+
log(`${chalk.bold("Option 3: Download Binary")}`);
135+
console.log(` Visit: https://github.com/{{ language.params.npmPackage|caseDash }}/releases/tag/${latestVersion}`);
136+
};
137+
138+
/**
139+
* Show interactive menu for choosing update method
140+
*/
141+
const chooseUpdateMethod = async (latestVersion) => {
142+
const choices = [
143+
{ name: 'NPM', value: 'npm' },
144+
{ name: 'Homebrew', value: 'homebrew' },
145+
{ name: 'Show manual instructions', value: 'manual' }
146+
];
147+
148+
const { method } = await inquirer.prompt([
149+
{
150+
type: 'list',
151+
name: 'method',
152+
message: 'Could not detect installation method. How would you like to update?',
153+
choices: choices
154+
}
155+
]);
156+
157+
switch (method) {
158+
case 'npm':
159+
await updateViaNpm();
160+
break;
161+
case 'homebrew':
162+
await updateViaHomebrew();
163+
break;
164+
case 'manual':
165+
showManualInstructions(latestVersion);
166+
break;
167+
}
168+
};
169+
170+
/**
171+
* Main update function
172+
*/
173+
const updateCli = async ({ manual } = {}) => {
174+
try {
175+
const latestVersion = await getLatestVersion();
176+
177+
const comparison = compareVersions(version, latestVersion);
178+
179+
if (comparison === 0) {
180+
success(`You're already running the latest version (${chalk.bold(version)})!`);
181+
return;
182+
} else if (comparison < 0) {
183+
warn(`You're running a newer version (${chalk.bold(version)}) than the latest released version (${chalk.bold(latestVersion)}).`);
184+
hint("This might be a pre-release or development version.");
185+
return;
186+
}
187+
188+
log(`Updating from ${chalk.blue(version)} to ${chalk.green(latestVersion)}...`);
189+
console.log("");
190+
191+
if (manual) {
192+
showManualInstructions(latestVersion);
193+
return;
194+
}
195+
196+
if (isInstalledViaNpm()) {
197+
await updateViaNpm();
198+
} else if (isInstalledViaHomebrew()) {
199+
await updateViaHomebrew();
200+
} else {
201+
await chooseUpdateMethod(latestVersion);
202+
}
203+
204+
} catch (e) {
205+
console.log("");
206+
error(`Failed to check for updates: ${e.message}`);
207+
hint("You can manually check for updates at: https://github.com/{{ language.params.npmPackage|caseDash }}/releases");
208+
}
209+
};
210+
211+
const update = new Command("update")
212+
.description("Update the {{ spec.title|caseUcfirst }} CLI to the latest version")
213+
.option("--manual", "Show manual update instructions instead of auto-updating")
214+
.action(actionRunner(updateCli));
215+
216+
module.exports = {
217+
update
218+
};

templates/cli/lib/parser.js.twig

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,7 @@ const commandDescriptions = {
216216
"sites": `The sites command allows you to view, create and manage your Appwrite Sites.`,
217217
"storage": `The storage command allows you to manage your project files.`,
218218
"teams": `The teams command allows you to group users of your project to enable them to share read and write access to your project resources.`,
219+
"update": `The update command allows you to update the {{ spec.title|caseUcfirst }} CLI to the latest version.`,
219220
"users": `The users command allows you to manage your project users.`,
220221
"client": `The client command allows you to configure your CLI`,
221222
"login": `The login command allows you to authenticate and manage a user account.`,

templates/cli/lib/utils.js.twig

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,43 @@ const path = require("path");
33
const net = require("net");
44
const childProcess = require('child_process');
55
const chalk = require('chalk');
6+
const { fetch } = require("undici");
67
const { localConfig, globalConfig } = require("./config");
78

9+
/**
10+
* Get the latest version from npm registry
11+
*/
12+
async function getLatestVersion() {
13+
try {
14+
const response = await fetch('https://registry.npmjs.org/{{ language.params.npmPackage|caseDash }}/latest');
15+
if (!response.ok) {
16+
throw new Error(`HTTP ${response.status}`);
17+
}
18+
const data = await response.json();
19+
return data.version;
20+
} catch (e) {
21+
throw new Error(`Failed to fetch latest version: ${e.message}`);
22+
}
23+
}
24+
25+
/**
26+
* Compare versions using semantic versioning
27+
*/
28+
function compareVersions(current, latest) {
29+
const currentParts = current.split('.').map(Number);
30+
const latestParts = latest.split('.').map(Number);
31+
32+
for (let i = 0; i < Math.max(currentParts.length, latestParts.length); i++) {
33+
const currentPart = currentParts[i] || 0;
34+
const latestPart = latestParts[i] || 0;
35+
36+
if (latestPart > currentPart) return 1; // Latest is newer
37+
if (latestPart < currentPart) return -1; // Current is newer
38+
}
39+
40+
return 0; // Same version
41+
}
42+
843
function getAllFiles(folder) {
944
const files = [];
1045
for (const pathDir of fs.readdirSync(folder)) {
@@ -285,5 +320,7 @@ module.exports = {
285320
systemHasCommand,
286321
checkDeployConditions,
287322
showConsoleLink,
288-
isCloud
323+
isCloud,
324+
getLatestVersion,
325+
compareVersions
289326
};

0 commit comments

Comments
 (0)