add option to run script directly in desktop app
This commit is contained in:
@@ -14,9 +14,11 @@
|
|||||||
|
|
||||||
## Get started
|
## Get started
|
||||||
|
|
||||||
- Online version: [https://privacy.sexy](https://privacy.sexy)
|
- Online version at [https://privacy.sexy](https://privacy.sexy)
|
||||||
- or download latest desktop version for [Windows](https://github.com/undergroundwires/privacy.sexy/releases/download/0.9.2/privacy.sexy-Setup-0.9.2.exe), [Linux](https://github.com/undergroundwires/privacy.sexy/releases/download/0.9.2/privacy.sexy-0.9.2.AppImage), [macOS](https://github.com/undergroundwires/privacy.sexy/releases/download/0.9.2/privacy.sexy-0.9.2.dmg)
|
- 💡 No need to run any compiled software on your computer.
|
||||||
- 💡 Come back regularly to apply latest version for stronger privacy and security.
|
- Alternatively download offline version for [Windows](https://github.com/undergroundwires/privacy.sexy/releases/download/0.9.2/privacy.sexy-Setup-0.9.2.exe), [macOS](https://github.com/undergroundwires/privacy.sexy/releases/download/0.9.2/privacy.sexy-0.9.2.dmg) or [Linux](https://github.com/undergroundwires/privacy.sexy/releases/download/0.9.2/privacy.sexy-0.9.2.AppImage).
|
||||||
|
- 💡 Single click to execute your script.
|
||||||
|
- ❗ Come back regularly to apply latest version for stronger privacy and security.
|
||||||
|
|
||||||
[](https://privacy.sexy)
|
[](https://privacy.sexy)
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
export class Clipboard {
|
export class Clipboard {
|
||||||
public static copyText(text: string): void {
|
public static copyText(text: string): void {
|
||||||
const el = document.createElement('textarea');
|
const el = document.createElement('textarea');
|
||||||
|
|||||||
69
src/infrastructure/CodeRunner.ts
Normal file
69
src/infrastructure/CodeRunner.ts
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import { Environment } from '@/application/Environment/Environment';
|
||||||
|
import os from 'os';
|
||||||
|
import path from 'path';
|
||||||
|
import fs from 'fs';
|
||||||
|
import child_process from 'child_process';
|
||||||
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
|
|
||||||
|
export async function runCodeAsync(
|
||||||
|
code: string, folderName: string, fileExtension: string,
|
||||||
|
node = getNodeJs(), environment = Environment.CurrentEnvironment): Promise<void> {
|
||||||
|
const dir = node.path.join(node.os.tmpdir(), folderName);
|
||||||
|
await node.fs.promises.mkdir(dir, {recursive: true});
|
||||||
|
const filePath = node.path.join(dir, `run.${fileExtension}`);
|
||||||
|
await node.fs.promises.writeFile(filePath, code);
|
||||||
|
await node.fs.promises.chmod(filePath, '755');
|
||||||
|
const command = getExecuteCommand(filePath, environment);
|
||||||
|
node.child_process.exec(command);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getExecuteCommand(scriptPath: string, environment: Environment): string {
|
||||||
|
switch (environment.os) {
|
||||||
|
case OperatingSystem.macOS:
|
||||||
|
return `open -a Terminal.app ${scriptPath}`;
|
||||||
|
// Another option with graphical sudo would be
|
||||||
|
// `osascript -e "do shell script \\"${scriptPath}\\" with administrator privileges"`
|
||||||
|
// However it runs in background
|
||||||
|
case OperatingSystem.Windows:
|
||||||
|
return scriptPath;
|
||||||
|
default:
|
||||||
|
throw Error('undefined os');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getNodeJs(): INodeJs {
|
||||||
|
return { os, path, fs, child_process };
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface INodeJs {
|
||||||
|
os: INodeOs;
|
||||||
|
path: INodePath;
|
||||||
|
fs: INodeFs;
|
||||||
|
child_process: INodeChildProcess;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface INodeOs {
|
||||||
|
tmpdir(): string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface INodePath {
|
||||||
|
join(...paths: string[]): string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface INodeChildProcess {
|
||||||
|
exec(command: string): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface INodeFs {
|
||||||
|
readonly promises: INodeFsPromises;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface INodeFsPromisesMakeDirectoryOptions {
|
||||||
|
recursive?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface INodeFsPromises { // https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/node/v13/fs.d.ts
|
||||||
|
chmod(path: string, mode: string | number): Promise<void>;
|
||||||
|
mkdir(path: string, options: INodeFsPromisesMakeDirectoryOptions): Promise<string>;
|
||||||
|
writeFile(path: string, data: string): Promise<void>;
|
||||||
|
}
|
||||||
@@ -7,8 +7,7 @@ import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
|
|||||||
import { faFolderOpen, faFolder, faSmile } from '@fortawesome/free-regular-svg-icons';
|
import { faFolderOpen, faFolder, faSmile } from '@fortawesome/free-regular-svg-icons';
|
||||||
/** SOLID ICONS (PREFIX: fas (default)) */
|
/** SOLID ICONS (PREFIX: fas (default)) */
|
||||||
import { faTimes, faFileDownload, faCopy, faSearch, faInfoCircle, faUserSecret, faDesktop,
|
import { faTimes, faFileDownload, faCopy, faSearch, faInfoCircle, faUserSecret, faDesktop,
|
||||||
faTag, faGlobe, faSave, faBatteryFull, faBatteryHalf } from '@fortawesome/free-solid-svg-icons';
|
faTag, faGlobe, faSave, faBatteryFull, faBatteryHalf, faPlay } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
|
||||||
|
|
||||||
export class IconBootstrapper implements IVueBootstrapper {
|
export class IconBootstrapper implements IVueBootstrapper {
|
||||||
public bootstrap(vue: VueConstructor): void {
|
public bootstrap(vue: VueConstructor): void {
|
||||||
@@ -24,6 +23,7 @@ export class IconBootstrapper implements IVueBootstrapper {
|
|||||||
faTimes,
|
faTimes,
|
||||||
faFileDownload, faSave,
|
faFileDownload, faSave,
|
||||||
faCopy,
|
faCopy,
|
||||||
|
faPlay,
|
||||||
faSearch,
|
faSearch,
|
||||||
faBatteryFull, faBatteryHalf,
|
faBatteryFull, faBatteryHalf,
|
||||||
faInfoCircle);
|
faInfoCircle);
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="instructions">
|
<div class="instructions">
|
||||||
<!-- <p>
|
<p>
|
||||||
Since you're using online version of {{ this.appName }}, you will need to do additional steps after downloading the file to execute your script on macOS:
|
Since you're using online version of {{ this.appName }}, you will need to do additional steps after downloading the file to execute your script on macOS:
|
||||||
</p> -->
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<ol>
|
<ol>
|
||||||
<li>
|
<li>
|
||||||
@@ -73,9 +73,9 @@
|
|||||||
</li>
|
</li>
|
||||||
</ol>
|
</ol>
|
||||||
</p>
|
</p>
|
||||||
<!-- <p>
|
<p>
|
||||||
Or download the <a :href="this.macOsDownloadUrl">offline version</a> to run your scripts directly to skip these steps.
|
Or download the <a :href="this.macOsDownloadUrl">offline version</a> to run your scripts directly to skip these steps.
|
||||||
</p> -->
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="container" v-if="hasCode">
|
<div class="container" v-if="hasCode">
|
||||||
|
<IconButton
|
||||||
|
v-if="this.canRun"
|
||||||
|
text="Run"
|
||||||
|
v-on:click="executeCodeAsync"
|
||||||
|
icon-prefix="fas" icon-name="play">
|
||||||
|
</IconButton>
|
||||||
<IconButton
|
<IconButton
|
||||||
:text="this.isDesktopVersion ? 'Save' : 'Download'"
|
:text="this.isDesktopVersion ? 'Save' : 'Download'"
|
||||||
v-on:click="saveCodeAsync"
|
v-on:click="saveCodeAsync"
|
||||||
@@ -38,6 +44,8 @@ import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
|||||||
import { IApplicationCode } from '@/application/Context/State/Code/IApplicationCode';
|
import { IApplicationCode } from '@/application/Context/State/Code/IApplicationCode';
|
||||||
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
|
import { runCodeAsync } from '@/infrastructure/CodeRunner';
|
||||||
|
import { IApplicationContext } from '@/application/Context/IApplicationContext';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: {
|
components: {
|
||||||
@@ -48,8 +56,9 @@ import { OperatingSystem } from '@/domain/OperatingSystem';
|
|||||||
export default class TheCodeButtons extends StatefulVue {
|
export default class TheCodeButtons extends StatefulVue {
|
||||||
public readonly macOsModalName = 'macos-instructions';
|
public readonly macOsModalName = 'macos-instructions';
|
||||||
|
|
||||||
|
public readonly isDesktopVersion = Environment.CurrentEnvironment.isDesktop;
|
||||||
|
public canRun = false;
|
||||||
public hasCode = false;
|
public hasCode = false;
|
||||||
public isDesktopVersion = Environment.CurrentEnvironment.isDesktop;
|
|
||||||
public isMacOsCollection = false;
|
public isMacOsCollection = false;
|
||||||
public fileName = '';
|
public fileName = '';
|
||||||
|
|
||||||
@@ -64,8 +73,13 @@ export default class TheCodeButtons extends StatefulVue {
|
|||||||
this.$modal.show(this.macOsModalName);
|
this.$modal.show(this.macOsModalName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
public async executeCodeAsync() {
|
||||||
|
const context = await this.getCurrentContextAsync();
|
||||||
|
await executeCodeAsync(context);
|
||||||
|
}
|
||||||
|
|
||||||
protected handleCollectionState(newState: ICategoryCollectionState): void {
|
protected handleCollectionState(newState: ICategoryCollectionState): void {
|
||||||
|
this.canRun = this.isDesktopVersion && newState.collection.os === Environment.CurrentEnvironment.os;
|
||||||
this.isMacOsCollection = newState.collection.os === OperatingSystem.macOS;
|
this.isMacOsCollection = newState.collection.os === OperatingSystem.macOS;
|
||||||
this.fileName = buildFileName(newState.collection.scripting);
|
this.fileName = buildFileName(newState.collection.scripting);
|
||||||
this.react(newState.code);
|
this.react(newState.code);
|
||||||
@@ -108,6 +122,15 @@ function buildFileName(scripting: IScriptingDefinition) {
|
|||||||
}
|
}
|
||||||
return fileName;
|
return fileName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function executeCodeAsync(context: IApplicationContext) {
|
||||||
|
await runCodeAsync(
|
||||||
|
/*code*/ context.state.code.current,
|
||||||
|
/*appName*/ context.app.info.name,
|
||||||
|
/*fileExtension*/ context.state.collection.scripting.fileExtension,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
|
|||||||
249
tests/unit/infrastructure/CodeRunner.spec.ts
Normal file
249
tests/unit/infrastructure/CodeRunner.spec.ts
Normal file
@@ -0,0 +1,249 @@
|
|||||||
|
import { EnvironmentStub } from './../stubs/EnvironmentStub';
|
||||||
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
|
import 'mocha';
|
||||||
|
import { expect } from 'chai';
|
||||||
|
import { runCodeAsync } from '@/infrastructure/CodeRunner';
|
||||||
|
|
||||||
|
describe('CodeRunner', () => {
|
||||||
|
describe('runCodeAsync', () => {
|
||||||
|
it('creates temporary directory recursively', async () => {
|
||||||
|
// arrange
|
||||||
|
const expectedDir = 'expected-dir';
|
||||||
|
const folderName = 'privacy.sexy';
|
||||||
|
const context = new TestContext();
|
||||||
|
context.mocks.os.setupTmpdir('tmp');
|
||||||
|
context.mocks.path.setupJoin(expectedDir, 'tmp', folderName);
|
||||||
|
|
||||||
|
// act
|
||||||
|
await context
|
||||||
|
.withFolderName(folderName)
|
||||||
|
.runCodeAsync();
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expect(context.mocks.fs.mkdirHistory.length).to.equal(1);
|
||||||
|
expect(context.mocks.fs.mkdirHistory[0].isRecursive).to.equal(true);
|
||||||
|
expect(context.mocks.fs.mkdirHistory[0].path).to.equal(expectedDir);
|
||||||
|
});
|
||||||
|
it('creates a file with expected code and path', async () => {
|
||||||
|
// arrange
|
||||||
|
const expectedCode = 'expected-code';
|
||||||
|
const expectedFilePath = 'expected-file-path';
|
||||||
|
|
||||||
|
const extension = '.sh';
|
||||||
|
const expectedName = `run.${extension}`;
|
||||||
|
const folderName = 'privacy.sexy';
|
||||||
|
const context = new TestContext();
|
||||||
|
context.mocks.os.setupTmpdir('tmp');
|
||||||
|
context.mocks.path.setupJoin('folder', 'tmp', folderName);
|
||||||
|
context.mocks.path.setupJoin(expectedFilePath, 'folder', expectedName);
|
||||||
|
|
||||||
|
// act
|
||||||
|
await context
|
||||||
|
.withCode(expectedCode)
|
||||||
|
.withFolderName(folderName)
|
||||||
|
.withExtension(extension)
|
||||||
|
.runCodeAsync();
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expect(context.mocks.fs.writeFileHistory.length).to.equal(1);
|
||||||
|
expect(context.mocks.fs.writeFileHistory[0].data).to.equal(expectedCode);
|
||||||
|
expect(context.mocks.fs.writeFileHistory[0].path).to.equal(expectedFilePath);
|
||||||
|
});
|
||||||
|
it('set file permissions as expected', async () => {
|
||||||
|
// arrange
|
||||||
|
const expectedMode = '755';
|
||||||
|
const expectedFilePath = 'expected-file-path';
|
||||||
|
|
||||||
|
const extension = '.sh';
|
||||||
|
const expectedName = `run.${extension}`;
|
||||||
|
const folderName = 'privacy.sexy';
|
||||||
|
const context = new TestContext();
|
||||||
|
context.mocks.os.setupTmpdir('tmp');
|
||||||
|
context.mocks.path.setupJoin('folder', 'tmp', folderName);
|
||||||
|
context.mocks.path.setupJoin(expectedFilePath, 'folder', expectedName);
|
||||||
|
|
||||||
|
// act
|
||||||
|
await context
|
||||||
|
.withFolderName(folderName)
|
||||||
|
.withExtension(extension)
|
||||||
|
.runCodeAsync();
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expect(context.mocks.fs.chmodCallHistory.length).to.equal(1);
|
||||||
|
expect(context.mocks.fs.chmodCallHistory[0].mode).to.equal(expectedMode);
|
||||||
|
expect(context.mocks.fs.chmodCallHistory[0].path).to.equal(expectedFilePath);
|
||||||
|
});
|
||||||
|
describe('executes as expected', () => {
|
||||||
|
// arrange
|
||||||
|
const filePath = 'expected-file-path';
|
||||||
|
const testData = [ {
|
||||||
|
os: OperatingSystem.Windows,
|
||||||
|
expected: filePath,
|
||||||
|
}, {
|
||||||
|
os: OperatingSystem.macOS,
|
||||||
|
expected: `open -a Terminal.app ${filePath}`,
|
||||||
|
}];
|
||||||
|
for (const data of testData) {
|
||||||
|
it(`returns ${data.expected} on ${OperatingSystem[data.os]}`, async () => {
|
||||||
|
const context = new TestContext();
|
||||||
|
context.mocks.os.setupTmpdir('non-important-temp-dir-name');
|
||||||
|
context.mocks.path.setupJoinSequence('non-important-folder-name', filePath);
|
||||||
|
context.withOs(data.os);
|
||||||
|
|
||||||
|
// act
|
||||||
|
await context
|
||||||
|
.withOs(data.os)
|
||||||
|
.runCodeAsync();
|
||||||
|
|
||||||
|
// assert
|
||||||
|
expect(context.mocks.child_process.executionHistory.length).to.equal(1);
|
||||||
|
expect(context.mocks.child_process.executionHistory[0]).to.equal(data.expected);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
it('runs in expected order', async () => {
|
||||||
|
// arrange
|
||||||
|
const expectedOrder = [ NodeJsCommand.mkdir, NodeJsCommand.writeFile, NodeJsCommand.chmod ];
|
||||||
|
const context = new TestContext();
|
||||||
|
context.mocks.os.setupTmpdir('non-important-temp-dir-name');
|
||||||
|
context.mocks.path.setupJoinSequence('non-important-folder-name1', 'non-important-folder-name2');
|
||||||
|
|
||||||
|
// act
|
||||||
|
await context.runCodeAsync();
|
||||||
|
|
||||||
|
// assert
|
||||||
|
const actualOrder = context.mocks.commandHistory.filter((command) => expectedOrder.includes(command));
|
||||||
|
expect(expectedOrder).to.deep.equal(actualOrder);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
class TestContext {
|
||||||
|
public mocks = getNodeJsMocks();
|
||||||
|
|
||||||
|
private code: string = 'code';
|
||||||
|
private folderName: string = 'folderName';
|
||||||
|
private fileExtension: string = 'fileExtension';
|
||||||
|
private env = mockEnvironment(OperatingSystem.Windows);
|
||||||
|
|
||||||
|
public async runCodeAsync(): Promise<void> {
|
||||||
|
await runCodeAsync(this.code, this.folderName, this.fileExtension, this.mocks, this.env);
|
||||||
|
}
|
||||||
|
public withOs(os: OperatingSystem) {
|
||||||
|
this.env = mockEnvironment(os);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
public withFolderName(folderName: string) {
|
||||||
|
this.folderName = folderName;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
public withCode(code: string) {
|
||||||
|
this.code = code;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
public withExtension(fileExtension: string) {
|
||||||
|
this.fileExtension = fileExtension;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function mockEnvironment(os: OperatingSystem) {
|
||||||
|
return new EnvironmentStub().withOs(os);
|
||||||
|
}
|
||||||
|
|
||||||
|
const enum NodeJsCommand { tmpdir, join, exec, mkdir, writeFile, chmod }
|
||||||
|
|
||||||
|
function getNodeJsMocks() {
|
||||||
|
const commandHistory = new Array<NodeJsCommand>();
|
||||||
|
return {
|
||||||
|
os: mockOs(commandHistory),
|
||||||
|
path: mockPath(commandHistory),
|
||||||
|
fs: mockNodeFs(commandHistory),
|
||||||
|
child_process: mockChildProcess(commandHistory),
|
||||||
|
commandHistory,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function mockOs(commandHistory: NodeJsCommand[]) {
|
||||||
|
let tmpDir: string;
|
||||||
|
return {
|
||||||
|
setupTmpdir: (value: string): void => {
|
||||||
|
tmpDir = value;
|
||||||
|
},
|
||||||
|
tmpdir: (): string => {
|
||||||
|
if (!tmpDir) {
|
||||||
|
throw new Error('tmpdir not set up');
|
||||||
|
}
|
||||||
|
commandHistory.push(NodeJsCommand.tmpdir);
|
||||||
|
return tmpDir;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function mockPath(commandHistory: NodeJsCommand[]) {
|
||||||
|
const sequence = new Array<string>();
|
||||||
|
const scenarios = new Map<string, string>();
|
||||||
|
const getScenarioKey = (paths: string[]) => paths.join('|');
|
||||||
|
return {
|
||||||
|
setupJoin: (returnValue: string, ...paths: string[]): void => {
|
||||||
|
scenarios.set(getScenarioKey(paths), returnValue);
|
||||||
|
},
|
||||||
|
setupJoinSequence: (...valuesToReturn: string[]): void => {
|
||||||
|
sequence.push(...valuesToReturn);
|
||||||
|
sequence.reverse();
|
||||||
|
},
|
||||||
|
join: (...paths: string[]): string => {
|
||||||
|
commandHistory.push(NodeJsCommand.join);
|
||||||
|
if (sequence.length > 0) {
|
||||||
|
return sequence.pop();
|
||||||
|
}
|
||||||
|
const key = getScenarioKey(paths);
|
||||||
|
if (!scenarios.has(key)) {
|
||||||
|
return paths.join('/');
|
||||||
|
}
|
||||||
|
return scenarios.get(key);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function mockChildProcess(commandHistory: NodeJsCommand[]) {
|
||||||
|
const executionHistory = new Array<string>();
|
||||||
|
return {
|
||||||
|
exec: (command: string): void => {
|
||||||
|
commandHistory.push(NodeJsCommand.exec);
|
||||||
|
executionHistory.push(command);
|
||||||
|
},
|
||||||
|
executionHistory,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function mockNodeFs(commandHistory: NodeJsCommand[]) {
|
||||||
|
interface IMkdirCall { path: string; isRecursive: boolean; }
|
||||||
|
interface IWriteFileCall { path: string; data: string; }
|
||||||
|
interface IChmodCall { path: string; mode: string | number; }
|
||||||
|
const mkdirHistory = new Array<IMkdirCall>();
|
||||||
|
const writeFileHistory = new Array<IWriteFileCall>();
|
||||||
|
const chmodCallHistory = new Array<IChmodCall>();
|
||||||
|
return {
|
||||||
|
promises: {
|
||||||
|
mkdir: (path, options) => {
|
||||||
|
commandHistory.push(NodeJsCommand.mkdir);
|
||||||
|
mkdirHistory.push({ path, isRecursive: options && options.recursive });
|
||||||
|
return Promise.resolve(path);
|
||||||
|
},
|
||||||
|
writeFile: (path, data) => {
|
||||||
|
commandHistory.push(NodeJsCommand.writeFile);
|
||||||
|
writeFileHistory.push({ path, data });
|
||||||
|
return Promise.resolve();
|
||||||
|
},
|
||||||
|
chmod: (path, mode) => {
|
||||||
|
commandHistory.push(NodeJsCommand.chmod);
|
||||||
|
chmodCallHistory.push({ path, mode });
|
||||||
|
return Promise.resolve();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mkdirHistory,
|
||||||
|
writeFileHistory,
|
||||||
|
chmodCallHistory,
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user