Skip to content

Commit 1afb7c8

Browse files
authored
Merge pull request #1 from fleetbase/dev-v0.0.3
v0.0.3 - Introduces 4 new commands - `bundle`, `bundle-upload`, `version-bump`, and `login`
2 parents 29bf353 + ec72f13 commit 1afb7c8

File tree

3 files changed

+2182
-50
lines changed

3 files changed

+2182
-50
lines changed

index.js

+271
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,16 @@ const { prompt } = require('enquirer');
66
const fs = require('fs-extra');
77
const path = require('path');
88
const axios = require('axios');
9+
const tar = require('tar');
10+
const semver = require('semver');
11+
const glob = require('glob-promise');
12+
const FormData = require('form-data');
913
const _ = require('lodash');
1014
const packageJson = require('./package.json');
1115
const maxBuffer = 1024 * 1024 * 50; // 50MB
1216
const defaultRegistry = 'https://registry.fleetbase.io';
1317
const packageLookupApi = 'https://api.fleetbase.io/~registry/v1/lookup';
18+
const bundleUploadApi = 'https://api.fleetbase.io/~registry/v1/bundle-upload';
1419
const starterExtensionRepo = 'https://github.com/fleetbase/starter-extension.git';
1520

1621
function publishPackage (packagePath, registry, options = {}) {
@@ -513,6 +518,234 @@ function runCommand (command, workingDirectory) {
513518
});
514519
}
515520

521+
// Function to bundle the extension
522+
async function bundleExtension (options) {
523+
const extensionPath = options.path || '.';
524+
const upload = options.upload;
525+
try {
526+
// Check if extension.json exists in the specified directory
527+
const extensionJsonPath = path.join(extensionPath, 'extension.json');
528+
if (!(await fs.pathExists(extensionJsonPath))) {
529+
console.error(`extension.json not found in ${extensionPath}`);
530+
process.exit(1);
531+
}
532+
// Read extension.json
533+
const extensionJson = await fs.readJson(extensionJsonPath);
534+
const name = extensionJson.name;
535+
const version = extensionJson.version;
536+
537+
if (!name || !version) {
538+
console.error('Name or version not specified in extension.json');
539+
process.exit(1);
540+
}
541+
// Build the bundle filename
542+
const nameDasherized = _.kebabCase(name.replace('@', ''));
543+
const bundleFilename = `${nameDasherized}-v${version}-bundle.tar.gz`;
544+
const bundlePath = path.join(extensionPath, bundleFilename);
545+
546+
// Exclude directories
547+
const excludeDirs = ['node_modules', 'server_vendor'];
548+
549+
console.log(`Creating bundle ${bundleFilename}...`);
550+
551+
await tar.c(
552+
{
553+
gzip: true,
554+
file: bundlePath,
555+
cwd: extensionPath,
556+
filter: (filePath, stat) => {
557+
// Exclude specified directories and the bundle file itself
558+
const relativePath = path.relative(extensionPath, filePath);
559+
560+
// Exclude directories
561+
if (excludeDirs.some(dir => relativePath.startsWith(dir + path.sep))) {
562+
return false; // exclude
563+
}
564+
565+
// Exclude the bundle file
566+
if (relativePath === bundleFilename) {
567+
return false; // exclude
568+
}
569+
570+
// Exclude any existing bundle files matching the pattern
571+
if (relativePath.match(/-v\d+\.\d+\.\d+(-[\w\.]+)?-bundle\.tar\.gz$/)) {
572+
return false; // exclude
573+
}
574+
575+
return true; // include
576+
},
577+
},
578+
['.']
579+
);
580+
581+
console.log(`Bundle created at ${bundlePath}`);
582+
583+
if (upload) {
584+
// Call upload function with the bundle path
585+
await uploadBundle(bundlePath, options);
586+
}
587+
} catch (error) {
588+
console.error(`Error bundling extension: ${error.message}`);
589+
process.exit(1);
590+
}
591+
}
592+
593+
// Function to upload the bundle
594+
async function uploadBundle (bundlePath, options) {
595+
const registry = options.registry || defaultRegistry;
596+
const uploadUrl = bundleUploadApi;
597+
598+
let authToken = options.authToken;
599+
if (!authToken) {
600+
// Try to get auth token from ~/.npmrc
601+
authToken = await getAuthToken(registry);
602+
if (!authToken) {
603+
console.error(`Auth token not found for registry ${registry}. Please provide an auth token using the --auth-token option.`);
604+
process.exit(1);
605+
}
606+
}
607+
608+
try {
609+
const form = new FormData();
610+
form.append('bundle', fs.createReadStream(bundlePath));
611+
612+
const response = await axios.post(uploadUrl, form, {
613+
headers: {
614+
...form.getHeaders(),
615+
Authorization: `Bearer ${authToken}`,
616+
},
617+
maxContentLength: Infinity,
618+
maxBodyLength: Infinity,
619+
});
620+
621+
console.log(`Bundle uploaded successfully: ${response.data.message}`);
622+
} catch (error) {
623+
console.log(error.response.data);
624+
console.error(`Error uploading bundle: ${error.response.data?.error ?? error.message}`);
625+
process.exit(1);
626+
}
627+
}
628+
629+
// Function to get the auth token from .npmrc
630+
async function getAuthToken (registryUrl) {
631+
const npmrcPath = path.join(require('os').homedir(), '.npmrc');
632+
if (!(await fs.pathExists(npmrcPath))) {
633+
return null;
634+
}
635+
636+
const npmrcContent = await fs.readFile(npmrcPath, 'utf-8');
637+
const lines = npmrcContent.split('\n');
638+
639+
const registryHost = new URL(registryUrl).host;
640+
641+
// Look for line matching //registry.fleetbase.io/:_authToken=...
642+
for (const line of lines) {
643+
const match = line.match(new RegExp(`^//${registryHost}/:_authToken=(.*)$`));
644+
if (match) {
645+
return match[1].replace(/^"|"$/g, ''); // Remove quotes if present
646+
}
647+
}
648+
649+
return null;
650+
}
651+
652+
// Function to find the latest bundle
653+
async function findLatestBundle (directory) {
654+
const pattern = '*-v*-bundle.tar.gz';
655+
const files = await glob(pattern, { cwd: directory });
656+
if (files.length === 0) {
657+
return null;
658+
}
659+
// Extract version numbers and sort
660+
const bundles = files
661+
.map(file => {
662+
const match = file.match(/-v(\d+\.\d+\.\d+(-[\w\.]+)?)-bundle\.tar\.gz$/);
663+
if (match) {
664+
const version = match[1];
665+
return { file, version };
666+
}
667+
return null;
668+
})
669+
.filter(Boolean);
670+
671+
if (bundles.length === 0) {
672+
return null;
673+
}
674+
675+
// Sort by version
676+
bundles.sort((a, b) => semver.compare(b.version, a.version));
677+
return bundles[0].file;
678+
}
679+
680+
// Command to handle the upload
681+
async function uploadCommand (bundleFile, options) {
682+
const directory = options.path || '.';
683+
const registry = options.registry || defaultRegistry;
684+
const authToken = options.authToken;
685+
686+
if (!bundleFile) {
687+
bundleFile = await findLatestBundle(directory);
688+
if (!bundleFile) {
689+
console.error('No bundle file found in the current directory.');
690+
process.exit(1);
691+
}
692+
}
693+
694+
const bundlePath = path.join(directory, bundleFile);
695+
696+
await uploadBundle(bundlePath, { registry, authToken });
697+
}
698+
699+
// Function to bump the version
700+
async function versionBump (options) {
701+
const extensionPath = options.path || '.';
702+
const releaseType = options.major ? 'major' : options.minor ? 'minor' : options.patch ? 'patch' : 'patch';
703+
const preRelease = options.preRelease;
704+
705+
const files = ['extension.json', 'package.json', 'composer.json'];
706+
for (const file of files) {
707+
const filePath = path.join(extensionPath, file);
708+
if (await fs.pathExists(filePath)) {
709+
const content = await fs.readJson(filePath);
710+
if (content.version) {
711+
let newVersion = semver.inc(content.version, releaseType, preRelease);
712+
if (!newVersion) {
713+
console.error(`Invalid version in ${file}: ${content.version}`);
714+
continue;
715+
}
716+
content.version = newVersion;
717+
await fs.writeJson(filePath, content, { spaces: 4 });
718+
console.log(`Updated ${file} to version ${newVersion}`);
719+
}
720+
}
721+
}
722+
}
723+
724+
// Command to handle login
725+
function loginCommand (options) {
726+
const npmLogin = require('npm-cli-login');
727+
const username = options.username;
728+
const password = options.password;
729+
const email = options.email;
730+
const registry = options.registry || defaultRegistry;
731+
const scope = options.scope || '';
732+
const quotes = options.quotes || '';
733+
const configPath = options.configPath || '';
734+
735+
if (!username || !password || !email) {
736+
console.error('Username, password, and email are required for login.');
737+
process.exit(1);
738+
}
739+
740+
try {
741+
npmLogin(username, password, email, registry, scope, quotes, configPath);
742+
console.log(`Logged in to registry ${registry}`);
743+
} catch (error) {
744+
console.error(`Error during login: ${error.message}`);
745+
process.exit(1);
746+
}
747+
}
748+
516749
program.name('flb').description('CLI tool for managing Fleetbase Extensions').version(`${packageJson.name} ${packageJson.version}`, '-v, --version', 'Output the current version');
517750
program.option('-r, --registry [url]', 'Specify a fleetbase extension repository', defaultRegistry);
518751

@@ -616,4 +849,42 @@ program
616849
console.log(`${packageJson.name} ${packageJson.version}`);
617850
});
618851

852+
program
853+
.command('bundle')
854+
.description('Bundle the Fleetbase extension into a tar.gz file')
855+
.option('-p, --path <path>', 'Path of the Fleetbase extension to bundle', '.')
856+
.option('-u, --upload', 'Upload the created bundle after bundling')
857+
.option('--auth-token <token>', 'Auth token for uploading the bundle')
858+
.action(bundleExtension);
859+
860+
program
861+
.command('bundle-upload [bundleFile]')
862+
.alias('upload-bundle')
863+
.description('Upload a Fleetbase extension bundle')
864+
.option('-p, --path <path>', 'Path where the bundle is located', '.')
865+
.option('--auth-token <token>', 'Auth token for uploading the bundle')
866+
.action(uploadCommand);
867+
868+
program
869+
.command('version-bump')
870+
.description('Bump the version of the Fleetbase extension')
871+
.option('-p, --path <path>', 'Path of the Fleetbase extension', '.')
872+
.option('--major', 'Bump major version')
873+
.option('--minor', 'Bump minor version')
874+
.option('--patch', 'Bump patch version')
875+
.option('--pre-release [identifier]', 'Add pre-release identifier')
876+
.action(versionBump);
877+
878+
program
879+
.command('login')
880+
.description('Log in to the Fleetbase registry')
881+
.option('-u, --username <username>', 'Username for the registry')
882+
.option('-p, --password <password>', 'Password for the registry')
883+
.option('-e, --email <email>', 'Email associated with your account')
884+
.option('-r, --registry <registry>', 'Registry URL', defaultRegistry)
885+
.option('--scope <scope>', 'Scope for the registry')
886+
.option('--quotes <quotes>', 'Quotes option for npm-cli-login')
887+
.option('--config-path <configPath>', 'Path to the npm config file')
888+
.action(loginCommand);
889+
619890
program.parse(process.argv);

0 commit comments

Comments
 (0)