Initial commit

Signed-off-by: Avior <florian.bouillon@delta-wings.net>
This commit is contained in:
Florian Bouillon 2020-01-08 22:47:50 +01:00
commit d0f8f945f3
No known key found for this signature in database
GPG Key ID: B143FF27EF555D16
21 changed files with 2146 additions and 0 deletions

8
.editorconfig Normal file
View File

@ -0,0 +1,8 @@
root = true
[*]
indent_style = tab
indent_size = 4
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

7
.gitignore vendored Normal file
View File

@ -0,0 +1,7 @@
build/
.vscode/
backups/
node_modules/
*.js
yarn-error.log
package-lock.json

20
FileInterface.ts Normal file
View File

@ -0,0 +1,20 @@
import Requirements from "./Prerequises";
export default interface FileInterface {
displayName?: string,
filename: string // name of the file in the internal system **MUST** be unique
path?: string // if set it will copy the folder/file from path to filename
prereqs?: Requirements|Array<Requirements>
commands?: { // if set see below
save: string // one command to be launch and the result will be saved internally
load: string // one command to be launched on restoration
/*
{filepath} = path of the internal file
(if set on save the result content will be ignored)
load only (only one can be in):
{line} = one line in the internal file (if set the command will be launched for each lines)
// NOT IMPLEMENTED: {content} = the whole content of the file
*/
}
}

161
Functions.ts Normal file
View File

@ -0,0 +1,161 @@
import fsAsync, { promises as fs } from 'fs'
import os from 'os'
import { exec as execSync } from "child_process";
import util from 'util'
import readline from 'readline'
import Options from './Options';
import Logger from './Logger';
import Statics from './Statics';
const { Confirm } = require('enquirer')
const exec = util.promisify(execSync)
export function getUserHome() {
return os.homedir()
}
export async function processDir(src: string, dest: string) {
try {
await fs.mkdir(dest)
} catch {/*folder already exist*/}
const files = await fs.readdir(src)
for (const file of files) {
const path = `${src}/${file}`
const fileDest = `${dest}/${file}`
// console.log(`${src}/${file}`)
const stats = await fs.stat(path)
if (stats.isDirectory()) {
await processDir(path, fileDest)
continue
}
await processFile(path, fileDest)
}
}
async function processFile(src: string, dest: string) {
try {
await fs.access(dest)
if (!await confirmOverride(dest)) {
return
}
} catch {
/* File don't exist, continue */
}
const folder = dest.substring(0, dest.lastIndexOf("/"))
await mkdir(folder)
await fs.copyFile(src, dest)
}
export async function confirmOverride(filename: string): Promise<boolean> {
const doOverride = new Options().getConfig().override
if (typeof doOverride === "boolean" ) return doOverride
Statics.multibar.stop()
clear()
const resp = await new Confirm({
name: "override",
message: `the file ${filename} is gonna be overrided, continue?`
}).run()
if (!resp) {
return false
}
return true
}
async function mkdir(folder: string) {
try {
await fs.mkdir(folder, {recursive: true})
} catch {
/* folder exists */
}
}
export function clear() {
console.clear()
}
export async function copy(src: string, dest: string) {
try {
const stats = await fs.stat(src)
if (stats.isDirectory()) {
await processDir(src, dest)
} else {
await processFile(src, dest)
}
} catch {
Logger.getInstance().prepare(`File/Folder don't exist! ${src}`)
}
}
/**
*
* @param command The command to be launched
* @param location Location of the internal file
* @param isRestoring is the system restoring
*/
export async function processCommand(command: string, location: string, filename: string/*, isRestoring: boolean = false*/) {
const regex = new RegExp(/{(\w+)}/g)
// const res = regex.exec(command)
let filepathSet = false
let lineSet = false
let res
while ((res = regex.exec(command)) !== null) {
console.log(res, res[0], res[1])
if (res[1] === "filepath") {
command = command.replace(res[0], location)
filepathSet = true
}
if (res[1] === "line") {
lineSet = true
}
}
if (filepathSet && !await confirmOverride(filename)) {
return
}
if (lineSet) {
const stream = fsAsync.createReadStream(location)
const rl = readline.createInterface({
input: stream,
crlfDelay: Infinity
})
for await (const line of rl) {
await exec(command.replace("{line}", line))
}
return
}
try {
const {stdout, stderr} = await exec(command)
if (!filepathSet) {
const tmp = await fs.mkdtemp("dotfiles")
const path = `${tmp}/${filename}`
await fs.writeFile(path, stdout)
await processFile(path, location)
await fs.unlink(path)
await fs.rmdir(tmp)
}
} catch {
Logger.getInstance().prepare(`Error in ${filename}, Command errored`)
}
}
export async function getModules(): Promise<Array<string>> {
const res = []
let els = await fs.readdir("./modules")
for (const el of els) {
res.push(
el.substr(0, el.length-3)
)
}
return res
}
export function capitalize(str: string) {
return str[0].toUpperCase + str.substr(1).toLowerCase()
}

26
Logger.ts Normal file
View File

@ -0,0 +1,26 @@
export default class Logger {
private messages: Array<string> = []
private static instance?: Logger
public static getInstance(): Logger {
if (!this.instance) {
this.instance = new Logger()
}
return this.instance
}
public prepare(message: string) {
this.messages.push(message)
}
public commit() {
for (const message of this.messages) {
this.log(message)
}
}
public log(message: string) {
console.log(message)
}
}

12
ModuleInterface.ts Normal file
View File

@ -0,0 +1,12 @@
import { SingleBar } from "cli-progress";
import ListI from "./interfaces/Listr";
export default interface ModuleInterface {
moduleName?: string
save(): Promise<ListI>
load(): Promise<ListI>
isInstalled(): Promise<boolean>
custom(): Promise<boolean>
}

121
Options.ts Normal file
View File

@ -0,0 +1,121 @@
import { getUserHome, getModules } from "./Functions"
import fs, {promises as fsp} from "fs"
import 'colors'
const { MultiSelect, Select } = require('enquirer')
interface p {
override?: boolean
enabled: string[]
}
export default class Options {
private configFolder = `${getUserHome()}/.config/dzeio-dotfiles/`
private configFile = `config.yml`
private location: string
private config: p
public constructor() {
this.location = `${this.configFolder}/${this.configFile}`
try {
fs.accessSync(this.location)
this.config = JSON.parse(fs.readFileSync(this.location).toString())
} catch {
fs.mkdirSync(this.configFolder)
this.config = this.defaultConfig()
this.save()
}
}
private defaultConfig(): p {
return {
enabled: [
"Dotfiles"
]
}
}
public getConfig(): p {
return this.config
}
public setConfig(config: p) {
this.config = config
}
public async save() {
await fsp.writeFile(this.location, JSON.stringify(this.config))
}
public async manager() {
const res = await new Select({
name: 'select',
message: 'Select an option',
choices: [
'Quick backup elements',
'default file override',
'Back'
]
}).run()
switch (res) {
case 'Quick backup elements':
await this.enabled()
break
case 'default file override':
await this.override()
break
default:
return
}
await this.manager()
}
public async override() {
let config = this.getConfig()
const res = await new Select({
name: 'select',
message: 'Default Override action ?',
footer: `current: ${this.getConfig().override}`,
choices: [
{message: "Override", value: true},
{message: "Ask", value: undefined},
{message: "Skip", value: false}
]
}).run()
config.override = res
this.setConfig(config)
this.save()
}
public async enabled() {
let choices: Array<choice> = []
const els = await getModules()
let config = this.getConfig()
for (const value of els) {
choices.push({
name: value,
value
})
}
const t = await new MultiSelect({
name: 'enabled',
message: 'Select wich one to Backup/Restore when selecting quick'.white,
initial: this.getConfig().enabled.filter((el) => els.includes(el)),
choices,
footer: 'space to select, enter to confirm'
}).run()
config.enabled = t
this.setConfig(config)
this.save()
}
}
interface choice {
name: string,
value: string
}

6
Prerequises.ts Normal file
View File

@ -0,0 +1,6 @@
enum Requirements {
INSTALLED = "installed",
CUSTOM = "custom"
}
export default Requirements

171
SimpleModule.ts Normal file
View File

@ -0,0 +1,171 @@
import ModuleInterface from "./ModuleInterface"
import { copy, processCommand } from "./Functions"
import FileInterface from "./FileInterface"
import Requirements from "./Prerequises"
import Logger from "./Logger"
import ListI, { ListrInterface } from "./interfaces/Listr"
const Listr: ListI = require('listr')
export default abstract class SimpleModule implements ModuleInterface {
protected files: FileInterface[] = []
private installed?: boolean
private logger = Logger.getInstance()
abstract moduleName: string
public constructor(files?: FileInterface[]) {
if (files) this.files = files
}
public async save(): Promise<ListI> {
const directory = `./backups/${this.moduleName}`
const subTasks: ListrInterface[] = []
for (const file of this.files) {
// this.logger.prepare(file.displayName || file.filename)
const location = `${directory}/${file.filename}`
let doSkip = false
if (file.prereqs && !await this.processPrereqs(file)) {
doSkip = true
}
subTasks.push({
title: file.displayName || file.filename,
task: async (ctx, task) => {
// await wait(1000)
// console.log(task.)
// this.logger.prepare(task as any)
if (file.path !== undefined) {
await copy(file.path, location)
return
}
if (file.commands !== undefined) {
// console.log(file.commands.save, location, file.filename)
await processCommand(file.commands.save, location, file.filename)
return
}
},
skip: () => {
if (doSkip) {
const resp = `${this.moduleName} is not installed, skipping ${file.displayName || file.filename}`
this.logger.prepare(resp)
return resp
}
}
})
}
return new Listr(subTasks, {concurrent: true})
}
public async load(): Promise<ListI> {
const directory = `./backups/${this.moduleName}`
const subTasks: ListrInterface[] = []
for (const file of this.files) {
const location = `${directory}/${file.filename}`
let doSkip = false
if (file.prereqs && !await this.processPrereqs(file)) {
doSkip = true
}
subTasks.push({
title: file.displayName || file.filename,
skip: () => {
if (doSkip) {
const resp = `${this.moduleName} is not installed, skipping ${file.displayName || file.filename}`
this.logger.prepare(resp)
return resp
}
},
task: async () => {
if (file.path !== undefined) {
await copy(file.path, location)
return
}
if (file.commands !== undefined) {
await processCommand(file.commands.load, location, file.filename)
}
}
})
if (file.prereqs && !this.processPrereqs(file)) {
continue
}
}
return new Listr(subTasks, {concurrent: true})
}
public async isInstalled(): Promise<boolean> {
return true
}
public async custom(): Promise<boolean> {
return true
}
private async processPrereqs(file: FileInterface): Promise<boolean> {
let pre = file.prereqs
if (typeof pre === "undefined") return true
if (typeof pre === "string") {
pre = [pre]
}
for (const req of pre) {
let res = false
switch (req) {
case Requirements.INSTALLED:
if (typeof this.installed === "undefined") {
this.installed = await this.isInstalled()
}
res = this.installed
if (!res) {
// this.logger.prepare(`${this.moduleName} is not installed, skipping ${file.displayName || file.filename}`)
return false
}
break
case Requirements.CUSTOM:
res = await this.custom()
break
default:
break
}
if (!res) {
return false
}
}
return true
}
}
function wait(ms: number): Promise<void> {
return new Promise((resolve) => {
// resolve(); return
setTimeout(() => {resolve()}, ms)
})
}

20
Statics.ts Normal file
View File

@ -0,0 +1,20 @@
import { Presets, MultiBar } from "cli-progress";
export default class Statics {
private static _multibar: MultiBar
public static set multibar(bar: MultiBar) {
this._multibar = bar
}
public static get multibar() {
if (!this._multibar) this._multibar = new MultiBar({
format: `{obj}: {percentage}% (${'{bar}'.cyan}) {action}: {el}`,
clearOnComplete: false,
hideCursor: true,
synchronousUpdate: false
}, Presets.shades_classic)
return this._multibar
}
}

115
cli.ts Normal file
View File

@ -0,0 +1,115 @@
#!/usr/bin/env node
console.clear()
import Logger from './Logger'
import ModuleInterface from './ModuleInterface'
import Options from './Options'
import { getModules } from './Functions'
import ListI, { ListrInterface } from './interfaces/Listr'
const logger = Logger.getInstance()
const options = new Options()
function getSelect(Select: any) {
return new Select({
name: "dotfiles",
message: "Select the action to run",
choices: [
{
name: 'quick-save',
message: 'Quick Save'
},{
name: 'quick-restore',
message: 'Quick Restore'
},{
name: 'save',
message: 'Save'
},{
name: 'restore',
message: 'Restore'
},{
name: 'options',
message: 'Options'
},{
name: 'exit',
message: 'Exit'
}
]
})
}
async function saveModule(moduleName: string, save = false): Promise<ListI> {
const md = `./modules/${moduleName}`
const module: ModuleInterface = new (await require(md).default)(moduleName)
if (save) {
return module.save()
} else {
return module.load()
}
}
async function bootstrap(): Promise<never> {
const { Select, MultiSelect } = await require('enquirer');
const Listr: ListI = await require('listr')
let response: string = ""
try {
response = await getSelect(Select).run()
} catch {
process.exit(process.exitCode)
}
const config = options.getConfig()
const modules = await getModules()
const quick = config.enabled.filter((el) => modules.includes(el))
let isSaving = false
const t: ListrInterface[] = []
switch (response) {
case "quick-save":
isSaving = true
case "quick-restore":
for (const mod of quick) {
t.push({
title: mod,
task: async () => saveModule(mod, isSaving)
})
}
break;
case "save":
isSaving = true
case "restore":
const res = await new MultiSelect({
name: "mutiSelect",
message: `Select what to ${response}`,
choices: modules
}).run()
if (res.length === 0) await bootstrap()
for (const mod of res) {
t.push({
title: mod,
task: async () => saveModule(mod, isSaving)
})
}
break
case "options":
await options.manager()
await bootstrap()
default:
break;
}
await new Listr(t, {concurrent: false}).run()
Logger.getInstance().commit()
return process.exit(process.exitCode)
}
try {
bootstrap()
} catch(e) {
logger.log("An error occured 😱")
console.log(e)
}

19
interfaces/Listr.ts Normal file
View File

@ -0,0 +1,19 @@
export default interface ListI {
new (list: ListrInterface[], options?: ListrOptions): ListI
run: () => Promise<void>
}
export interface ListrOptions {
concurrent?: boolean
}
export interface ListrInterface {
title: string,
enabled?: (ctx: any) => boolean,
skip?: (ctx?: any) => string|undefined|boolean|Promise<string|undefined|boolean>,
task: (ctx?: any, task?: Task) => (void|string|ListI|Promise<void|string|ListI>)
}
export interface Task {
skip: (str: string) => boolean|string|undefined
}

14
modules/Fish.ts Normal file
View File

@ -0,0 +1,14 @@
import SimpleModule from "../SimpleModule";
import { getUserHome } from "../Functions";
import FileInterface from "../FileInterface";
export default class Fish extends SimpleModule {
files: FileInterface[] = [
{
displayName: "Functions",
filename: "functions",
path: `${getUserHome()}/.config/fish/functions`
}
]
moduleName = "Fish"
}

14
modules/HyperJS.ts Normal file
View File

@ -0,0 +1,14 @@
import SimpleModule from "../SimpleModule";
import { getUserHome } from "../Functions";
import FileInterface from "../FileInterface";
export default class HyperJS extends SimpleModule {
files: FileInterface[] = [
{
displayName: "Config",
filename: "hyper.js.bak",
path: `${getUserHome()}/.hyper.js`
}
]
moduleName = "HyperJS"
}

13
modules/Nano.ts Normal file
View File

@ -0,0 +1,13 @@
import SimpleModule from "../SimpleModule"
import FileInterface from "../FileInterface"
import { getUserHome } from "../Functions"
export default class Nano extends SimpleModule {
files: FileInterface[] = [
{
filename: "Nanorc",
path: `${getUserHome()}/.nanorc`
}
]
moduleName = "Nano"
}

32
modules/OhMyFish.ts Normal file
View File

@ -0,0 +1,32 @@
import SimpleModule from "../SimpleModule"
import FileInterface from "../FileInterface"
import Requirements from "../Prerequises"
import { execSync } from "child_process"
export default class OhMyFish extends SimpleModule {
files: FileInterface[] = [
{
filename: "Extensions",
prereqs: Requirements.INSTALLED,
commands: {
save: `${this.findCommand()} "omf list | tr '\\t' '\\n"' | grep '^[a-z-]' > {filepath}`,
load: `${this.findCommand()} "omg install {line}"`
}
}
]
moduleName = "OhMyFish"
private findCommand(): string {
try {
execSync('which fish 2> /dev/null')
execSync('which omf 2> /dev/null')
return 'fish -c'
} catch {
return ""
}
}
async isInstalled(): Promise<boolean> {
return this.findCommand() !== ""
}
}

79
modules/VSCode.ts Normal file
View File

@ -0,0 +1,79 @@
import SimpleModule from "../SimpleModule";
import fsSync, { promises as fs } from "fs";
import { getUserHome } from "../Functions";
import { execSync } from "child_process";
import FileInterface from "../FileInterface";
import Requirements from "../Prerequises";
import 'colors'
export default class VSCode extends SimpleModule {
files: FileInterface[] = [
{
displayName: "Settings",
filename: "settings.json",
path: `${this.findVSCodeFolder()}/User/settings.json`
},
{
displayName: "Keybindings",
filename: "keybindings.json",
path: `${this.findVSCodeFolder()}/User/keybindings.json`
},
{
displayName: "Snippets",
filename: "snippets",
path: `${this.findVSCodeFolder()}/User/snippets`
},
{
displayName: "Extensions",
filename: "extensions.txt",
prereqs: Requirements.INSTALLED,
commands: {
save: `${this.findCommand()} --list-extensions`,
load: `${this.findCommand()} --install-extension {line}`
}
}
]
public moduleName: string
public constructor() {
super()
const filename = __filename.split("/")
this.moduleName = filename[filename.length - 1].replace(".ts", "")
}
private static commands = ["vscodium", "vscode", "code", "codium"]
private findVSCodeFolder(): string {
const possibilities = [
`${getUserHome()}/.config/Code`,
`${getUserHome()}/.config/VSCodium`,
`${getUserHome()}/.config/Code - OSS`,
]
for (const pos of possibilities) {
try {
fsSync.accessSync(pos)
return pos
} catch {continue}
}
return possibilities[0] // default to VSCode folder
}
private findCommand(): string {
for (const cmd of VSCode.commands) {
try {
execSync(`which ${cmd}`)
return cmd
} catch {
continue
}
}
return ""
}
public async isInstalled(): Promise<boolean> {
return this.findCommand() !== ""
}
}

13
modules/Yarn.ts Normal file
View File

@ -0,0 +1,13 @@
import SimpleModule from "../SimpleModule";
import { getUserHome } from "../Functions";
import FileInterface from "../FileInterface";
export default class Yarn extends SimpleModule {
files: FileInterface[] = [
{
filename: "yarnrc",
path: `${getUserHome()}/.yarnrc`
}
]
moduleName = "Yarn"
}

30
package.json Normal file
View File

@ -0,0 +1,30 @@
{
"name": "dotfiles",
"version": "0.1.0",
"main": "build/cli.js",
"bin": {
"dotfiles": "./build/cli.js"
},
"license": "MIT",
"private": false,
"scripts": {
"start": "ts-node index.ts",
"build": "tsc"
},
"dependencies": {
"clear": "^0.1.0",
"cli-color": "^2.0.0",
"cli-progress": "^3.3.1",
"colors": "^1.4.0",
"enquirer": "^2.3.2",
"listr": "^0.14.3",
"ora": "^4.0.3"
},
"devDependencies": {
"@types/cli-progress": "^3.4.0",
"@types/node": "^13.1.1",
"ts-node": "^8.5.4",
"ts-node-dev": "^1.0.0-pre.44",
"typescript": "^3.7.4"
}
}

72
tsconfig.json Normal file
View File

@ -0,0 +1,72 @@
{
"include": ["cli.ts", "modules/*"],
"exclude": [
".vscode/",
"node_modules/",
"backups/"
],
"compilerOptions": {
/* Basic Options */
// "incremental": true, /* Enable incremental compilation */
"target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
// "lib": [], /* Specify library files to be included in the compilation. */
// "allowJs": true, /* Allow javascript files to be compiled. */
// "checkJs": true, /* Report errors in .js files. */
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
// "declaration": true, /* Generates corresponding '.d.ts' file. */
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
// "sourceMap": true, /* Generates corresponding '.map' file. */
// "outFile": "./dotfiles.js", /* Concatenate and emit output to single file. */
"outDir": "./build/", /* Redirect output structure to the directory. */
// "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
// "composite": true, /* Enable project compilation */
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
// "removeComments": true, /* Do not emit comments to output. */
// "noEmit": true, /* Do not emit outputs. */
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
/* Strict Type-Checking Options */
"strict": true, /* Enable all strict type-checking options. */
// "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* Enable strict null checks. */
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
/* Additional Checks */
// "noUnusedLocals": true, /* Report errors on unused locals. */
// "noUnusedParameters": true, /* Report errors on unused parameters. */
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
/* Module Resolution Options */
// "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
// "typeRoots": [], /* List of folders to include type definitions from. */
// "types": [], /* Type declaration files to be included in compilation. */
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
/* Source Map Options */
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
/* Experimental Options */
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
/* Advanced Options */
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
}
}

1193
yarn.lock Normal file

File diff suppressed because it is too large Load Diff