├── .gitignore ├── README.md ├── deps.ts ├── license ├── main.ts ├── mod.ts ├── plugin ├── mod.ts ├── repository.ts ├── shell.ts └── symlink.ts ├── testing └── utils.test.ts ├── types.ts └── util ├── mod.ts └── util.ts /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # dfm (Dotfiles Manager written in TypeScript) 2 | 3 | 4 | 5 | Example: [My settings is here](https://github.com/kat0h/dotfiles/blob/master/bin/dot) 6 | 7 | ```typescript 8 | #!/usr/bin/env -S deno run -A --ext ts 9 | import Dfm from "https://deno.land/x/dfm/mod.ts"; 10 | import { Shell, Repository, Symlink } from "https://deno.land/x/dfm/plugin/mod.ts"; 11 | import { fromFileUrl } from "https://deno.land/std@0.149.0/path/mod.ts"; 12 | import { os } from "https://deno.land/x/dfm/util/mod.ts"; 13 | 14 | const dfm = new Dfm({ 15 | dotfilesDir: "~/dotfiles", 16 | dfmFilePath: fromFileUrl(import.meta.url), 17 | }); 18 | 19 | const links: [string, string][] = [ 20 | ["zshrc", "~/.zshrc"], 21 | ["tmux.conf", "~/.tmux.conf"], 22 | ["vimrc", "~/.vimrc"], 23 | ["vim", "~/.vim"], 24 | ["config/alacritty", "~/.config/alacritty"], 25 | ]; 26 | 27 | const cmds: string[] = [ 28 | "vim", 29 | "nvim", 30 | "clang", 31 | "curl", 32 | "wget", 33 | ]; 34 | 35 | let path: string[] = [ 36 | "~/.deno/bin", 37 | ] 38 | 39 | if (os() == "darwin") { 40 | path = path.concat([ 41 | "~/mybin", 42 | "/usr/local/opt/ruby/bin" 43 | ]) 44 | 45 | } else if (os() == "linux") { 46 | path = path.concat([]) 47 | } 48 | 49 | 50 | dfm.use( 51 | new Symlink(dfm, links), 52 | new Shell({ 53 | env_path: "~/.cache/env", 54 | cmds: cmds, 55 | path: path 56 | }), 57 | new Repository(dfm), 58 | ); 59 | dfm.end(); 60 | // vim:filetype=typescript 61 | ``` 62 | 63 | **WARNING** DFM is a experimental implementation based on my idea 64 | 65 | DFM is a dotfiles manager framework written in deno. This library is based on a 66 | new (?) design. 67 | 68 | - not a command just library 69 | - no DSL 70 | - declarative setting (?) 71 | - do not depends on your memory 72 | 73 | I had the following complaints with my previous Dotfiles manager 74 | 75 | - I always forget command arguments. 76 | - It is difficult to learn complex DSLs. 77 | - The installation is complicated and requires many dependencies. 78 | 79 | Deno's dependency resolution system has solved these problems brilliantly. 80 | 81 | A single file manages the configuration settings and the commands to execute. 82 | Deno automatically resolves dependencies. Since all configuration settings are 83 | written in Typescript, conditional branching by the OS can be easily described 84 | in a familiar way. 85 | 86 | ```typescript 87 | #!/usr/bin/env deno run -A 88 | import Dfm from "https://deno.land/x/dfm/mod.ts"; 89 | import { fromFileUrl } from "https://deno.land/std/path/mod.ts"; 90 | 91 | const dfm = new Dfm({ 92 | dotfilesDir: "~/dotfiles", 93 | dfmFilePath: fromFileUrl(import.meta.url), 94 | }); 95 | 96 | dfm.end(); 97 | ``` 98 | 99 | 1. Import Dfm module from deno.land 100 | 2. make instance of Dfm manager 101 | 3. run command with `Dfm.prototype.end()` 102 | 103 | Save the script as command.sh and run then you would get this help. 104 | 105 | ``` 106 | $ ./command.sh 107 | 108 | dfm(3) v0.3 109 | A dotfiles manager written in deno (typescript) 110 | 111 | USAGE: 112 | deno run -A [filename] [SUBCOMMANDS] 113 | 114 | SUBCOMMANDS: 115 | stat show status of settings 116 | list show list of settings 117 | sync apply settings 118 | help show this help 119 | ``` 120 | 121 | As it is, it cannot be used as a Dotfiles manager. DFM provides the following 122 | functions as plugins. 123 | 124 | - symlink.ts 125 | - Paste the specified symbolic link starting from the path specified by the 126 | dotfilesDir option. 127 | - cmdcheck.ts 128 | - Checks if the specified command exists in $PATH. 129 | - repository.ts 130 | - It provides a subcommand that executes git commands starting from 131 | dotfilesDir, a dir command that outputs dotfilesDir, and an edit command 132 | that opens the configuration file itself in $EDITOR. 133 | 134 | Please check the examples at the top of the page for specific usage. 135 | 136 | ## Command 137 | 138 | Suppose the configuration file described above is placed as dfm in a directory 139 | with $PATH. 140 | 141 | ``` 142 | $ dfm 143 | $ dfm help # Display help. All subcommands are listed here 144 | 145 | $ dfm stat # The stat() function implemented in Plugin is executed. For example, the Symlink Plugin checks if the link is properly posted and then calls the 146 | $ dfm list # The list of all loaded Plugins and the list() function implemented in the Plugin are called. 147 | $ dfm sync # Synchronizes the settings described in the configuration file with the actual PC; the sync() function implemented in the plugin is called 148 | ``` 149 | 150 | ![](https://user-images.githubusercontent.com/45391880/181022336-b752eecf-4c1c-495d-98b0-8d0c96f6ae50.png) 151 | If the configuration is correctly described, the `$ dfm list` command returns 152 | output similar to the above. 153 | 154 | ## Utility functions 155 | 156 | You can import these functions from `https://deno.land/x/dfm/util/mod.ts` 157 | 158 | - `expandTilde()` 159 | - expand "~/" 160 | - `resolvePath(path: string, basedir?: string)` 161 | - ~ $BASEDIR -> $HOME 162 | - ../ $BASEDIR -> $BASEDIR/../ 163 | - ./ $BASEDIR -> $BASEDIR 164 | - a $BASEDIR -> $BASEDIR/a 165 | - ./hoge/hugo -> join($(pwd), "./hoge/hugo") 166 | - /hoge/hugo -> "/hoge/hugo" 167 | - ~/hoge -> "$HOME/hugo" 168 | - `isatty()` 169 | - Same as isatty() in c language 170 | - `os()` 171 | - Determines for which OS Deno was built 172 | 173 | ## Security 174 | 175 | Deno imports and executes URLs described in the source code as is. While this 176 | feature is convenient, it can easily lead to a supply chain attack if used 177 | incorrectly, so care must be taken. In the case of `deno.land/x/`, since 178 | deno.land guarantees that the source code returned by the URL with a version 179 | number is immutable, you can ensure safety by specifying @ in the URL. In the 180 | above example, the version number is not attached to the URL for the sake of 181 | simplicity, but when actually using the URL, be sure to specify the version and 182 | import it. 183 | 184 | ## Author 185 | 186 | kotakato (@kat0h) 187 | 188 | ## License 189 | 190 | MIT 191 | -------------------------------------------------------------------------------- /deps.ts: -------------------------------------------------------------------------------- 1 | export { 2 | dirname, 3 | fromFileUrl, 4 | join, 5 | resolve, 6 | toFileUrl, 7 | } from "https://deno.land/std@0.145.0/path/mod.ts"; 8 | export { assertEquals } from "https://deno.land/std@0.145.0/testing/asserts.ts"; 9 | export { 10 | ensureDirSync, 11 | ensureSymlinkSync, 12 | } from "https://deno.land/std@0.145.0/fs/mod.ts"; 13 | export * as colors from "https://deno.land/std@0.145.0/fmt/colors.ts"; 14 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Kota Kato 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /main.ts: -------------------------------------------------------------------------------- 1 | import { colors } from "./deps.ts"; 2 | import { DfmOptions, Plugin, Subcmd, SubcmdOptions } from "./types.ts"; 3 | import { isatty, resolvePath } from "./util/util.ts"; 4 | const { blue, bold, green, red, yellow, setColorEnabled, inverse } = colors; 5 | 6 | const version = "v0.3"; 7 | 8 | export default class Dfm { 9 | private options: DfmOptions; 10 | private plugins: Plugin[] = []; 11 | private subcmds: Subcmd[]; 12 | 13 | dfmFilePath: string; 14 | dotfilesDir: string; 15 | 16 | constructor(options: { 17 | dotfilesDir: string; 18 | dfmFilePath: string; 19 | }) { 20 | this.dfmFilePath = resolvePath(options.dfmFilePath); 21 | this.dotfilesDir = resolvePath(options.dotfilesDir); 22 | 23 | this.options = parse_argment(Deno.args); 24 | // サブコマンドの定義 25 | this.subcmds = [ 26 | { 27 | // 状態を確認する 28 | name: "stat", 29 | info: "show status of settings", 30 | func: (options: SubcmdOptions) => { 31 | return this.cmd_base.bind(this)(options, "stat"); 32 | }, 33 | }, 34 | { 35 | name: "list", 36 | info: "show list of settings", 37 | func: (options: SubcmdOptions) => { 38 | // プラグインの一覧を表示 39 | console.log(inverse(blue(bold("PLUGINS")))); 40 | this.plugins.forEach((plugin) => { 41 | console.log(`・ ${plugin.name}`); 42 | }); 43 | console.log(); 44 | return this.cmd_base.bind(this)(options, "list"); 45 | }, 46 | }, 47 | { 48 | // 設定を同期する 49 | name: "sync", 50 | info: "apply settings", 51 | func: (options: SubcmdOptions) => { 52 | return this.cmd_base.bind(this)(options, "sync"); 53 | }, 54 | }, 55 | { 56 | // ヘルプを表示する 57 | name: "help", 58 | info: "show this help", 59 | func: this.cmd_help.bind(this), 60 | }, 61 | ]; 62 | } 63 | 64 | use(...plugins: Plugin[]) { 65 | // プラグインを登録する 66 | 67 | plugins.forEach((plugin) => { 68 | this.plugins.push(plugin); 69 | if (plugin.subcmds !== undefined) { 70 | plugin.subcmds.forEach((subcmd) => { 71 | this.subcmds.push({ 72 | name: subcmd.name, 73 | info: subcmd.info, 74 | func: subcmd.func.bind(plugin), 75 | }); 76 | }); 77 | } 78 | }); 79 | } 80 | 81 | async end() { 82 | // コマンドを実行する 83 | 84 | // もし他のコマンドにパイプされていた場合、エスケープシーケンスを利用しない 85 | if (!isatty()) { 86 | setColorEnabled(false); 87 | } 88 | // サブコマンドを実行 89 | if (this.options.subcmdOptions === undefined) { 90 | // 無引数で呼ばれた場合、ヘルプを表示する 91 | this.cmd_help({ cmdName: "help", args: [] }); 92 | } else { 93 | const subcmd = this.options.subcmdOptions; 94 | const cmd = this.subcmds.find((sc: Subcmd) => sc.name === subcmd.cmdName); 95 | if (cmd !== undefined) { 96 | const status = await cmd.func(subcmd); 97 | if (!status) { 98 | // コマンドの実行に失敗した場合、プロセスを終了する 99 | Deno.exit(1); 100 | } 101 | } else { 102 | // サブコマンドが見つからない場合、プロセスを終了する 103 | console.log(bold(red("Err: subcmd not found"))); 104 | Deno.exit(1); 105 | } 106 | } 107 | } 108 | 109 | private async cmd_base( 110 | _: SubcmdOptions, 111 | func: "stat" | "sync" | "list", 112 | ): Promise { 113 | // statとlist, syncは性質が似ているため、処理を共通化している 114 | 115 | const exit_status: { name: string; is_failed: boolean }[] = []; 116 | for (const s of this.plugins) { 117 | const command = s[func]; 118 | if (command != undefined) { 119 | console.log(inverse(blue(bold(s.name.toUpperCase())))); 120 | const is_failed = !(await command.bind(s)()); 121 | console.log(); 122 | exit_status.push({ name: s.name, is_failed: is_failed }); 123 | } 124 | } 125 | 126 | const noerr = exit_status.filter((s) => s.is_failed).length === 0; 127 | if (noerr) { 128 | console.log(bold(green("✔ NO Error was detected"))); 129 | return true; 130 | } else { 131 | console.log(bold(red("✘ Error was detected"))); 132 | exit_status.forEach((s) => { 133 | if (s.is_failed) { 134 | console.log(`・${s.name}`); 135 | } 136 | }); 137 | return false; 138 | } 139 | } 140 | 141 | private cmd_help(_: SubcmdOptions): boolean { 142 | // ヘルプ 143 | 144 | const p = console.log; 145 | p(inverse(yellow(bold(`dfm(3) ${version}`)))); 146 | p(" A dotfiles manager written in deno (typescript)\n"); 147 | p(inverse(yellow(bold("USAGE:")))); 148 | p(" deno run -A [filename] [SUBCOMMANDS]\n"); 149 | p(inverse(yellow(bold("SUBCOMMANDS:")))); 150 | this.subcmds.forEach((c) => { 151 | console.log(` ${green(c.name)} ${c.info}`); 152 | }); 153 | return true; 154 | } 155 | } 156 | 157 | function parse_argment(args: typeof Deno.args): DfmOptions { 158 | // コマンドライン引数を解析 159 | 160 | let subcmdOptions: SubcmdOptions | undefined = undefined; 161 | if (args.length !== 0) { 162 | subcmdOptions = { 163 | cmdName: Deno.args[0], 164 | args: Deno.args.slice(1), 165 | }; 166 | } 167 | 168 | return { 169 | subcmdOptions: subcmdOptions, 170 | }; 171 | } 172 | -------------------------------------------------------------------------------- /mod.ts: -------------------------------------------------------------------------------- 1 | import Dfm from "./main.ts"; 2 | export default Dfm; 3 | -------------------------------------------------------------------------------- /plugin/mod.ts: -------------------------------------------------------------------------------- 1 | import Symlink from "./symlink.ts"; 2 | import Shell from "./shell.ts"; 3 | import Repository from "./repository.ts"; 4 | 5 | export { Repository, Shell, Symlink }; 6 | -------------------------------------------------------------------------------- /plugin/repository.ts: -------------------------------------------------------------------------------- 1 | import Dfm from "../main.ts"; 2 | import { Plugin, SubcmdOptions } from "../types.ts"; 3 | import { resolvePath } from "../util/mod.ts"; 4 | 5 | // dotfilesのディレクトリを表示します 6 | // cd $(dfm dir) などのように使ってください 7 | export default class Repository implements Plugin { 8 | private dotfilesDir: string; 9 | private dfmFilePath: string; 10 | name = "dir"; 11 | 12 | constructor(dfm: Dfm) { 13 | this.dotfilesDir = resolvePath(dfm.dotfilesDir); 14 | this.dfmFilePath = resolvePath(dfm.dfmFilePath); 15 | } 16 | 17 | list() { 18 | console.log(`・ ${this.dotfilesDir}`); 19 | return true; 20 | } 21 | 22 | subcmds = [ 23 | { 24 | name: "dir", 25 | info: "print dotfiles dir", 26 | func: this.dir, 27 | }, 28 | { 29 | name: "git", 30 | info: "run git command in dotfiles directory", 31 | func: this.git, 32 | }, 33 | { 34 | name: "edit", 35 | info: "edit dotfiles with $EDITOR", 36 | func: this.edit, 37 | }, 38 | ]; 39 | 40 | private dir() { 41 | console.log(`${this.dotfilesDir}`); 42 | return true; 43 | } 44 | 45 | private async git(options: SubcmdOptions) { 46 | await Deno.run({ 47 | cmd: ["git", ...options.args], 48 | cwd: this.dotfilesDir, 49 | }).status(); 50 | return true; 51 | } 52 | 53 | private async edit(options: SubcmdOptions) { 54 | let editor = undefined; 55 | if (options.args.length === 0) { 56 | editor = Deno.env.get("EDITOR"); 57 | if (editor === undefined) { 58 | console.error( 59 | "$EDITOR is undefined, specify the command you want to use as an argument", 60 | ); 61 | return false; 62 | } 63 | } else { 64 | editor = options.args[0]; 65 | } 66 | await Deno.run({ 67 | cmd: [editor, resolvePath(this.dfmFilePath)], 68 | cwd: this.dotfilesDir, 69 | }).status(); 70 | return true; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /plugin/shell.ts: -------------------------------------------------------------------------------- 1 | import { Plugin } from "../types.ts"; 2 | import { colors } from "../deps.ts"; 3 | import { resolvePath } from "../util/mod.ts" 4 | import { ensureFileSync } from "https://deno.land/std@0.151.0/fs/mod.ts"; 5 | const { green, red, bold } = colors; 6 | 7 | // コマンドの存在を command -v を用いてチェックします 8 | export default class Shell implements Plugin { 9 | private env_path: string | undefined; 10 | 11 | private cmds: string[] = []; 12 | private path: string[] = []; 13 | 14 | name = "shell"; 15 | 16 | constructor(opts: { 17 | env_path?: string; 18 | cmds?: string[]; 19 | path?: string[]; 20 | }) { 21 | if (opts.env_path != undefined) 22 | this.env_path = resolvePath(opts.env_path) 23 | opts.cmds?.forEach((cmd) => { 24 | this.cmds.push(cmd); 25 | }); 26 | opts.path?.forEach((path) => { 27 | this.path.push(resolvePath(path)); 28 | }); 29 | } 30 | 31 | async stat() { 32 | console.log(bold('env_path')) 33 | console.log(`・ ${this.env_path}`) 34 | 35 | console.log() 36 | console.log(bold("COMMAND")) 37 | const p: { cmd: string; promise: Promise }[] = []; 38 | this.cmds.forEach((cmd) => { 39 | p.push({ 40 | cmd: cmd, 41 | promise: Deno.run({ 42 | cmd: ["sh", "-c", `command -v '${cmd}'`], 43 | stdin: "null", 44 | stdout: "null", 45 | stderr: "null", 46 | }).status(), 47 | }); 48 | }); 49 | 50 | const succe: string[] = []; 51 | const fails: string[] = []; 52 | 53 | for (const i of p) { 54 | if ((await i.promise).success) { 55 | succe.push(i.cmd); 56 | } else { 57 | fails.push(i.cmd); 58 | } 59 | } 60 | 61 | if (succe.length !== 0) { 62 | console.log(`${green("✔︎ ")} ${succe}`); 63 | } 64 | if (fails.length !== 0) { 65 | console.log(`${red("✘ ")} ${fails}`); 66 | } 67 | 68 | if (fails.length === 0) { 69 | return true; 70 | } else { 71 | return false; 72 | } 73 | } 74 | 75 | sync() { 76 | if (this.env_path === undefined) { 77 | if (this.path.length > 0) { 78 | console.error(`PATH was passed, but env_path wasn't setted`); 79 | return false 80 | } else { 81 | return true; 82 | } 83 | } 84 | 85 | const buffer: string[] = [] 86 | buffer.push(`# ${this.env_path}: Generated by dfm Shell plugin}`) 87 | buffer.push(`# on ${Date()}`) 88 | 89 | this.path.forEach((path) => { 90 | // add path 91 | // TODO: sanitize PATH 92 | buffer.push( 93 | `export PATH='${path.replace(/\\/, "\\\\").replace(/'/, `\\\'`)}':$PATH` 94 | ) 95 | }) 96 | 97 | ensureFileSync(this.env_path); 98 | Deno.writeTextFileSync(this.env_path, buffer.join("\n")); 99 | console.log("OK") 100 | 101 | return true; 102 | } 103 | 104 | list() { 105 | console.log(bold("COMMANDS")); 106 | console.log(`・ ${this.cmds}`); 107 | console.log(); 108 | 109 | console.log(bold("PATH")); 110 | this.path.forEach((path) => { 111 | console.log(`・ ${path}`); 112 | }); 113 | return true; 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /plugin/symlink.ts: -------------------------------------------------------------------------------- 1 | import Dfm from "../main.ts"; 2 | import { Plugin } from "../types.ts"; 3 | import { 4 | colors, 5 | dirname, 6 | ensureDirSync, 7 | ensureSymlinkSync, 8 | fromFileUrl, 9 | toFileUrl, 10 | } from "../deps.ts"; 11 | import { resolvePath } from "../util/mod.ts"; 12 | const { green, red, gray } = colors; 13 | 14 | export default class Symlink implements Plugin { 15 | name = "symlink"; 16 | 17 | // links[n][0]: 実体 links[n][1]: シンボリックリンク 18 | private links: { from: URL; to: URL }[] = []; 19 | private dotfilesDir: string; 20 | 21 | constructor(dfm: Dfm, links: [string, string][]) { 22 | // set dotfiles basedir 23 | this.dotfilesDir = resolvePath(dfm.dotfilesDir); 24 | // 作成するシンボリックリンクを登録 25 | links.forEach((link) => { 26 | const from_url = toFileUrl( 27 | resolvePath(link[0], this.dotfilesDir), 28 | ); 29 | const to_url = toFileUrl(resolvePath(link[1])); 30 | this.links.push({ 31 | from: from_url, 32 | to: to_url, 33 | }); 34 | }); 35 | } 36 | 37 | stat() { 38 | // link()で指定されたリンクが正常に貼られているかを確認 39 | const stat = check_symlinks(this.links); 40 | return stat; 41 | } 42 | 43 | list() { 44 | this.links.forEach((link) => { 45 | console.log(`・ ${link.from.pathname} → ${link.to.pathname}`); 46 | }); 47 | return true; 48 | } 49 | 50 | sync() { 51 | // リンクが存在していなければ貼る 52 | let status = ensure_make_symlinks(this.links); 53 | if (status) { 54 | console.log("OK"); 55 | return true; 56 | } else { 57 | return false; 58 | } 59 | } 60 | } 61 | 62 | function check_symlinks(links: { from: URL; to: URL }[]): boolean { 63 | let stat = true; 64 | links.forEach((link) => { 65 | const ok = check_symlink(link); 66 | if (!ok) { 67 | stat = false; 68 | console.log( 69 | `・ ${ok ? green("✔ ") : red("✘ ")} ${fromFileUrl(link.from)} → ${ 70 | fromFileUrl(link.to) 71 | }`, 72 | ); 73 | } 74 | }); 75 | if (stat) { 76 | console.log("OK"); 77 | } 78 | return stat; 79 | } 80 | 81 | // シンボリックリンクが適切に作成されているかを確かめる 82 | function check_symlink(link: { from: URL; to: URL }): boolean { 83 | try { 84 | // Check for the presence of links 85 | const lstat = Deno.lstatSync(link.to); 86 | if (lstat.isSymlink) { 87 | // Check where the link points 88 | if (Deno.readLinkSync(link.to) === fromFileUrl(link.from)) { 89 | return true; 90 | } else { 91 | return false; 92 | } 93 | } else { 94 | return false; 95 | } 96 | } catch (_) { 97 | return false; 98 | } 99 | } 100 | 101 | // if symlink does not exist, make symlink 102 | // syncコマンドから直接呼ばれる 103 | function ensure_make_symlinks(links: { from: URL; to: URL }[]): boolean { 104 | let err = links.map((link) => { 105 | const from = link.from.pathname; 106 | const to = link.to.pathname; 107 | ensureDirSync(dirname(to)); 108 | if (!check_symlink(link)) { 109 | try { 110 | ensureSymlinkSync(from, to); 111 | } catch (err) { 112 | if (err.message.indexOf("Ensure path exists, expected") === 0) { 113 | console.log( 114 | `・ ${red("✘ ")} ${from} → ${to}: ${red("File or Directory already exists.")}`, 115 | ); 116 | // エラー 117 | return false 118 | } else { 119 | throw err 120 | } 121 | } 122 | // 成功のメッセージはシンボリックリンクの作成に成功してから表示する 123 | console.log(`・ ${green("✔ ")} ${from} → ${to}`); 124 | } else { 125 | console.log(`・ ${green("✔ ")} ${gray(to)}`); 126 | } 127 | return true 128 | }); 129 | 130 | // すべての処理が正常に終了しているかチェック 131 | if (err.find(s => !s) !== undefined) { 132 | return false 133 | } 134 | return true 135 | } 136 | -------------------------------------------------------------------------------- /testing/utils.test.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals, join } from "../deps.ts"; 2 | import { expandTilde, resolvePath } from "../util/mod.ts"; 3 | 4 | const home = Deno.env.get("HOME") ?? ""; 5 | const pwd = Deno.env.get("PWD") ?? ""; 6 | 7 | // no tilde 8 | Deno.test("expand_tilde #1", () => { 9 | const expect = "test/hoge/~"; 10 | const actual = expandTilde("test/hoge/~"); 11 | return assertEquals(expect, actual); 12 | }); 13 | 14 | // ~ 15 | Deno.test("expand_tilde #2", () => { 16 | const expect = home; 17 | const actual = expandTilde("~"); 18 | return assertEquals(expect, actual); 19 | }); 20 | 21 | // ~/hoge/hugo 22 | Deno.test("expand_tilde #3", () => { 23 | const expect = join(home, "hoge/hugo"); 24 | const actual = expandTilde("~/hoge/hugo"); 25 | return assertEquals(expect, actual); 26 | }); 27 | 28 | const basedir = "/home/hoge"; 29 | 30 | // ~ $BASEDIR -> $HOME 31 | Deno.test("resolve_path #1", () => { 32 | const expect = home; 33 | const actual = resolvePath("~", basedir); 34 | return assertEquals(expect, actual); 35 | }); 36 | 37 | // ../ $BASEDIR -> $BASEDIR/../ 38 | Deno.test("resolve_path #1", () => { 39 | const expect = "/home"; 40 | const actual = resolvePath("../", basedir); 41 | return assertEquals(expect, actual); 42 | }); 43 | 44 | // ./ $BASEDIR -> $BASEDIR 45 | Deno.test("resolve_path #2", () => { 46 | const expect = basedir; 47 | const actual = resolvePath("./", basedir); 48 | return assertEquals(expect, actual); 49 | }); 50 | 51 | // a $BASEDIR -> $BASEDIR/a 52 | Deno.test("resolve_path #3", () => { 53 | const expect = join(basedir, "a"); 54 | const actual = resolvePath("a", basedir); 55 | return assertEquals(expect, actual); 56 | }); 57 | 58 | // ./hoge/hugo -> join($(pwd), "./hoge/hugo") 59 | Deno.test("resolve_path #4", () => { 60 | const expect = join(pwd, "./hoge/hugo"); 61 | const actual = resolvePath("./hoge/hugo"); 62 | return assertEquals(expect, actual); 63 | }); 64 | 65 | // /hoge/hugo -> "/hoge/hugo" 66 | Deno.test("resolve_path #5", () => { 67 | const expect = "/hoge/hugo"; 68 | const actual = resolvePath("/hoge/hugo"); 69 | return assertEquals(expect, actual); 70 | }); 71 | 72 | // ~/hoge -> "$HOME/hoge" 73 | Deno.test("resolve_path #6", () => { 74 | const expect = join(home, "hoge"); 75 | const actual = resolvePath("~/hoge"); 76 | return assertEquals(expect, actual); 77 | }); 78 | -------------------------------------------------------------------------------- /types.ts: -------------------------------------------------------------------------------- 1 | export interface Plugin { 2 | name: string; 3 | 4 | // souces must returns exit status 5 | // if the process failed, the function returns false 6 | stat?: () => boolean | Promise; 7 | list?: () => boolean | Promise; 8 | sync?: () => boolean | Promise; 9 | 10 | subcmds?: Subcmd[]; 11 | } 12 | 13 | export type Subcmd = { 14 | name: string; 15 | info: string; 16 | func: (options: SubcmdOptions) => boolean | Promise; 17 | }; 18 | 19 | export interface DfmOptions { 20 | subcmdOptions?: SubcmdOptions; 21 | } 22 | 23 | export interface SubcmdOptions { 24 | cmdName: string; 25 | args: SubcmdArgs; 26 | } 27 | 28 | export type SubcmdArgs = string[]; 29 | -------------------------------------------------------------------------------- /util/mod.ts: -------------------------------------------------------------------------------- 1 | export { expandTilde, isatty, os, resolvePath } from "./util.ts"; 2 | -------------------------------------------------------------------------------- /util/util.ts: -------------------------------------------------------------------------------- 1 | import { join, resolve } from "../deps.ts"; 2 | 3 | const homedir = Deno.env.get("HOME"); 4 | 5 | export function expandTilde(path: string) { 6 | if (homedir === undefined) { 7 | return path; 8 | } else { 9 | if (!path) return path; 10 | if (path === "~") return homedir; 11 | if (path.slice(0, 2) !== "~/") return path; 12 | return join(homedir, path.slice(2)); 13 | } 14 | } 15 | 16 | // BASEDIR is basedir of dotfiles 17 | // ~ $BASEDIR -> $HOME 18 | // ../ $BASEDIR -> $BASEDIR/../ 19 | // ./ $BASEDIR -> $BASEDIR 20 | // a $BASEDIR -> $BASEDIR/a 21 | // ./hoge/hugo -> join($(pwd), "./hoge/hugo") 22 | // /hoge/hugo -> "/hoge/hugo" 23 | // ~/hoge -> "$HOME/hugo" 24 | export function resolvePath(path: string, basedir?: string): string { 25 | if (basedir !== undefined) { 26 | return resolve(basedir, expandTilde(path)); 27 | } else { 28 | return resolve("", expandTilde(path)); 29 | } 30 | } 31 | 32 | export function isatty(): boolean { 33 | return Deno.isatty(Deno.stdout.rid); 34 | } 35 | 36 | export function os(): typeof Deno.build.os { 37 | return Deno.build.os; 38 | } 39 | --------------------------------------------------------------------------------