Skip to content

Commit 94210ed

Browse files
committed
feat(site): add site:deploy command for one-step project deployment
1 parent 8acb6ea commit 94210ed

File tree

7 files changed

+242
-138
lines changed

7 files changed

+242
-138
lines changed

bin/index.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { init } from '../src/commands/init.js';
66
import { startShell } from '../src/commands/shell.js';
77
import { PROJECT_NAME, getLatestVersion } from '../src/commons.js';
88
import { createApp } from '../src/commands/apps.js';
9+
import { deploy } from '../src/commands/deploy.js';
910

1011
async function main() {
1112
const version = await getLatestVersion(PROJECT_NAME);
@@ -62,6 +63,23 @@ async function main() {
6263
process.exit(0);
6364
});
6465

66+
/*/ Deploy command
67+
program
68+
.command('site:deploy')
69+
.description('Deploy a local web project to Puter')
70+
.argument('[name]', 'Name of the site')
71+
.argument('[remoteDir]', 'Remote directory path')
72+
.option('--subdomain <subdomain>', 'Subdomain for the site')
73+
.action(async (name, remoteDir, options) => {
74+
try {
75+
await deploy([name, remoteDir, `--subdomain=${options.subdomain}`].filter(Boolean));
76+
} catch (error) {
77+
console.error(chalk.red(error.message));
78+
}
79+
process.exit(0);
80+
});
81+
*/
82+
6583
if (process.argv.length === 2) {
6684
startShell();
6785
} else {

src/commands/deploy.js

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import chalk from 'chalk';
2+
import { generateAppName, isValidAppName } from '../commons.js';
3+
import { syncDirectory } from './files.js';
4+
import { createSite } from './sites.js';
5+
import { getCurrentDirectory } from './auth.js';
6+
import { getSubdomains } from './subdomains.js';
7+
8+
/**
9+
* Deploy a local web project to Puter.
10+
* @param {string[]} args - Command-line arguments (e.g., <valid_site_app> [<remote_dir>] [--subdomain=<subdomain>]).
11+
*/
12+
export async function deploy(args = []) {
13+
if (args.length < 1 || !isValidAppName(args[0])) {
14+
console.log(chalk.red('Usage: site:deploy <valid_site_app> [<remote_dir>] [--subdomain=<subdomain>]'));
15+
console.log(chalk.yellow('Example: site:deploy'));
16+
console.log(chalk.yellow('Example: site:deploy my-app'));
17+
console.log(chalk.yellow('Example: site:deploy my-app ./my-app'));
18+
console.log(chalk.yellow('Example: site:deploy my-app ./my-app --subdomain=my-app-new'));
19+
return;
20+
}
21+
const appName = args[0]; // && !args[0].startsWith('--') ? args[0] : generateAppName();
22+
const remoteDirArg = args.find(arg => !arg.startsWith('--') && arg !== appName);
23+
const remoteDir = remoteDirArg || '.'; // `./${appName}`;
24+
const subdomain = args.find(arg => arg.startsWith('--subdomain='))?.split('=')[1] || appName;
25+
const sourceDir = '.'; // Deploy from the current directory
26+
27+
console.log(chalk.cyan(`Deploying '${appName}' from local '${sourceDir}' to remote '${remoteDir}' at '${subdomain}.puter.site'...`));
28+
29+
try {
30+
// 1. Upload files
31+
console.log(chalk.cyan(`Uploading files from '${sourceDir}' to '${remoteDir}'...`));
32+
await syncDirectory([sourceDir, remoteDir, '--delete', '-r', '--overwrite']);
33+
34+
// 2. Create the site
35+
console.log(chalk.cyan(`Creating site...`));
36+
const site = await createSite([appName, remoteDir, `--subdomain=${subdomain}`]);
37+
38+
if (site) {
39+
console.log(chalk.green('Deployment successful!'));
40+
} else {
41+
console.log(chalk.yellow('Deployment successfuly updated!'));
42+
}
43+
if (subdomain){
44+
console.log(chalk.cyan('Your site is live at:'));
45+
console.log(chalk.green(`https://${subdomain}.puter.site`));
46+
} else {
47+
console.log(chalk.red('Deployment failed. Subdomain cannot be reserved!'));
48+
}
49+
} catch (error) {
50+
console.error(chalk.red(`Deployment failed: ${error.message}`));
51+
}
52+
}

src/commands/files.js

Lines changed: 34 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { minimatch } from 'minimatch';
77
import chalk from 'chalk';
88
import Conf from 'conf';
99
import fetch from 'node-fetch';
10-
import { API_BASE, BASE_URL, PROJECT_NAME, getHeaders, showDiskSpaceUsage, resolvePath } from '../commons.js';
10+
import { API_BASE, BASE_URL, PROJECT_NAME, getHeaders, showDiskSpaceUsage, resolvePath, resolveRemotePath } from '../commons.js';
1111
import { formatDateTime, formatSize, getSystemEditor } from '../utils.js';
1212
import inquirer from 'inquirer';
1313
import { getAuthToken, getCurrentDirectory, getCurrentUserName } from './auth.js';
@@ -1263,7 +1263,7 @@ async function ensureRemoteDirectoryExists(remotePath) {
12631263
* @param {string[]} args - Command-line arguments (e.g., [localDir, remoteDir, --delete, -r]).
12641264
*/
12651265
export async function syncDirectory(args = []) {
1266-
const usageMessage = 'Usage: update <local_directory> <remote_directory> [--delete] [-r]';
1266+
const usageMessage = 'Usage: update <local_directory> <remote_directory> [--delete] [-r] [--overwrite]';
12671267
if (args.length < 2) {
12681268
console.log(chalk.red(usageMessage));
12691269
return;
@@ -1273,11 +1273,13 @@ export async function syncDirectory(args = []) {
12731273
let remoteDir = '';
12741274
let deleteFlag = '';
12751275
let recursiveFlag = false;
1276+
let overwriteFlag = false;
12761277
try {
12771278
localDir = await resolveLocalDirectory(args[0]);
1278-
remoteDir = resolvePath(getCurrentDirectory(), args[1]);
1279+
remoteDir = resolveRemotePath(getCurrentDirectory(), args[1]);
12791280
deleteFlag = args.includes('--delete'); // Whether to delete extra files
12801281
recursiveFlag = args.includes('-r'); // Whether to recursively process subdirectories
1282+
overwriteFlag = args.includes('--overwrite');
12811283
} catch (error) {
12821284
console.error(chalk.red(error.message));
12831285
console.log(chalk.green(usageMessage));
@@ -1294,10 +1296,10 @@ export async function syncDirectory(args = []) {
12941296
}
12951297

12961298
// Step 2: Fetch remote directory contents
1297-
const remoteFiles = await listRemoteFiles(remoteDir);
1299+
let remoteFiles = await listRemoteFiles(remoteDir);
12981300
if (!Array.isArray(remoteFiles)) {
1299-
console.error(chalk.red('Failed to fetch remote directory contents.'));
1300-
return;
1301+
console.log(chalk.yellow('Remote directory is empty or does not exist. Continuing...'));
1302+
remoteFiles = [];
13011303
}
13021304

13031305
// Step 3: List local files
@@ -1311,30 +1313,34 @@ export async function syncDirectory(args = []) {
13111313
// Step 5: Handle conflicts (if any)
13121314
const conflicts = findConflicts(toUpload, toDownload);
13131315
if (conflicts.length > 0) {
1314-
1315-
console.log(chalk.yellow('The following files have conflicts:'));
1316-
conflicts.forEach(file => console.log(chalk.dim(`- ${file}`)));
1317-
1318-
const { resolve } = await inquirer.prompt([
1319-
{
1320-
type: 'list',
1321-
name: 'resolve',
1322-
message: 'How would you like to resolve conflicts?',
1323-
choices: [
1324-
{ name: 'Keep local version', value: 'local' },
1325-
{ name: 'Keep remote version', value: 'remote' },
1326-
{ name: 'Skip conflicting files', value: 'skip' }
1327-
]
1328-
}
1329-
]);
1330-
1331-
if (resolve === 'local') {
1316+
if (overwriteFlag) {
1317+
console.log(chalk.yellow('Overwriting existing files with local version.'));
13321318
filteredToDownload = filteredToDownload.filter(file => !conflicts.includes(file.relativePath));
1333-
} else if (resolve === 'remote') {
1334-
filteredToUpload = filteredToUpload.filter(file => !conflicts.includes(file.relativePath));
13351319
} else {
1336-
filteredToUpload = filteredToUpload.filter(file => !conflicts.includes(file.relativePath));
1337-
filteredToDownload = filteredToDownload.filter(file => !conflicts.includes(file.relativePath));
1320+
console.log(chalk.yellow('The following files have conflicts:'));
1321+
conflicts.forEach(file => console.log(chalk.dim(`- ${file}`)));
1322+
1323+
const { resolve } = await inquirer.prompt([
1324+
{
1325+
type: 'list',
1326+
name: 'resolve',
1327+
message: 'How would you like to resolve conflicts?',
1328+
choices: [
1329+
{ name: 'Keep local version', value: 'local' },
1330+
{ name: 'Keep remote version', value: 'remote' },
1331+
{ name: 'Skip conflicting files', value: 'skip' }
1332+
]
1333+
}
1334+
]);
1335+
1336+
if (resolve === 'local') {
1337+
filteredToDownload = filteredToDownload.filter(file => !conflicts.includes(file.relativePath));
1338+
} else if (resolve === 'remote') {
1339+
filteredToUpload = filteredToUpload.filter(file => !conflicts.includes(file.relativePath));
1340+
} else {
1341+
filteredToUpload = filteredToUpload.filter(file => !conflicts.includes(file.relativePath));
1342+
filteredToDownload = filteredToDownload.filter(file => !conflicts.includes(file.relativePath));
1343+
}
13381344
}
13391345
}
13401346

src/commands/init.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export async function init() {
2626
}
2727
]);
2828

29-
let jsFiles = ['puter-sdk'];
29+
let jsFiles = [];
3030
let jsDevFiles = [];
3131
let cssFiles = [];
3232
let jsExtraLibraries = [];

src/commands/sites.js

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import chalk from 'chalk';
22
import fetch from 'node-fetch';
33
import Table from 'cli-table3';
44
import { getCurrentUserName, getCurrentDirectory } from './auth.js';
5-
import { API_BASE, getHeaders, generateAppName, resolvePath, isValidAppName } from '../commons.js';
5+
import { API_BASE, getHeaders, generateAppName, resolveRemotePath, isValidAppName } from '../commons.js';
66
import { displayNonNullValues, formatDate, isValidAppUuid } from '../utils.js';
77
import { getSubdomains, createSubdomain, deleteSubdomain } from './subdomains.js';
88
import { ErrorAPI } from '../modules/ErrorModule.js';
@@ -98,7 +98,7 @@ export async function infoSite(args = []) {
9898
}
9999
}
100100

101-
/**
101+
/**
102102
* Delete hosted web site
103103
* @param {any[]} args Array of site uuid
104104
*/
@@ -109,6 +109,10 @@ export async function infoSite(args = []) {
109109
}
110110
for (const uuid of args)
111111
try {
112+
if (!uuid){
113+
console.log(chalk.yellow(`We could not find the site ID: ${uuid}`));
114+
return false;
115+
}
112116
// The uuid must be prefixed with: 'subdomainObj-'
113117
const response = await fetch(`${API_BASE}/delete-site`, {
114118
headers: getHeaders(),
@@ -136,7 +140,7 @@ export async function infoSite(args = []) {
136140
return true;
137141
}
138142

139-
/**
143+
/**
140144
* Create a static web app from the current directory to Puter cloud.
141145
* @param {string[]} args - Command-line arguments (e.g., [name, --subdomain=<subdomain>]).
142146
*/
@@ -151,8 +155,10 @@ export async function infoSite(args = []) {
151155

152156
const appName = args[0]; // Site name (required)
153157
const subdomainOption = args.find(arg => arg.toLocaleLowerCase().startsWith('--subdomain='))?.split('=')[1]; // Optional subdomain
158+
const remoteDirArg = (args[1] && !args[1].startsWith('--')) ? args[1] : '.';
159+
154160
// Use the current directory as the root directory if none specified
155-
const remoteDir = resolvePath(getCurrentDirectory(), (args[1] && !args[1].startsWith('--'))?args[1]:'.');
161+
const remoteDir = resolveRemotePath(getCurrentDirectory(), remoteDirArg);
156162

157163
console.log(chalk.dim(`Creating site ${chalk.green(appName)} from: ${chalk.green(remoteDir)}...\n`));
158164
try {
@@ -212,8 +218,10 @@ export async function infoSite(args = []) {
212218

213219
console.log(chalk.green(`App ${chalk.dim(appName)} created successfully and accessible at:`));
214220
console.log(chalk.cyan(`https://${site.subdomain}.puter.site`));
221+
return site;
215222
} catch (error) {
216223
console.error(chalk.red('Failed to create site.'));
217224
console.error(chalk.red(`Error: ${error.message}`));
225+
return null;
218226
}
219227
}

0 commit comments

Comments
 (0)