Skip to content

Commit

Permalink
#109 #122: lando shellenv
Browse files Browse the repository at this point in the history
* first pass on lando shellenv

* rework lando update to use new install paths and auto add to path if needed

* clean up

* clean up 2

* breakman

* breakman2
  • Loading branch information
pirog committed Feb 26, 2024
1 parent 0432f55 commit 93ec59b
Show file tree
Hide file tree
Showing 12 changed files with 269 additions and 3 deletions.
12 changes: 11 additions & 1 deletion lib/updates.js
Original file line number Diff line number Diff line change
Expand Up @@ -139,11 +139,12 @@ module.exports = class UpdateManager {
const arch = ['arm64', 'aarch64'].includes(process.arch) ? 'arm64' : 'x64';
const os = getOS();
const ext = process.platform === 'win32' ? '.exe' : '';
const slim = cli.slim ? '-slim' : '';

// @NOTE: should this always work?
const release = data.find(release => release.tag_name === `v${hv}`);
release.version = hv;
release.binary = `lando-${os}-${arch}-v${release.version}${ext}`;
release.binary = `lando-${os}-${arch}-v${release.version}${slim}${ext}`;
release.channel = release.prerelease ? 'edge' : 'stable';
// @NOTE: ditto
const asset = release.assets.find(asset => asset.name === release.binary);
Expand Down Expand Up @@ -242,6 +243,15 @@ module.exports = class UpdateManager {
fs.symlinkSync(dest, sl);
this.debug('symlinked @lando/cli %o to %o', dest, sl);

// is sl is not in PATH then attempt to add it
if (!require('../utils/is-in-path')(sl)) {
const shellPaths = require('../utils/get-shellenv-paths')(this._cli);
const shellEnv = require('../utils/get-shellenv')(shellPaths);
const rcFile = require('../utils/get-shell-profile')();
require('../utils/update-shell-profile')(rcFile, shellEnv);
this.debug('added %o to %o', shellEnv, rcFile);
}

// finish
task.title = `Updated @lando/cli to ${version}`;
resolve(data);
Expand Down
65 changes: 65 additions & 0 deletions tasks/shellenv.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
'use strict';

const os = require('os');
const {color} = require('listr2');

module.exports = lando => {
return {
command: 'shellenv',
level: 'tasks',
options: {
add: {
describe: 'Add to shell profile if blank lando will attempt discovery',
alias: ['a'],
string: true,
},
check: {
describe: 'Check to see if lando is in PATH',
alias: ['c'],
boolean: true,
},
},

run: async options => {
// does nothing on windows
if (process.platform === 'win32') throw new Error('shellenv is not available on Windows!');

// get shell paths from cli
// @NOTE: in lando 3 it _should_be impossible for this to be undefined but should be throw an error?
const shellPaths = require('../utils/get-shellenv-paths')(lando?.config?.cli);
const shellEnv = require('../utils/get-shellenv')(shellPaths);

// if add is passed in but is empty then attempt to discover
if (options.add !== undefined && options.add === '') {
options.add = require('../utils/get-shell-profile')();
options.a = options.add;
}

// if we are adding
if (options.add) {
if (shellEnv.length > 0) {
require('../utils/update-shell-profile')(options.add, shellEnv);
console.log(`Updated ${color.green(options.add)} to include:`);
console.log();
console.log(color.bold(shellEnv.map(line => line[0]).join(os.EOL)));
console.log();
console.log(`Open a new shell or run ${color.bold(`source ${options.add}`)} to load the changes`);
} else {
console.log(`Looks like ${color.green(options.add)} is already ready to go!`);
}

// if we are checking
} else if (options.check) {
const {entrypoint} = lando?.config?.cli ?? {};
if (require('../utils/is-in-path')(entrypoint)) {
console.log(`${color.green(entrypoint)} is in ${color.bold('PATH')}`);
// throw if not
} else throw new Error(`${color.red(entrypoint)} does not appear to be in ${color.bold('PATH')}!`);

// finally just print the thing
} else {
console.log(shellEnv.map(line => line[0]).join(os.EOL));
};
},
};
};
51 changes: 51 additions & 0 deletions test/get-user.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
'use strict';

const chai = require('chai');
const expect = chai.expect;
const sinon = require('sinon');

const getUser = require('../utils/get-user');

describe('get-user', function() {
let sandbox;

beforeEach(function() {
// Create a sinon sandbox to restore stubs/spies after each test
sandbox = sinon.createSandbox();
});

afterEach(function() {
// Restore the sandbox back to its original state
sandbox.restore();
});

it('should return "www-data" if no matching service is found', function() {
const info = [{service: 'not-matching'}];
expect(getUser('test-service', info)).to.equal('www-data');
});

it('should return "www-data" for a "no-api" docker-compose service', function() {
const info = [{service: 'test-service', type: 'docker-compose'}];
expect(getUser('test-service', info)).to.equal('www-data');
});

it('should return "www-data" if service.api is 4 but no user is specified', function() {
const info = [{service: 'test-service', api: 4}];
expect(getUser('test-service', info)).to.equal('www-data');
});

it('should return specified user if service.api is 4 and user is specified', function() {
const info = [{service: 'test-service', api: 4, user: 'custom-user'}];
expect(getUser('test-service', info)).to.equal('custom-user');
});

it('should return "www-data" if service.api is not 4 and no meUser is specified', function() {
const info = [{service: 'test-service', api: 3}];
expect(getUser('test-service', info)).to.equal('www-data');
});

it('should return specified meUser if service.api is not 4 and meUser is specified', function() {
const info = [{service: 'test-service', api: 3, meUser: 'custom-meUser'}];
expect(getUser('test-service', info)).to.equal('custom-meUser');
});
});
30 changes: 30 additions & 0 deletions utils/get-global-shell-profile.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
'use strict';

const path = require('path');

module.exports = () => {
const shellPath = process.env.SHELL;
if (!shellPath) {
console.error('SHELL environment variable is not set.');
return null;
}

// Extract the shell name from the SHELL environment variable
const shellName = path.basename(shellPath);

// Map common shell names to their profile file names
const shellRcMap = {
'bash': '/etc/bash.bashrc',
'zsh': ['/etc/zsh/zshrc', '/etc/zshrc'],
'fish': '/etc/fish/config.fish',
'csh': '/etc/csh.cshrc',
};

const rcFileName = shellRcMap[shellName];
if (!rcFileName) {
console.error(`Unsupported or unknown shell: ${shellName}`);
return null;
}

return rcFileName;
};
5 changes: 5 additions & 0 deletions utils/get-shell-profile.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
'use strict';

module.exports = () => {
return require('is-root')() ? require('./get-global-shell-profile')() : require('./get-user-shell-profile')();
};
11 changes: 11 additions & 0 deletions utils/get-shellenv-paths.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
'use strict';

const fs = require('fs');
const path = require('path');

module.exports = ({entrypoint, file, installPath}) => {
return [entrypoint, file, installPath]
.filter(p => typeof p === 'string' && p !== '')
.map(p => !fs.lstatSync(p).isDirectory() ? path.dirname(p) : p)
.filter(p => !process.env.PATH.split(path.delimiter).includes(p));
};
13 changes: 13 additions & 0 deletions utils/get-shellenv.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
'use strict';

const path = require('path');

module.exports = (paths = []) => {
if (paths.length > 0) {
return [
['# Lando'],
[`export PATH="${paths.join(path.delimiter)}\${PATH+${path.delimiter}$PATH}"; #landopath`, '#landopath'],
];
}
return [];
};
33 changes: 33 additions & 0 deletions utils/get-user-shell-profile.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
'use strict';

const os = require('os');
const path = require('path');

module.exports = () => {
const shellPath = process.env.SHELL;
if (!shellPath) {
console.error('SHELL environment variable is not set.');
return null;
}

// Extract the shell name from the SHELL environment variable
const shellName = path.basename(shellPath);

// Map common shell names to their profile file names
const shellRcMap = {
'bash': '.bashrc',
'zsh': '.zshrc',
'fish': '.config/fish/config.fish',
'csh': '.cshrc',
'tcsh': '.tcshrc',
'ksh': '.kshrc',
};

const rcFileName = shellRcMap[shellName];
if (!rcFileName) {
console.error(`Unsupported or unknown shell: ${shellName}`);
return null;
}

return path.join(os.homedir(), rcFileName);
};
10 changes: 10 additions & 0 deletions utils/is-in-path.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
'use strict';

const path = require('path');
const fs = require('fs');

module.exports = file => {
const dirstring = process?.env?.PATH ?? [];
const dirs = dirstring.split(path.delimiter);
return fs.lstatSync(file).isDirectory() ? dirs.includes(file) : dirs.includes(path.dirname(file));
};
2 changes: 1 addition & 1 deletion utils/read-file.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,6 @@ module.exports = (file, options = {}) => {
case 'json':
return require('jsonfile').readFileSync(file, options);
default:
// throw error
return fs.readFileSync(file, 'utf8');
}
};
34 changes: 34 additions & 0 deletions utils/update-shell-profile.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
'use strict';

const os = require('os');
const read = require('./read-file');
const write = require('./write-file');

module.exports = (file, updates = []) => {
try {
// get the content
const content = read(file);
// split into lines
const lines = content.split('\n');

// loops through the updates and add/update as needed
for (const [update, search] of updates) {
const index = lines.findIndex(line => line.includes(search) || line.includes(update));
if (index === -1) {
lines.push(update);
} else {
lines[index] = update;
}
}

// if the last element doesnt contain a newline
if (lines[lines.length - 1].includes(os.EOL)) lines.push();

// Write the modified content back to the file
write(file, lines.join(os.EOL));

// handle errors
} catch (error) {
throw new Error(`Failed to update file: ${error.message}`);
}
};
6 changes: 5 additions & 1 deletion utils/write-file.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,18 @@ module.exports = (file, data, options = {}) => {
// otherwise use the normal js-yaml dump
} else {
try {
return fs.writeFileSync(file, require('js-yaml').dump(data, options));
fs.writeFileSync(file, require('js-yaml').dump(data, options));
} catch (error) {
throw new Error(error);
}
}
break;
case '.json':
case 'json':
require('jsonfile').writeFileSync(file, data, options);
break;
default:
if (!fs.existsSync(file)) fs.mkdirSync(path.dirname(file), {recursive: true});
fs.writeFileSync(file, data, 'utf8');
}
};

0 comments on commit 93ec59b

Please sign in to comment.