@frakjs/frak 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,138 +1,210 @@
1
- # Frak
1
+ <a id="readme-top"></a>
2
+
3
+ <!-- PROJECT SHIELDS -->
4
+ [![Contributors][contributors-shield]][contributors-url]
5
+ [![Forks][forks-shield]][forks-url]
6
+ [![Stargazers][stars-shield]][stars-url]
7
+ [![Issues][issues-shield]][issues-url]
8
+ [![MIT][license-shield]][license-url]
9
+
10
+ <!-- PROJECT LOGO -->
11
+ <br />
12
+ <div align="center">
13
+ <a href="https://github.com/frakjs/frak">
14
+ <img src="images/logos/Frak Logo - Green - 256.png" width="112" />
15
+ </a>
16
+
17
+ <h3 align="center">frak</h3>
18
+
19
+ <p align="center">
20
+ Deploy from point A → point B, without the drama.
21
+ <br />
22
+ <br />
23
+ <a href="https://github.com/frakjs/frak/issues/new?labels=bug&template=bug-report---.md">Report Bug</a>
24
+ &middot;
25
+ <a href="https://github.com/frakjs/frak/issues/new?labels=enhancement&template=feature-request---.md">Request Feature</a>
26
+ </p>
27
+ </div>
28
+
29
+ <!-- TABLE OF CONTENTS -->
30
+ <details>
31
+ <summary>Table of Contents</summary>
32
+ <ol>
33
+ <li>
34
+ <a href="#about-the-project">About The Project</a>
35
+ </li>
36
+ <li>
37
+ <a href="#getting-started">Getting Started</a>
38
+ <ul>
39
+ <li><a href="#prerequisites">Prerequisites</a></li>
40
+ </ul>
41
+ </li>
42
+ <li><a href="#usage">Usage</a></li>
43
+ <li><a href="#roadmap">Roadmap</a></li>
44
+ <li><a href="#contributing">Contributing</a></li>
45
+ <li><a href="#license">License</a></li>
46
+ <li><a href="#contact">Contact</a></li>
47
+ </ol>
48
+ </details>
49
+
50
+
51
+
52
+ <!-- ABOUT THE PROJECT -->
53
+ ## About The Project
54
+
55
+ [![Product Name Screen Shot][product-screenshot]](https://frakjs.com)
56
+
57
+ **`frak`** is a tiny JavaScript binary for deployments that don’t need containers or repos—just fast, reliable file transfer from point A to point B.
58
+
59
+ It leverages `rsync` over SSH to push files to a remote server, and is able to visualize file changes in a developer-friendly way. **`frak`** has a number of advanced features beyond syncing files to a remote server, such as keeping a history of deployments for easy rollback, showing file differences prior to pushing, and executing post-deploy hooks.
60
+
61
+ ### 🚀 Features
62
+
63
+ 🔌 **Init once, deploy anytime**<br>
64
+ 🔎 **Interactive diffs** before every push/pull<br>
65
+ 📦 **Patch-based backups** stored on your server<br>
66
+ 🧾 **Post-deploy commands** (e.g. restart services)<br>
67
+ 📡 **Quick SSH console access** into deploy root<br>
68
+ 🔁 **Pull support** for syncing back changes<br>
69
+ 📋 **Backup inspection** with additions/deletions summary
70
+
71
+ <p align="right">(<a href="#readme-top">back to top</a>)</p>
72
+
73
+ <!-- GETTING STARTED -->
74
+ ## Getting Started
75
+
76
+ Using **`frak`** is simple. All you need is a [Node.js](https://nodejs.org) environment with `npx`.
77
+
78
+ ### Prerequisites
79
+
80
+ Make sure you have Node.js and `npx` installed.
2
81
 
3
- **Frak** is an agentless deployment tool for web apps. Powered by `rsync` over SSH, it tracks file changes, creates patch backups, and supports dry-run diffs, interactive confirmations, and post-deploy hooks.
4
-
5
- > “Frak it, ship it.”
82
+ ```bash
83
+ node --version
84
+ npm --version
85
+ npx --version
86
+ ```
6
87
 
7
- &nbsp;
88
+ [![Node.js and NPM Screenshot][prerequisites-screenshot]]()
8
89
 
9
- ## 🚀 Features
90
+ <p align="right">(<a href="#readme-top">back to top</a>)</p>
10
91
 
11
- - 🔌 **Init once, deploy anytime**
12
- - 🔎 **Interactive diffs** before every push/pull
13
- - 📦 **Patch-based backups** stored on your server
14
- - 🧾 **Post-deploy commands** (e.g. restart services)
15
- - 📡 **SSH console access** into deploy root
16
- - 🔁 **Pull support** for syncing back changes
17
- - 📋 **Backup inspection** with additions/deletions summary
92
+ <!-- USAGE EXAMPLES -->
93
+ ## Usage
18
94
 
19
- ## 📦 Installation
95
+ The recommended way to run **`frak`** is via the `npx` command.
20
96
 
21
97
  ```bash
22
- npm install -g @frakjs/frak
98
+ npx @frakjs/frak --help
23
99
  ```
24
100
 
25
- Or run it on-demand with `npx`
101
+ If you're prompted to install the package, say _yes_.
26
102
 
27
- ```bash
28
- npx @frakjs/frak <command>
103
+ ```
104
+ Need to install the following packages:
105
+ @frakjs/frak@0.1.0
106
+ Ok to proceed? (y)
29
107
  ```
30
108
 
31
- ## 🛠️ Setup
109
+ **`frak`** needs to be initialized in your project directory with the `init` command.
32
110
 
33
111
  ```bash
34
- frak init
112
+ cd project-folder
113
+ npx @frakjs/frak init
35
114
  ```
36
115
 
37
- This creates a `frak.config.js` file:
116
+ [![frak init][frak-init-screenshot]]()
117
+
118
+ This creates a `frak.config.js` file that looks something like this:
38
119
 
39
120
  ```js
40
121
  export default {
41
122
  server: "user@example.com",
42
123
  root: "/var/www/html",
43
- after: "npm run restart" // optional post-deploy hook
44
124
  };
45
125
  ```
46
126
 
47
- ## 🧩 Commands
127
+ You can replace `server` and `root` with the appropriate values for your environment.
48
128
 
49
- ```
50
- frak init # Create config file
51
- frak console # SSH into the deploy root
52
- frak diff # Show dry-run changes
53
- frak push # Deploy changes (with confirmation)
54
- frak pull # Download changes from remote
55
- frak backups:list # List previous patch deployments
56
- ```
129
+ With the configuration in place, you can now start <b>`frak`</b>ing files to your server. It's as simple as the basic command without any arguments.
57
130
 
58
- ## 🔐 Authentication
131
+ ```bash
132
+ npx @frakjs/frak
133
+ ```
59
134
 
60
- Frak uses SSH. Make sure your public key is in `~/.ssh/authorized_keys` on the remote server.
135
+ The results of the command will show you a dry-run preview of the changes that will be synced to the remote server. You will be prompted before it proceeds with the actual file transfer.
61
136
 
62
- ## 🧪 Example: Deployment Flow
137
+ If you'd like, you can cancel the sync by typing "no" or any non-"yes" value. Perhaps you want to see the changes line-by-line. **`frak`** can do this too!
63
138
 
64
139
  ```bash
65
- frak diff # See what will change
66
- frak push # Sync files, confirm, deploy
140
+ npx @frakjs/frak diff
67
141
  ```
68
142
 
69
- Behind the scenes:
143
+ The command above will compare all the changes, and present a colorful git-style patch that you can inspect in your terminal.
70
144
 
71
- - Files are compared with a dry-run rsync
72
- - If approved, a patch file is generated and saved
73
- - Files are synced live
74
- - Optional after script is run remotely
145
+ <p align="right">(<a href="#readme-top">back to top</a>)</p>
75
146
 
76
- ## 💾 Backups
147
+ <!-- ROADMAP -->
148
+ ## Roadmap
77
149
 
78
- Each deployment saves a `.patch` file on the server under `.backups/`. You can list them with:
150
+ - [ ] Send an alert via Slack after push events
151
+ - [ ] Parallel deployments to multiple environments at once
79
152
 
80
- ```bash
81
- frak backups:list
82
- ```
153
+ See the [open issues](https://github.com/frakjs/frak/issues) for a full list of proposed features (and known issues).
83
154
 
84
- Output shows:
155
+ <p align="right">(<a href="#readme-top">back to top</a>)</p>
85
156
 
86
- - Timestamp
87
- - Filename
88
- - Additions and deletions in the patch
157
+ <!-- CONTRIBUTING -->
158
+ ## Contributing
89
159
 
90
- ## 🧠 Design Philosophy
160
+ Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are **greatly appreciated**.
91
161
 
92
- - Minimal dependencies
93
- - Pure CLI
94
- - Patch-based history
95
- - No Docker or Node on server
96
- - Human-friendly
162
+ If you have a suggestion that would make this better, please fork the repo and create a pull request. You can also simply open an issue with the tag "enhancement".
163
+ Don't forget to give the project a star! Thanks again!
97
164
 
98
- ## 🧰 Advanced Config (optional)
165
+ 1. Fork the Project
166
+ 2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`)
167
+ 3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`)
168
+ 4. Push to the Branch (`git push origin feature/AmazingFeature`)
169
+ 5. Open a Pull Request
99
170
 
100
- ```js
101
- export default {
102
- server: 'user@host',
103
- root: '/srv/www',
104
- after: 'sudo systemctl reload nginx',
171
+ <p align="right">(<a href="#readme-top">back to top</a>)</p>
105
172
 
106
- dev: {
107
- root: '/srv/www-dev',
108
- },
109
-
110
- production: {
111
- root: '/srv/www-prod',
112
- },
113
- };
114
- ```
173
+ ### Top contributors:
115
174
 
116
- ```bash
117
- frak push # uses default root-level properties
118
- frak push env=dev # uses properties in "dev" to override
119
- frak push env=production # uses properties in "production" to override
120
- ```
175
+ <a href="https://github.com/frakjs/frak/graphs/contributors">
176
+ <img src="https://contrib.rocks/image?repo=frakjs/frak" alt="contrib.rocks image" />
177
+ </a>
121
178
 
122
- ## 🐛 Debugging
179
+ <!-- LICENSE -->
180
+ ## License
123
181
 
124
- Enable debug output by setting `DEBUG=1`:
182
+ Distributed under the MIT License. See `LICENSE.txt` for more information.
125
183
 
126
- ```bash
127
- DEBUG=1 frak push
128
- ```
184
+ <p align="right">(<a href="#readme-top">back to top</a>)</p>
129
185
 
130
- ## 📄 License
186
+ <!-- CONTACT -->
187
+ ## Contact
131
188
 
132
- MIT
189
+ Frank Strube - [@strube](https://x.com/strube) - contact@frakjs.com
133
190
 
134
- ## 🧑‍💻 Author
191
+ Project Link: [https://github.com/frakjs/frak](https://github.com/frakjs/frak)
135
192
 
136
- Built by [Frank Strube](https://github.com/fstrube)
193
+ <p align="right">(<a href="#readme-top">back to top</a>)</p>
137
194
 
138
- Contributions and suggestions welcome!
195
+ <!-- MARKDOWN LINKS & IMAGES -->
196
+ <!-- https://www.markdownguide.org/basic-syntax/#reference-style-links -->
197
+ [contributors-shield]: https://img.shields.io/github/contributors/frakjs/frak.svg?style=for-the-badge
198
+ [contributors-url]: https://github.com/frakjs/frak/graphs/contributors
199
+ [forks-shield]: https://img.shields.io/github/forks/frakjs/frak.svg?style=for-the-badge
200
+ [forks-url]: https://github.com/frakjs/frak/network/members
201
+ [stars-shield]: https://img.shields.io/github/stars/frakjs/frak.svg?style=for-the-badge
202
+ [stars-url]: https://github.com/frakjs/frak/stargazers
203
+ [issues-shield]: https://img.shields.io/github/issues/frakjs/frak.svg?style=for-the-badge
204
+ [issues-url]: https://github.com/frakjs/frak/issues
205
+ [license-shield]: https://img.shields.io/github/license/frakjs/frak.svg?style=for-the-badge
206
+ [license-url]: https://github.com/frakjs/frak/blob/master/LICENSE.txt
207
+ [green-logo]: images/logos/Frak%20Logo%20-%20Green%20-%20256.png
208
+ [product-screenshot]: images/screenshots/screenshot.png
209
+ [prerequisites-screenshot]: images/screenshots/node-npm-version.png
210
+ [frak-init-screenshot]: images/screenshots/frak-init-screenshot.png
package/bin/frak.js CHANGED
@@ -1,8 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { execSync, spawn } from 'node:child_process';
4
- import { readFileSync, writeFileSync, existsSync, statSync } from 'node:fs';
5
- import { fileURLToPath } from 'node:url';
4
+ import { writeFileSync, existsSync } from 'node:fs';
6
5
  import path from 'node:path';
7
6
  import * as ansi from '../src/ansi.js';
8
7
  import * as cli from '../src/cli.js';
@@ -15,8 +14,6 @@ import * as rsync from '../src/rsync.js';
15
14
  import * as ssh from '../src/ssh.js';
16
15
  import * as date from '../src/date.js';
17
16
 
18
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
19
-
20
17
  /**
21
18
  * Try to load config. If it fails, print error message in red.
22
19
  *
@@ -26,7 +23,7 @@ async function tryLoadConfig(env) {
26
23
  try {
27
24
  await loadConfig(env);
28
25
  } catch (e) {
29
- console.log(ansi.red(e.message));
26
+ console.error(ansi.red(e.message));
30
27
 
31
28
  process.exit(1);
32
29
  }
@@ -58,7 +55,6 @@ async function main(args) {
58
55
 
59
56
  debug('parsed:', { command, options });
60
57
 
61
-
62
58
  // Initialize Frak in the current directory
63
59
  if (command === 'init') {
64
60
  const configPath = path.join(process.cwd(), 'frak.config.js');
@@ -91,9 +87,17 @@ async function main(args) {
91
87
  } else if (command === 'diff') {
92
88
  await tryLoadConfig(options.env);
93
89
 
94
- const { output } = await rsync.exec('--dry-run');
90
+ try {
91
+ const { output } = await rsync.exec('--dry-run');
95
92
 
96
- await diff(output, options);
93
+ debug({ output });
94
+
95
+ await diff(output, options);
96
+ } catch (e) {
97
+ debug(e);
98
+
99
+ console.error(e.message);
100
+ }
97
101
 
98
102
  // Push or pull changes to remote server
99
103
  } else if (command === 'push' || command === 'pull') {
@@ -102,10 +106,13 @@ async function main(args) {
102
106
  // Execute dry-run
103
107
  const { output } = await rsync.exec('--dry-run');
104
108
 
109
+ debug({ output });
110
+
105
111
  console.log(humanize(output));
106
112
 
107
113
  if (command === 'push' && config.after) {
108
- console.log(ansi.blue('Run command:'), config.after, '\n');
114
+ console.log(ansi.blue('Run command:'), config.after);
115
+ console.log('');
109
116
  }
110
117
 
111
118
  const proceed = await cli.agree(ansi.yellow('The above actions will be taken. Continue? (This cannot be undone): '));
@@ -115,14 +122,18 @@ async function main(args) {
115
122
  }
116
123
 
117
124
  // Prepare patch file
118
- let patch;
125
+ let patch = '';
119
126
 
120
- try {
121
- patch = await diff(output, { interactive: false });
122
- } catch (e) {
123
- console.error(e);
127
+ if (command === 'push') {
128
+ try {
129
+ patch = await diff(output, { interactive: false });
130
+ /* node:coverage disable */
131
+ } catch (e) {
132
+ console.error(e);
124
133
 
125
- process.exit(1);
134
+ process.exit(1);
135
+ }
136
+ /* node:coverage enable */
126
137
  }
127
138
 
128
139
  // Execute actual rsync command
@@ -132,12 +143,14 @@ async function main(args) {
132
143
  console.log(humanize(actualOutput));
133
144
 
134
145
  // Place patch file in backup dir
135
- const hash = sha256(patch).slice(0, 7);
136
- const copy = spawn('ssh', [config.server, '--', `cat > '${config.root || '.'}/${backupPath}/${hash}.patch'`]);
146
+ if (command === 'push') {
147
+ const hash = sha256(patch).slice(0, 7);
148
+ const copy = spawn('ssh', [config.server, '--', `cat > '${config.root || '.'}/${backupPath}/${hash}.patch'`]);
137
149
 
138
- copy.stdout.on('data', (data) => debug('' + data));
139
- copy.stderr.on('data', (data) => debug('' + data));
140
- copy.stdin.write(patch);
150
+ copy.stdout.on('data', (data) => debug('' + data));
151
+ copy.stderr.on('data', (data) => debug('' + data));
152
+ copy.stdin.write(patch);
153
+ }
141
154
 
142
155
  // Execute "after" command
143
156
  if (command === 'push' && config.after) {
@@ -147,9 +160,11 @@ async function main(args) {
147
160
 
148
161
  try {
149
162
  await ssh.exec(config.after, { server: config.server, root: config.root });
163
+ /* node:coverage disable */
150
164
  } catch (e) {
151
165
  debug(e);
152
166
  }
167
+ /* node:coverage enable */
153
168
  }
154
169
 
155
170
  // TODO Webhook
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@frakjs/frak",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "A commandline deployment tool for syncing files to remote servers.",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -8,7 +8,8 @@
8
8
  },
9
9
  "type": "module",
10
10
  "scripts": {
11
- "test": "echo \"Error: no test specified\" && exit 1"
11
+ "eslint": "eslint",
12
+ "test": "node --test --experimental-test-coverage"
12
13
  },
13
14
  "repository": {
14
15
  "type": "git",
@@ -24,5 +25,13 @@
24
25
  "bugs": {
25
26
  "url": "https://github.com/frakjs/frak/issues"
26
27
  },
27
- "homepage": "https://github.com/frakjs/frak#readme"
28
+ "homepage": "https://github.com/frakjs/frak#readme",
29
+ "devDependencies": {
30
+ "@eslint/js": "^9.33.0",
31
+ "eslint": "^9.33.0",
32
+ "globals": "^16.3.0"
33
+ },
34
+ "dependencies": {
35
+ "yaml": "^2.8.1"
36
+ }
28
37
  }
package/src/ansi.js CHANGED
@@ -15,7 +15,7 @@ const ansi = {
15
15
  };
16
16
 
17
17
  export function strip(string) {
18
- return string
18
+ return `${string}`
19
19
  .replace(new RegExp(ansi.CLEAR.replace('[', '\\['), 'g'), '')
20
20
  .replace(new RegExp(ansi.BOLD.replace('[', '\\['), 'g'), '')
21
21
  .replace(new RegExp(ansi.UNDERLINE.replace('[', '\\['), 'g'), '')
package/src/cli.js CHANGED
@@ -13,7 +13,11 @@ export function parse(args) {
13
13
  };
14
14
 
15
15
  for (let arg of args) {
16
- if (arg.includes('=')) {
16
+ if (arg === '--yes' || arg === '-y') {
17
+ options.interactive = false;
18
+ } else if (arg === '--no-color') {
19
+ options.colorize = false;
20
+ } else if (arg.includes('=')) {
17
21
  const [option, value] = arg.split('=', 2);
18
22
 
19
23
  options[option] = value;
@@ -25,11 +29,14 @@ export function parse(args) {
25
29
  return { command, options };
26
30
  }
27
31
 
28
- export async function agree(string) {
29
- process.stdout.write(string);
32
+ export async function agree(string, { stdin = null, stdout = null } = {}) {
33
+ stdout = stdout ?? process.stdout;
34
+ stdin = stdin ?? process.stdin;
30
35
 
31
- const answer = await new Promise((resolve, reject) => {
32
- process.stdin.on('data', data => resolve(data));
36
+ stdout.write(string);
37
+
38
+ const answer = await new Promise((resolve) => {
39
+ stdin.on('data', data => resolve(data));
33
40
  });
34
41
 
35
42
  return ['y', 'yes'].includes(`${answer}`.trim().toLowerCase());
@@ -67,7 +74,6 @@ export class Table {
67
74
  const width = ansi.strip(cell).length
68
75
  const padding = Math.max(0, this.columns[i].width - width - 2);
69
76
  const paddingString = (new Array(padding + 1)).join(' ');
70
- const borderString = (new Array(this.columns[i].width + 1).join('-'));
71
77
 
72
78
  return `${row || ''}${ansi.gray('|')} ${cell}${paddingString} ${i === r.length - 1 ? ansi.gray('|') : ''}`
73
79
  }, '');
package/src/config.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
+ import { parse } from 'yaml';
3
4
 
4
5
  // Cached config object. The config is stored in here so it can
5
6
  // be loaded once and imported across all modules.
@@ -13,10 +14,15 @@ let config = {};
13
14
  export function locate() {
14
15
  for (let dir = process.cwd(); dir != '/'; dir = path.dirname(dir)) {
15
16
  const configPath = path.join(dir, 'frak.config.js');
17
+ const yamlPath = path.join(dir, '.frak');
16
18
 
17
19
  if (fs.existsSync(configPath)) {
18
20
  return configPath;
19
21
  }
22
+
23
+ if (fs.existsSync(yamlPath)) {
24
+ return yamlPath;
25
+ }
20
26
  }
21
27
 
22
28
  return null;
@@ -30,21 +36,51 @@ export function locate() {
30
36
  export async function load(env = null) {
31
37
  const configPath = locate();
32
38
 
33
- if (fs.existsSync(configPath)) {
39
+ if (!fs.existsSync(configPath)) {
40
+ throw new Error('Config file not found! Try running frak init.');
41
+ }
42
+
43
+ if (path.basename(configPath) === 'frak.config.js') {
34
44
  Object.assign(config, (await import(configPath)).default);
45
+ } else if (path.basename(configPath) === '.frak') {
46
+ Object.assign(config, parse(fs.readFileSync(configPath).toString('utf-8')));
47
+ }
35
48
 
36
- if (env in config) {
37
- return Object.assign(config, {...config[env]});
38
- }
49
+ if (env in config) {
50
+ Object.assign(config, {...config[env]});
39
51
 
40
- if (env) {
41
- throw new Error(`Missing configuration for environment "${env}"!`);
42
- }
52
+ delete config[env];
43
53
 
44
54
  return config;
45
55
  }
46
56
 
47
- throw new Error('Config file not found! Try running frak init.');
57
+ if (env) {
58
+ throw new Error(`Missing configuration for environment "${env}"!`);
59
+ }
60
+
61
+ // Support legacy config, remote_path instead of root
62
+ if ('remote_path' in config) {
63
+ config.root = config.remote_path;
64
+
65
+ delete config.remote_path;
66
+ }
67
+
68
+ return config;
69
+ }
70
+
71
+ /**
72
+ * Reset the config, useful when unit testing.
73
+ *
74
+ * @returns {Object}
75
+ */
76
+ export function reset() {
77
+ const old = { ...config };
78
+
79
+ Object.entries(config).forEach(([key]) => {
80
+ delete config[key];
81
+ });
82
+
83
+ return old;
48
84
  }
49
85
 
50
86
  export default config;
package/src/date.js CHANGED
@@ -54,7 +54,7 @@ export function relative(date) {
54
54
  value = Math.floor(seconds / 60 / 60 / 24 / 30);
55
55
  unit = plural('month', value);
56
56
  } else {
57
- return `${months[datetime.getMonth()]} ${datetime.getDay()}, ${datetime.getFullYear()} @ ${datetime.getHours() % 12}:${`0${datetime.getMinutes()}`.slice(-2)} ${datetime.getHours() >= 12 ? 'pm' : 'am'}`;
57
+ return `${months[datetime.getMonth()]} ${datetime.getDate()}, ${datetime.getFullYear()} @ ${datetime.getHours() % 12}:${`0${datetime.getMinutes()}`.slice(-2)} ${datetime.getHours() >= 12 ? 'pm' : 'am'}`;
58
58
  }
59
59
 
60
60
  return `${value} ${unit} ${agoOrFromNow}`.trim();
package/src/debug.js CHANGED
@@ -5,6 +5,6 @@ import * as ansi from '../src/ansi.js';
5
5
  */
6
6
  export default function debug() {
7
7
  if (process.env.DEBUG) {
8
- console.log.apply(console, [ansi.bold(ansi.cyan('DEBUG:')), ...arguments]);
8
+ console.log.apply(console, [ ansi.bold(ansi.cyan('DEBUG:')), ...arguments ]);
9
9
  }
10
10
  }
package/src/diff.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { spawn } from 'node:child_process';
2
2
  import { statSync } from 'node:fs';
3
3
  import * as ansi from './ansi.js';
4
+ import * as cli from './cli.js';
4
5
  import config from './config.js';
5
6
  import debug from './debug.js';
6
7
 
@@ -21,8 +22,8 @@ export default async function diff(dryRunOutput, options) {
21
22
 
22
23
  let file, target;
23
24
 
24
- // Links also have a target
25
- if (action === 'cL+++++++++' || action === 'cLc.T......') {
25
+ // Links also have a target, cL+++++++++ or cLc.T......
26
+ if (action.match(/^cL.+/)) {
26
27
  const index = parts.indexOf('->');
27
28
 
28
29
  debug({ parts })
@@ -35,64 +36,68 @@ export default async function diff(dryRunOutput, options) {
35
36
 
36
37
  return [action, file, target];
37
38
  })
38
- .filter(([action, file]) => file.slice(-1) !== '/');
39
+ .filter(([, file]) => file.slice(-1) !== '/');
39
40
 
40
41
  debug({ changes })
41
42
 
42
43
  let proceed = true;
43
44
 
44
45
  if (changes.length >= 50 && options.interactive) {
45
- proceed = await cli.agree(ansi.yellow(`${changes.length} file(s) will be compared. Continue?`))
46
+ proceed = await cli.agree(ansi.yellow(`${changes.length} file(s) will be compared. Continue? `))
46
47
  }
47
48
 
48
49
  if (!proceed) {
49
50
  return Promise.reject('Process canceled by user');
50
51
  }
51
52
 
52
- const files = changes.map(([action, file]) => file);
53
+ const files = changes.map(([, file]) => file);
53
54
 
54
55
  const diffs = changes.sort((a, b) => a[1].localeCompare(b[1])).map(change => {
55
56
  const [action, file] = change;
57
+
58
+ // Escape double quotes in file
59
+ const fileEscaped = file.replace(/"/g, '\\"');
60
+
56
61
  const header = [
57
- `diff --git a/${file} b/${file}`,
62
+ file.match(/"/) ? `diff --git "a/${fileEscaped}" "b/${fileEscaped}"` : `diff --git a/${file} b/${file}`,
58
63
  ];
59
64
 
60
- let diff = [];
65
+ let diff = '';
61
66
 
62
67
  if (action === '*deleting') {
63
68
  header.push('deleted file mode 100644');
64
69
  header.push('index 0000000..0000000');
65
70
 
66
- diff = ['diff', '-Naurp', `--label=a/${file}`, `--label=/dev/null`, file, '/dev/null'];
67
- } else if (action === '<f+++++++++') {
71
+ diff = `diff -Naurp --label="a/${fileEscaped}" --label=/dev/null "${fileEscaped}" /dev/null`;
72
+ } else if (action.match(/^<f\++/)) {
68
73
  const { mode } = statSync(file);
69
74
 
70
75
  header.push(`new file mode ${mode.toString(8)}`);
71
76
  header.push('index 0000000..0000000');
72
77
 
73
- diff = ['diff', '-Naurp', `--label=/dev/null`, `--label=b/${file}`, '/dev/null', `<(tar xzOf $tarball --warning=no-unknown-keyword ${file} || cat /dev/null)`];
74
- } else if (action === 'cLc.T......') {
78
+ diff = `diff -Naurp --label=/dev/null --label="b/${fileEscaped}" /dev/null <(tar xzOf $tarball "${fileEscaped}" || cat /dev/null)`;
79
+ } else if (action.match(/^cL\++/)) {
75
80
  const target = change[2];
76
81
 
82
+ header.push('new file mode 120000');
77
83
  header.push('index 0000000..0000000');
78
84
 
79
- diff = ['diff', '-Naurp', `--label=a/${file}`, `--label=b/${file}`, `<(printf %s $(readlink ${file}))`, `<(printf %s ${target})`];
80
- } else if (action === 'cL+++++++++') {
85
+ diff = `diff -Naurp --label=/dev/null --label="b/${fileEscaped}" /dev/null <(printf %s "${target}")`;
86
+ } else if (action.match(/^cL.+/)) {
81
87
  const target = change[2];
82
88
 
83
- header.push('new file mode 120000');
84
89
  header.push('index 0000000..0000000');
85
90
 
86
- diff = ['diff', '-Naurp', `--label=/dev/null`, `--label=b/${file}`, '/dev/null', `<(printf %s ${target})`];
91
+ diff = `diff -Naurp --label="a/${fileEscaped}" --label="b/${fileEscaped}" <(printf %s "$(readlink "${fileEscaped}")") <(printf %s "${target}")`;
87
92
  } else {
88
93
  const { mode } = statSync(file);
89
94
 
90
95
  header.push(`index 0000000..0000000 ${mode.toString(8)}`);
91
96
 
92
- diff = ['diff', '-Naurp', `--label=a/${file}`, `--label=b/${file}`, file, `<(tar xzOf $tarball --warning=no-unknown-keyword ${file} || cat /dev/null)`];
97
+ diff = `diff -Naurp --label="a/${fileEscaped}" --label="b/${fileEscaped}" "${fileEscaped}" <(tar xzOf $tarball "${fileEscaped}" || cat /dev/null)`;
93
98
  }
94
99
 
95
- return [`printf "%s\\n" "${header.join('\n')}"`, ';', ...diff, ';'];
100
+ return `printf "%s\\n" '${header.join('\n')}'; ${diff}`;
96
101
  });
97
102
 
98
103
  if (diffs.length > 0) {
@@ -103,19 +108,18 @@ export default async function diff(dryRunOutput, options) {
103
108
  const ssh = spawn('ssh', [
104
109
  config.server,
105
110
  '--',
106
- 'tarball=$(mktemp /tmp/frak-diff-tarball.XXXXXXXX);',
111
+ 'tarball=$(mktemp /tmp/frak-diff-tarball.XXXXXX);',
107
112
  'cat > $tarball;',
108
113
  `cd ${config.root};`,
109
- ...diffs.reduce((commands, diff) => commands.concat(...diff), []),
110
- // `${diffs.join(';')};`,
111
- // 'rm $tarball',
114
+ diffs.join('; '), ';',
115
+ 'rm $tarball',
112
116
  ])
113
117
  tar.stdout.pipe(ssh.stdin);
114
118
 
115
119
  let less;
116
120
 
117
121
  if (options.interactive) {
118
- less = spawn('less', ['-r'], { stdio: ['pipe', 'inherit', 'inherit'] });
122
+ less = spawn('less', ['-R'], { stdio: ['pipe', process.stdout, process.stderr] });
119
123
  }
120
124
 
121
125
  if (options.interactive && options.colorize) {
@@ -149,25 +153,31 @@ export default async function diff(dryRunOutput, options) {
149
153
  less.on('exit', (code) => {
150
154
  if (code === 0) {
151
155
  resolve();
156
+ /* node:coverage disable */
152
157
  } else {
153
158
  reject(`exited with code ${code}`);
154
159
  }
160
+ /* node:coverage enable */
155
161
  });
156
162
  });
163
+ /* node:coverage disable */
157
164
  } else {
158
- return await new Promise((resolve, reject) => {
165
+ return await new Promise((resolve) => {
159
166
  let diff = '';
160
167
 
161
168
  ssh.stdout.on('data', (data) => diff += data);
162
169
 
163
170
  ssh.stderr.pipe(process.stderr);
164
171
 
165
- ssh.on('exit', (code) => {
172
+ ssh.on('exit', () => {
166
173
  resolve(diff);
167
174
  });
168
175
  });
169
176
  }
177
+ /* node:coverage enable */
170
178
  } else if (options.interactive) {
171
179
  console.log(ansi.yellow('No changes detected.'));
172
180
  }
181
+
182
+ return '';
173
183
  }
package/src/humanize.js CHANGED
@@ -9,5 +9,5 @@ export default function humanize(changes) {
9
9
  .replace(/<fc..\.\.\.\.\.\./g, ansi.yellow('Update '))
10
10
  .replace(/>fc..\.\.\.\.\.\./g, ansi.yellow('Update '))
11
11
  .replace(/cLc\.T\.\.\.\.\.\./g, ansi.yellow('Update '))
12
- .replace(/\*deleting /g, ansi.red('Delete '))
12
+ .replace(/\*deleting {2}/g, ansi.red('Delete '))
13
13
  }
package/src/rsync.js CHANGED
@@ -19,6 +19,7 @@ export function filters() {
19
19
  '.git*',
20
20
  '.svn*',
21
21
  '.tags*',
22
+ 'frak.config.js',
22
23
  'Capfile',
23
24
  'Gemfile*',
24
25
  'Vagrantfile*',
@@ -31,7 +32,8 @@ export function filters() {
31
32
 
32
33
  // merge path
33
34
  if (options.path) {
34
- const paths = options.path.split(/ +/)
35
+ const paths = options.path.split(/((?<!\\) )+/)
36
+ .map(p => p.replace(/\\ /g, ' '))
35
37
  .map(p => path.resolve(p))
36
38
  .map(p => p) // TODO glob // paths.map! { |p| p[/[*?]/] ? Dir.glob(p) : p }
37
39
  .flat(Infinity)
@@ -136,9 +138,11 @@ export function exec(...options) {
136
138
  output += `${data}`;
137
139
  });
138
140
 
141
+ /* node:coverage disable */
139
142
  rsync.stderr.on('data', (data) => {
140
143
  process.stderr.write(`${data}`); // Print any errors from the child process
141
144
  });
145
+ /* node:coverage enable */
142
146
 
143
147
  return new Promise((resolve, reject) => {
144
148
  rsync.on('exit', (code) => {
@@ -147,9 +151,11 @@ export function exec(...options) {
147
151
  output,
148
152
  backupPath,
149
153
  });
154
+ /* node:coverage disable */
150
155
  } else {
151
156
  reject(`rsync exited with code ${code}`);
152
157
  }
158
+ /* node:coverage enable */
153
159
  });
154
160
  });
155
161
  }
package/src/ssh.js CHANGED
@@ -31,9 +31,11 @@ export function exec(command, options = {}) {
31
31
  ssh.on('exit', (code) => {
32
32
  if (code === 0) {
33
33
  resolve();
34
+ /* node:coverage disable */
34
35
  } else {
35
36
  reject(`exited with code ${code}`);
36
37
  }
38
+ /* node:coverage enable */
37
39
  });
38
40
  });
39
41
 
package/.frakignore DELETED
File without changes
package/eslint.config.js DELETED
@@ -1 +0,0 @@
1
- export default [{}]
package/frak.config.js DELETED
@@ -1,10 +0,0 @@
1
- export default {
2
- server: 'franklinstrube.com',
3
- root: '/home/ash/frak',
4
- after: 'touch deployed; pwd',
5
- method: 'rsync',
6
-
7
- production: {
8
- root: '/home/ash/frak-production',
9
- },
10
- };
File without changes