codify-plugin-lib 1.0.180 → 1.0.181
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/build.d.ts +1 -0
- package/dist/bin/build.js +80 -0
- package/dist/bin/deploy-plugin.d.ts +2 -0
- package/dist/bin/deploy-plugin.js +8 -0
- package/dist/pty/background-pty.d.ts +1 -0
- package/dist/pty/background-pty.js +8 -2
- package/dist/resource/resource-settings.js +3 -2
- package/dist/scripts/deploy.d.ts +1 -0
- package/dist/scripts/deploy.js +2 -0
- package/dist/utils/codify-spawn.d.ts +29 -0
- package/dist/utils/codify-spawn.js +136 -0
- package/dist/utils/pty-local-storage.d.ts +0 -1
- package/package.json +16 -11
- package/src/pty/background-pty.ts +9 -2
- package/src/resource/resource-settings.ts +3 -2
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function build(): Promise<void>;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { Ajv } from 'ajv';
|
|
2
|
+
import { IpcMessageSchema, MessageStatus, ResourceSchema } from 'codify-schemas';
|
|
3
|
+
// @ts-ignore
|
|
4
|
+
import mergeJsonSchemas from 'merge-json-schemas';
|
|
5
|
+
import { fork } from 'node:child_process';
|
|
6
|
+
import fs from 'node:fs/promises';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import * as url from 'node:url';
|
|
9
|
+
import { codifySpawn } from '../utils/codify-spawn.js';
|
|
10
|
+
const ajv = new Ajv({
|
|
11
|
+
strict: true
|
|
12
|
+
});
|
|
13
|
+
const ipcMessageValidator = ajv.compile(IpcMessageSchema);
|
|
14
|
+
function sendMessageAndAwaitResponse(process, message) {
|
|
15
|
+
return new Promise((resolve, reject) => {
|
|
16
|
+
process.on('message', (response) => {
|
|
17
|
+
if (!ipcMessageValidator(response)) {
|
|
18
|
+
throw new Error(`Invalid message from plugin. ${JSON.stringify(message, null, 2)}`);
|
|
19
|
+
}
|
|
20
|
+
// Wait for the message response. Other messages such as sudoRequest may be sent before the response returns
|
|
21
|
+
if (response.cmd === message.cmd + '_Response') {
|
|
22
|
+
if (response.status === MessageStatus.SUCCESS) {
|
|
23
|
+
resolve(response.data);
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
reject(new Error(String(response.data)));
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
// Send message last to ensure listeners are all registered
|
|
31
|
+
process.send(message);
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
export async function build() {
|
|
35
|
+
await fs.rm('./dist', { force: true, recursive: true });
|
|
36
|
+
await codifySpawn('npm run rollup -- -f es');
|
|
37
|
+
const plugin = fork('./dist/index.js', [], {
|
|
38
|
+
// Use default true to test plugins in secure mode (un-able to request sudo directly)
|
|
39
|
+
detached: true,
|
|
40
|
+
env: { ...process.env },
|
|
41
|
+
execArgv: ['--import', 'tsx/esm'],
|
|
42
|
+
});
|
|
43
|
+
const initializeResult = await sendMessageAndAwaitResponse(plugin, {
|
|
44
|
+
cmd: 'initialize',
|
|
45
|
+
data: {}
|
|
46
|
+
});
|
|
47
|
+
const { resourceDefinitions } = initializeResult;
|
|
48
|
+
const resourceTypes = resourceDefinitions.map((i) => i.type);
|
|
49
|
+
const schemasMap = new Map();
|
|
50
|
+
for (const type of resourceTypes) {
|
|
51
|
+
const resourceInfo = await sendMessageAndAwaitResponse(plugin, {
|
|
52
|
+
cmd: 'getResourceInfo',
|
|
53
|
+
data: { type }
|
|
54
|
+
});
|
|
55
|
+
schemasMap.set(type, resourceInfo.schema);
|
|
56
|
+
}
|
|
57
|
+
const mergedSchemas = [...schemasMap.entries()].map(([type, schema]) => {
|
|
58
|
+
// const resolvedSchema = await $RefParser.dereference(schema)
|
|
59
|
+
const resourceSchema = JSON.parse(JSON.stringify(ResourceSchema));
|
|
60
|
+
delete resourceSchema.$id;
|
|
61
|
+
delete resourceSchema.$schema;
|
|
62
|
+
delete resourceSchema.title;
|
|
63
|
+
delete resourceSchema.oneOf;
|
|
64
|
+
delete resourceSchema.properties.type;
|
|
65
|
+
if (schema) {
|
|
66
|
+
delete schema.$id;
|
|
67
|
+
delete schema.$schema;
|
|
68
|
+
delete schema.title;
|
|
69
|
+
delete schema.oneOf;
|
|
70
|
+
}
|
|
71
|
+
return mergeJsonSchemas([schema ?? {}, resourceSchema, { properties: { type: { const: type, type: 'string' } } }]);
|
|
72
|
+
});
|
|
73
|
+
await fs.rm('./dist', { force: true, recursive: true });
|
|
74
|
+
await codifySpawn('npm run rollup'); // re-run rollup without building for es
|
|
75
|
+
console.log('Generated JSON Schemas for all resources');
|
|
76
|
+
const distFolder = path.resolve(path.dirname(url.fileURLToPath(import.meta.url)), '..', 'dist');
|
|
77
|
+
const schemaOutputPath = path.resolve(distFolder, 'schemas.json');
|
|
78
|
+
await fs.writeFile(schemaOutputPath, JSON.stringify(mergedSchemas, null, 2));
|
|
79
|
+
console.log('Successfully wrote schema to ./dist/schemas.json');
|
|
80
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import * as fs from 'node:fs';
|
|
4
|
+
import { build } from './build.js';
|
|
5
|
+
const packageJson = fs.readFileSync(path.join(process.env['npm_config_local_prefix'], 'package.json'), 'utf8');
|
|
6
|
+
const { name: libraryName, version: libraryVersion } = JSON.parse(packageJson);
|
|
7
|
+
console.log(libraryName, libraryVersion);
|
|
8
|
+
await build();
|
|
@@ -16,7 +16,7 @@ EventEmitter.defaultMaxListeners = 1000;
|
|
|
16
16
|
* without a tty (or even a stdin) attached so interactive commands will not work.
|
|
17
17
|
*/
|
|
18
18
|
export class BackgroundPty {
|
|
19
|
-
basePty = pty.spawn(
|
|
19
|
+
basePty = pty.spawn(this.getDefaultShell(), ['-i'], {
|
|
20
20
|
env: process.env, name: nanoid(6),
|
|
21
21
|
handleFlowControl: true
|
|
22
22
|
});
|
|
@@ -103,7 +103,10 @@ export class BackgroundPty {
|
|
|
103
103
|
await this.promiseQueue.run(async () => {
|
|
104
104
|
let outputBuffer = '';
|
|
105
105
|
return new Promise(resolve => {
|
|
106
|
-
|
|
106
|
+
// zsh-specific commands
|
|
107
|
+
if (this.getDefaultShell() === 'zsh') {
|
|
108
|
+
this.basePty.write('setopt hist_ignore_space;\n');
|
|
109
|
+
}
|
|
107
110
|
this.basePty.write(' unset PS1;\n');
|
|
108
111
|
this.basePty.write(' unset PS0;\n');
|
|
109
112
|
this.basePty.write(' echo setup complete\\"\n');
|
|
@@ -117,4 +120,7 @@ export class BackgroundPty {
|
|
|
117
120
|
});
|
|
118
121
|
});
|
|
119
122
|
}
|
|
123
|
+
getDefaultShell() {
|
|
124
|
+
return process.platform === 'darwin' ? 'zsh' : 'bash';
|
|
125
|
+
}
|
|
120
126
|
}
|
|
@@ -12,8 +12,9 @@ const ParameterEqualsDefaults = {
|
|
|
12
12
|
if (transformedB.startsWith('.')) { // Only relative paths start with '.'
|
|
13
13
|
transformedB = path.resolve(transformedB);
|
|
14
14
|
}
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
// macOS has case-insensitive filesystem by default, Linux is case-sensitive
|
|
16
|
+
const isCaseSensitive = process.platform === 'linux';
|
|
17
|
+
if (!isCaseSensitive) {
|
|
17
18
|
transformedA = transformedA.toLowerCase();
|
|
18
19
|
transformedB = transformedB.toLowerCase();
|
|
19
20
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { SpawnOptions } from 'node:child_process';
|
|
2
|
+
export declare enum SpawnStatus {
|
|
3
|
+
SUCCESS = "success",
|
|
4
|
+
ERROR = "error"
|
|
5
|
+
}
|
|
6
|
+
export interface SpawnResult {
|
|
7
|
+
status: SpawnStatus;
|
|
8
|
+
data: string;
|
|
9
|
+
}
|
|
10
|
+
type CodifySpawnOptions = {
|
|
11
|
+
cwd?: string;
|
|
12
|
+
throws?: boolean;
|
|
13
|
+
requiresRoot?: boolean;
|
|
14
|
+
requestsTTY?: boolean;
|
|
15
|
+
} & Omit<SpawnOptions, 'detached' | 'shell' | 'stdio'>;
|
|
16
|
+
/**
|
|
17
|
+
*
|
|
18
|
+
* @param cmd Command to run. Ex: `rm -rf`
|
|
19
|
+
* @param opts Standard options for node spawn. Additional argument:
|
|
20
|
+
* throws determines if a shell will throw a JS error. Defaults to true
|
|
21
|
+
*
|
|
22
|
+
* @see promiseSpawn
|
|
23
|
+
* @see spawn
|
|
24
|
+
*
|
|
25
|
+
* @returns SpawnResult { status: SUCCESS | ERROR; data: string }
|
|
26
|
+
*/
|
|
27
|
+
export declare function codifySpawn(cmd: string, opts?: CodifySpawnOptions): Promise<SpawnResult>;
|
|
28
|
+
export declare function isDebug(): boolean;
|
|
29
|
+
export {};
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { Ajv } from 'ajv';
|
|
2
|
+
import { MessageCmd, SudoRequestResponseDataSchema } from 'codify-schemas';
|
|
3
|
+
import { nanoid } from 'nanoid';
|
|
4
|
+
import { spawn } from 'node:child_process';
|
|
5
|
+
import stripAnsi from 'strip-ansi';
|
|
6
|
+
import { VerbosityLevel } from './utils.js';
|
|
7
|
+
import { SudoError } from '../errors.js';
|
|
8
|
+
const ajv = new Ajv({
|
|
9
|
+
strict: true,
|
|
10
|
+
});
|
|
11
|
+
const validateSudoRequestResponse = ajv.compile(SudoRequestResponseDataSchema);
|
|
12
|
+
export var SpawnStatus;
|
|
13
|
+
(function (SpawnStatus) {
|
|
14
|
+
SpawnStatus["SUCCESS"] = "success";
|
|
15
|
+
SpawnStatus["ERROR"] = "error";
|
|
16
|
+
})(SpawnStatus || (SpawnStatus = {}));
|
|
17
|
+
/**
|
|
18
|
+
*
|
|
19
|
+
* @param cmd Command to run. Ex: `rm -rf`
|
|
20
|
+
* @param opts Standard options for node spawn. Additional argument:
|
|
21
|
+
* throws determines if a shell will throw a JS error. Defaults to true
|
|
22
|
+
*
|
|
23
|
+
* @see promiseSpawn
|
|
24
|
+
* @see spawn
|
|
25
|
+
*
|
|
26
|
+
* @returns SpawnResult { status: SUCCESS | ERROR; data: string }
|
|
27
|
+
*/
|
|
28
|
+
export async function codifySpawn(cmd, opts) {
|
|
29
|
+
const throws = opts?.throws ?? true;
|
|
30
|
+
console.log(`Running command: ${cmd}` + (opts?.cwd ? `(${opts?.cwd})` : ''));
|
|
31
|
+
try {
|
|
32
|
+
// TODO: Need to benchmark the effects of using sh vs zsh for shell.
|
|
33
|
+
// Seems like zsh shells run slower
|
|
34
|
+
const result = await (opts?.requiresRoot
|
|
35
|
+
? externalSpawnWithSudo(cmd, opts)
|
|
36
|
+
: internalSpawn(cmd, opts ?? {}));
|
|
37
|
+
if (result.status !== SpawnStatus.SUCCESS) {
|
|
38
|
+
throw new Error(result.data);
|
|
39
|
+
}
|
|
40
|
+
return result;
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
if (isDebug()) {
|
|
44
|
+
console.error(`CodifySpawn error for command ${cmd}`, error);
|
|
45
|
+
}
|
|
46
|
+
// @ts-ignore
|
|
47
|
+
if (error.message?.startsWith('sudo:')) {
|
|
48
|
+
throw new SudoError(cmd);
|
|
49
|
+
}
|
|
50
|
+
if (throws) {
|
|
51
|
+
throw error;
|
|
52
|
+
}
|
|
53
|
+
if (error instanceof Error) {
|
|
54
|
+
return {
|
|
55
|
+
status: SpawnStatus.ERROR,
|
|
56
|
+
data: error.message,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
status: SpawnStatus.ERROR,
|
|
61
|
+
data: String(error),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
async function internalSpawn(cmd, opts) {
|
|
66
|
+
return new Promise((resolve) => {
|
|
67
|
+
const output = [];
|
|
68
|
+
// If TERM_PROGRAM=Apple_Terminal is set then ANSI escape characters may be included
|
|
69
|
+
// in the response.
|
|
70
|
+
const env = { ...process.env, ...opts.env, TERM_PROGRAM: 'codify', COMMAND_MODE: 'unix2003', COLORTERM: 'truecolor' };
|
|
71
|
+
const shell = getDefaultShell();
|
|
72
|
+
const rcFile = shell === 'zsh' ? '~/.zshrc' : '~/.bashrc';
|
|
73
|
+
// Source start up shells to emulate a users environment vs. a non-interactive non-login shell script
|
|
74
|
+
// Ignore all stdin
|
|
75
|
+
// If tty is requested then we'll need to sleep 1 to avoid race conditions. This is because if the terminal updates async after the tty message is
|
|
76
|
+
// displayed then it'll disappear. By adding sleep 1 it'll allow ink.js to finish all the updates before the tty message is shown
|
|
77
|
+
const _process = spawn(`source ${rcFile}; ${opts.requestsTTY ? 'sleep 1;' : ''}${cmd}`, [], {
|
|
78
|
+
...opts,
|
|
79
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
80
|
+
shell,
|
|
81
|
+
env
|
|
82
|
+
});
|
|
83
|
+
const { stdout, stderr, stdin } = _process;
|
|
84
|
+
stdout.setEncoding('utf8');
|
|
85
|
+
stderr.setEncoding('utf8');
|
|
86
|
+
stdout.on('data', (data) => {
|
|
87
|
+
output.push(data.toString());
|
|
88
|
+
});
|
|
89
|
+
stderr.on('data', (data) => {
|
|
90
|
+
output.push(data.toString());
|
|
91
|
+
});
|
|
92
|
+
_process.on('error', (data) => { });
|
|
93
|
+
// please node that this is not a full replacement for 'inherit'
|
|
94
|
+
// the child process can and will detect if stdout is a pty and change output based on it
|
|
95
|
+
// the terminal context is lost & ansi information (coloring) etc will be lost
|
|
96
|
+
if (stdout && stderr && VerbosityLevel.get() > 0) {
|
|
97
|
+
stdout.pipe(process.stdout);
|
|
98
|
+
stderr.pipe(process.stderr);
|
|
99
|
+
}
|
|
100
|
+
_process.on('close', (code) => {
|
|
101
|
+
resolve({
|
|
102
|
+
status: code === 0 ? SpawnStatus.SUCCESS : SpawnStatus.ERROR,
|
|
103
|
+
data: stripAnsi(output.join('\n')),
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
async function externalSpawnWithSudo(cmd, opts) {
|
|
109
|
+
return new Promise((resolve) => {
|
|
110
|
+
const requestId = nanoid(8);
|
|
111
|
+
const listener = (data) => {
|
|
112
|
+
if (data.requestId === requestId) {
|
|
113
|
+
process.removeListener('message', listener);
|
|
114
|
+
if (!validateSudoRequestResponse(data.data)) {
|
|
115
|
+
throw new Error(`Invalid response for sudo request: ${JSON.stringify(validateSudoRequestResponse.errors, null, 2)}`);
|
|
116
|
+
}
|
|
117
|
+
resolve(data.data);
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
process.on('message', listener);
|
|
121
|
+
process.send({
|
|
122
|
+
cmd: MessageCmd.SUDO_REQUEST,
|
|
123
|
+
data: {
|
|
124
|
+
command: cmd,
|
|
125
|
+
options: opts ?? {},
|
|
126
|
+
},
|
|
127
|
+
requestId
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
export function isDebug() {
|
|
132
|
+
return process.env.DEBUG != null && process.env.DEBUG.includes('codify'); // TODO: replace with debug library
|
|
133
|
+
}
|
|
134
|
+
function getDefaultShell() {
|
|
135
|
+
return process.platform === 'darwin' ? 'zsh' : 'bash';
|
|
136
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codify-plugin-lib",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.181",
|
|
4
4
|
"description": "Library plugin library",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"typings": "dist/index.d.ts",
|
|
@@ -10,42 +10,47 @@
|
|
|
10
10
|
"posttest": "tsc",
|
|
11
11
|
"prepublishOnly": "tsc"
|
|
12
12
|
},
|
|
13
|
+
"bin": {
|
|
14
|
+
"codify-deploy": "./dist/bin/deploy-plugin.js"
|
|
15
|
+
},
|
|
13
16
|
"keywords": [],
|
|
14
17
|
"author": "",
|
|
15
18
|
"license": "ISC",
|
|
16
19
|
"dependencies": {
|
|
20
|
+
"@homebridge/node-pty-prebuilt-multiarch": "^0.13.1",
|
|
21
|
+
"@npmcli/promise-spawn": "^7.0.1",
|
|
17
22
|
"ajv": "^8.12.0",
|
|
18
23
|
"ajv-formats": "^2.1.1",
|
|
24
|
+
"clean-deep": "^3.4.0",
|
|
19
25
|
"codify-schemas": "1.0.83",
|
|
20
|
-
"@npmcli/promise-spawn": "^7.0.1",
|
|
21
|
-
"@homebridge/node-pty-prebuilt-multiarch": "^0.12.0-beta.5",
|
|
22
|
-
"uuid": "^10.0.0",
|
|
23
26
|
"lodash.isequal": "^4.5.0",
|
|
24
27
|
"nanoid": "^5.0.9",
|
|
25
28
|
"strip-ansi": "^7.1.0",
|
|
26
|
-
"
|
|
29
|
+
"uuid": "^10.0.0"
|
|
27
30
|
},
|
|
28
31
|
"devDependencies": {
|
|
32
|
+
"@apidevtools/json-schema-ref-parser": "^11.7.2",
|
|
29
33
|
"@oclif/prettier-config": "^0.2.1",
|
|
30
34
|
"@oclif/test": "^3",
|
|
31
|
-
"@types/
|
|
35
|
+
"@types/lodash.isequal": "^4.5.8",
|
|
32
36
|
"@types/node": "^20",
|
|
37
|
+
"@types/npmcli__promise-spawn": "^6.0.3",
|
|
33
38
|
"@types/semver": "^7.5.4",
|
|
34
39
|
"@types/sinon": "^17.0.3",
|
|
35
40
|
"@types/uuid": "^10.0.0",
|
|
36
|
-
"@types/lodash.isequal": "^4.5.8",
|
|
37
41
|
"chai-as-promised": "^7.1.1",
|
|
38
|
-
"vitest": "^3.0.5",
|
|
39
|
-
"vitest-mock-extended": "^1.3.1",
|
|
40
|
-
"sinon": "^17.0.1",
|
|
41
42
|
"eslint": "^8.51.0",
|
|
42
43
|
"eslint-config-oclif": "^5",
|
|
43
44
|
"eslint-config-oclif-typescript": "^3",
|
|
45
|
+
"eslint-config-prettier": "^9.0.0",
|
|
46
|
+
"merge-json-schemas": "^1.0.0",
|
|
44
47
|
"shx": "^0.3.3",
|
|
48
|
+
"sinon": "^17.0.1",
|
|
45
49
|
"ts-node": "^10.9.1",
|
|
46
50
|
"tsc-watch": "^6.0.4",
|
|
47
51
|
"typescript": "^5",
|
|
48
|
-
"
|
|
52
|
+
"vitest": "^3.0.5",
|
|
53
|
+
"vitest-mock-extended": "^1.3.1"
|
|
49
54
|
},
|
|
50
55
|
"engines": {
|
|
51
56
|
"node": ">=18.0.0"
|
|
@@ -19,7 +19,7 @@ EventEmitter.defaultMaxListeners = 1000;
|
|
|
19
19
|
* without a tty (or even a stdin) attached so interactive commands will not work.
|
|
20
20
|
*/
|
|
21
21
|
export class BackgroundPty implements IPty {
|
|
22
|
-
private basePty = pty.spawn(
|
|
22
|
+
private basePty = pty.spawn(this.getDefaultShell(), ['-i'], {
|
|
23
23
|
env: process.env, name: nanoid(6),
|
|
24
24
|
handleFlowControl: true
|
|
25
25
|
});
|
|
@@ -127,7 +127,10 @@ export class BackgroundPty implements IPty {
|
|
|
127
127
|
let outputBuffer = '';
|
|
128
128
|
|
|
129
129
|
return new Promise(resolve => {
|
|
130
|
-
|
|
130
|
+
// zsh-specific commands
|
|
131
|
+
if (this.getDefaultShell() === 'zsh') {
|
|
132
|
+
this.basePty.write('setopt hist_ignore_space;\n');
|
|
133
|
+
}
|
|
131
134
|
this.basePty.write(' unset PS1;\n');
|
|
132
135
|
this.basePty.write(' unset PS0;\n')
|
|
133
136
|
this.basePty.write(' echo setup complete\\"\n')
|
|
@@ -142,4 +145,8 @@ export class BackgroundPty implements IPty {
|
|
|
142
145
|
})
|
|
143
146
|
})
|
|
144
147
|
}
|
|
148
|
+
|
|
149
|
+
private getDefaultShell(): string {
|
|
150
|
+
return process.platform === 'darwin' ? 'zsh' : 'bash';
|
|
151
|
+
}
|
|
145
152
|
}
|
|
@@ -350,8 +350,9 @@ const ParameterEqualsDefaults: Partial<Record<ParameterSettingType, (a: unknown,
|
|
|
350
350
|
transformedB = path.resolve(transformedB)
|
|
351
351
|
}
|
|
352
352
|
|
|
353
|
-
|
|
354
|
-
|
|
353
|
+
// macOS has case-insensitive filesystem by default, Linux is case-sensitive
|
|
354
|
+
const isCaseSensitive = process.platform === 'linux';
|
|
355
|
+
if (!isCaseSensitive) {
|
|
355
356
|
transformedA = transformedA.toLowerCase();
|
|
356
357
|
transformedB = transformedB.toLowerCase();
|
|
357
358
|
}
|