Skip to content

Commit 2a8de70

Browse files
committed
fix(doctor): add install checks for various nvm edge cases
closes #281 - throw error if npm bin directory is not the same as the one used to install ghost-cli
1 parent d0225f8 commit 2a8de70

File tree

2 files changed

+167
-27
lines changed

2 files changed

+167
-27
lines changed

lib/commands/doctor/checks/install.js

+20-6
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ const eol = os.EOL;
1717

1818
const tasks = {
1919
// While it's not an actual task, we put it here to make it easier to test
20-
checkDirectoryAndAbove: function checkDirectoryAndAbove(dir) {
20+
checkDirectoryAndAbove: function checkDirectoryAndAbove(dir, extra) {
2121
if (isRoot(dir)) {
2222
return Promise.resolve();
2323
}
@@ -29,14 +29,24 @@ const tasks = {
2929
return Promise.reject(new errors.SystemError(
3030
`The path ${dir} is not readable by other users on the system.${eol}` +
3131
'This can cause issues with the CLI, please either make this directory ' +
32-
'readable by others or install in another location.'
32+
`readable by others or ${extra} in another location.`
3333
));
3434
}
3535

36-
return checkDirectoryAndAbove(path.join(dir, '../'));
36+
return tasks.checkDirectoryAndAbove(path.join(dir, '../'), extra);
3737
});
3838
},
39-
nodeVersion: function nodeVersion() {
39+
nodeVersion: function nodeVersion(ctx) {
40+
let globalBin = execa.shellSync('npm bin -g').stdout;
41+
42+
if (process.argv[1] !== path.join(__dirname, '../../../../bin/ghost') && !process.argv[1].startsWith(globalBin)) {
43+
return Promise.reject(new errors.SystemError(
44+
`The version of Ghost-CLI you are running was not installed with this version of Node.${eol}` +
45+
`This means there are likely two versions of Node running on your system, please ensure${eol}` +
46+
'that you are only running one global version of Node before continuing.'
47+
));
48+
}
49+
4050
if (process.env.GHOST_NODE_VERSION_CHECK !== 'false' &&
4151
!semver.satisfies(process.versions.node, cliPackage.engines.node)) {
4252
return Promise.reject(new errors.SystemError(
@@ -48,7 +58,11 @@ const tasks = {
4858
));
4959
}
5060

51-
return Promise.resolve();
61+
if (ctx.local || os.platform() !== 'linux' || (ctx.argv && ctx.argv['setup-linux-user'] === false)) {
62+
return Promise.resolve();
63+
}
64+
65+
return tasks.checkDirectoryAndAbove(process.argv[0], 'install node and Ghost-CLI');
5266
},
5367
folderPermissions: function folderPermissions(ctx) {
5468
return fs.access(process.cwd(), constants.R_OK | constants.W_OK).catch(() => {
@@ -61,7 +75,7 @@ const tasks = {
6175
return Promise.resolve();
6276
}
6377

64-
return tasks.checkDirectoryAndAbove(process.cwd());
78+
return tasks.checkDirectoryAndAbove(process.cwd(), 'run `ghost install`');
6579
});
6680
},
6781
systemStack: function systemStack(ctx) {

test/unit/commands/doctor/install-spec.js

+147-21
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ const expect = require('chai').expect;
33
const sinon = require('sinon');
44
const stripAnsi = require('strip-ansi');
55
const proxyquire = require('proxyquire').noCallThru();
6+
const path = require('path');
67

78
const modulePath = '../../../../lib/commands/doctor/checks/install';
89
const errors = require('../../../../lib/errors');
@@ -34,57 +35,182 @@ describe('Unit: Doctor Checks > Install', function () {
3435
});
3536

3637
describe('node version check', function () {
37-
it('doesn\'t do anything if GHOST_NODE_VERSION_CHECK is false', function () {
38-
let originalEnv = process.env;
38+
it('rejects if global bin is different than the one ghost is running from', function () {
39+
let execaStub = sinon.stub().returns({stdout: '/usr/local/bin'});
40+
let originalArgv = process.argv;
41+
process.argv = ['node', '/home/ghost/.nvm/versions/6.11.1/bin'];
42+
43+
const task = proxyquire(modulePath, {
44+
execa: {shellSync: execaStub}
45+
}).tasks.nodeVersion;
46+
47+
return task().then(() => {
48+
expect(false, 'error should be thrown').to.be.true;
49+
process.argv = originalArgv;
50+
}).catch((error) => {
51+
process.argv = originalArgv;
52+
expect(error).to.be.an.instanceof(errors.SystemError);
53+
expect(error.message).to.match(/version of Ghost-CLI you are running was not installed with this version of Node./);
54+
expect(execaStub.calledOnce).to.be.true;
55+
expect(execaStub.calledWithExactly('npm bin -g')).to.be.true;
56+
});
57+
});
58+
59+
it('rejects if node version is not in range', function () {
3960
let cliPackage = {
4061
engines: {
4162
node: '0.10.0'
4263
}
4364
};
44-
process.env = { GHOST_NODE_VERSION_CHECK: 'false' };
65+
let execaStub = sinon.stub().returns({stdout: process.argv[1]});
4566

4667
const task = proxyquire(modulePath, {
47-
'../../../../package': cliPackage
68+
'../../../../package': cliPackage,
69+
execa: {shellSync: execaStub}
4870
}).tasks.nodeVersion;
4971

5072
return task().then(() => {
51-
process.env = originalEnv;
73+
expect(false, 'error should be thrown').to.be.true;
74+
}).catch((error) => {
75+
expect(error).to.be.an.instanceof(errors.SystemError);
76+
let message = stripAnsi(error.message);
77+
78+
expect(message).to.match(/Supported: 0.10.0/);
79+
expect(message).to.match(new RegExp(`Installed: ${process.versions.node}`));
80+
expect(execaStub.calledOnce).to.be.true;
5281
});
5382
});
5483

55-
it('doesn\'t do anything if node version is in range', function () {
84+
it('doesn\'t reject if bin is the local ghost bin file from the install (and local is true)', function () {
5685
let cliPackage = {
5786
engines: {
5887
node: process.versions.node // this future-proofs the test
5988
}
6089
};
90+
let execaStub = sinon.stub().returns({stdout: '/usr/local/bin'});
91+
let originalArgv = process.argv;
92+
process.argv = ['node', path.join(__dirname, '../../../../bin/ghost')];
6193

62-
const task = proxyquire(modulePath, {
63-
'../../../../package': cliPackage
64-
}).tasks.nodeVersion;
94+
const tasks = proxyquire(modulePath, {
95+
'../../../../package': cliPackage,
96+
execa: {shellSync: execaStub}
97+
}).tasks;
98+
let checkDirectoryStub = sinon.stub(tasks, 'checkDirectoryAndAbove').resolves();
6599

66-
return task();
100+
return tasks.nodeVersion({local: true}).then(() => {
101+
process.argv = originalArgv;
102+
expect(execaStub.calledOnce).to.be.true;
103+
expect(checkDirectoryStub.called).to.be.false;
104+
});
67105
});
68106

69-
it('throws error if node version is not in range', function () {
107+
it('doesn\'t do anything if GHOST_NODE_VERSION_CHECK is false and local is true', function () {
108+
let originalEnv = process.env;
70109
let cliPackage = {
71110
engines: {
72111
node: '0.10.0'
73112
}
74113
};
114+
process.env = { GHOST_NODE_VERSION_CHECK: 'false' };
115+
let execaStub = sinon.stub().returns({stdout: process.argv[1]});
75116

76-
const task = proxyquire(modulePath, {
77-
'../../../../package': cliPackage
78-
}).tasks.nodeVersion;
117+
const tasks = proxyquire(modulePath, {
118+
'../../../../package': cliPackage,
119+
execa: {shellSync: execaStub}
120+
}).tasks;
121+
let checkDirectoryStub = sinon.stub(tasks, 'checkDirectoryAndAbove').resolves();
79122

80-
return task().then(() => {
81-
expect(false, 'error should be thrown').to.be.true;
82-
}).catch((error) => {
83-
expect(error).to.be.an.instanceof(errors.SystemError);
84-
let message = stripAnsi(error.message);
123+
return tasks.nodeVersion({local: true}).then(() => {
124+
process.env = originalEnv;
125+
expect(execaStub.calledOnce).to.be.true;
126+
expect(checkDirectoryStub.called).to.be.false;
127+
});
128+
});
85129

86-
expect(message).to.match(/Supported: 0.10.0/);
87-
expect(message).to.match(new RegExp(`Installed: ${process.versions.node}`));
130+
it('doesn\'t do anything if node version is in range and local is true', function () {
131+
let cliPackage = {
132+
engines: {
133+
node: process.versions.node // this future-proofs the test
134+
}
135+
};
136+
let execaStub = sinon.stub().returns({stdout: process.argv[1]});
137+
138+
const tasks = proxyquire(modulePath, {
139+
'../../../../package': cliPackage,
140+
execa: {shellSync: execaStub}
141+
}).tasks;
142+
let checkDirectoryStub = sinon.stub(tasks, 'checkDirectoryAndAbove').resolves();
143+
144+
return tasks.nodeVersion({local: true}).then(() => {
145+
expect(execaStub.calledOnce).to.be.true;
146+
expect(checkDirectoryStub.called).to.be.false;
147+
});
148+
});
149+
150+
it('doesn\'t call checkDirectoryAndAbove if os is not linux', function () {
151+
let cliPackage = {
152+
engines: {
153+
node: process.versions.node // this future-proofs the test
154+
}
155+
};
156+
let execaStub = sinon.stub().returns({stdout: process.argv[1]});
157+
let platformStub = sinon.stub().returns('darwin');
158+
159+
const tasks = proxyquire(modulePath, {
160+
'../../../../package': cliPackage,
161+
execa: {shellSync: execaStub},
162+
os: {platform: platformStub}
163+
}).tasks;
164+
let checkDirectoryStub = sinon.stub(tasks, 'checkDirectoryAndAbove').resolves();
165+
166+
return tasks.nodeVersion({local: false}).then(() => {
167+
expect(execaStub.calledOnce).to.be.true;
168+
expect(checkDirectoryStub.called).to.be.false;
169+
});
170+
});
171+
172+
it('doesn\'t call checkDirectoryAndAbove if no-setup-linux-user is passed', function () {
173+
let cliPackage = {
174+
engines: {
175+
node: process.versions.node // this future-proofs the test
176+
}
177+
};
178+
let execaStub = sinon.stub().returns({stdout: process.argv[1]});
179+
let platformStub = sinon.stub().returns('linux');
180+
181+
const tasks = proxyquire(modulePath, {
182+
'../../../../package': cliPackage,
183+
execa: {shellSync: execaStub},
184+
os: {platform: platformStub}
185+
}).tasks;
186+
let checkDirectoryStub = sinon.stub(tasks, 'checkDirectoryAndAbove').resolves();
187+
188+
return tasks.nodeVersion({local: false, argv: {'setup-linux-user': false}}).then(() => {
189+
expect(execaStub.calledOnce).to.be.true;
190+
expect(checkDirectoryStub.called).to.be.false;
191+
});
192+
});
193+
194+
it('calls checkDirectoryAndAbove if none of the three conditions are true', function () {
195+
let cliPackage = {
196+
engines: {
197+
node: process.versions.node // this future-proofs the test
198+
}
199+
};
200+
let execaStub = sinon.stub().returns({stdout: process.argv[1]});
201+
let platformStub = sinon.stub().returns('linux');
202+
203+
const tasks = proxyquire(modulePath, {
204+
'../../../../package': cliPackage,
205+
execa: {shellSync: execaStub},
206+
os: {platform: platformStub}
207+
}).tasks;
208+
let checkDirectoryStub = sinon.stub(tasks, 'checkDirectoryAndAbove').resolves();
209+
210+
return tasks.nodeVersion({local: false, argv: {'setup-linux-user': true}}).then(() => {
211+
expect(execaStub.calledOnce).to.be.true;
212+
expect(checkDirectoryStub.calledOnce).to.be.true;
213+
expect(checkDirectoryStub.calledWith(process.argv[0])).to.be.true;
88214
});
89215
});
90216
});

0 commit comments

Comments
 (0)