├── .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 | 
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 |
--------------------------------------------------------------------------------