@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 +158 -86
- package/bin/frak.js +35 -20
- package/images/logos/Frak Logo - Dark - 256.png +0 -0
- package/images/logos/Frak Logo - Green - 256.png +0 -0
- package/images/logos/Frak Logo - Light - 256.png +0 -0
- package/images/screenshots/frak-init-screenshot.png +0 -0
- package/images/screenshots/node-npm-version.png +0 -0
- package/images/screenshots/screenshot.png +0 -0
- package/package.json +12 -3
- package/src/ansi.js +1 -1
- package/src/cli.js +12 -6
- package/src/config.js +44 -8
- package/src/date.js +1 -1
- package/src/debug.js +1 -1
- package/src/diff.js +34 -24
- package/src/humanize.js +1 -1
- package/src/rsync.js +7 -1
- package/src/ssh.js +2 -0
- package/.frakignore +0 -0
- package/eslint.config.js +0 -1
- package/frak.config.js +0 -10
- /package/{LICENSE → LICENSE.txt} +0 -0
package/README.md
CHANGED
|
@@ -1,138 +1,210 @@
|
|
|
1
|
-
|
|
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
|
+
·
|
|
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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
82
|
+
```bash
|
|
83
|
+
node --version
|
|
84
|
+
npm --version
|
|
85
|
+
npx --version
|
|
86
|
+
```
|
|
6
87
|
|
|
7
|
-
|
|
88
|
+
[![Node.js and NPM Screenshot][prerequisites-screenshot]]()
|
|
8
89
|
|
|
9
|
-
|
|
90
|
+
<p align="right">(<a href="#readme-top">back to top</a>)</p>
|
|
10
91
|
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
95
|
+
The recommended way to run **`frak`** is via the `npx` command.
|
|
20
96
|
|
|
21
97
|
```bash
|
|
22
|
-
|
|
98
|
+
npx @frakjs/frak --help
|
|
23
99
|
```
|
|
24
100
|
|
|
25
|
-
|
|
101
|
+
If you're prompted to install the package, say _yes_.
|
|
26
102
|
|
|
27
|
-
```
|
|
28
|
-
|
|
103
|
+
```
|
|
104
|
+
Need to install the following packages:
|
|
105
|
+
@frakjs/frak@0.1.0
|
|
106
|
+
Ok to proceed? (y)
|
|
29
107
|
```
|
|
30
108
|
|
|
31
|
-
|
|
109
|
+
**`frak`** needs to be initialized in your project directory with the `init` command.
|
|
32
110
|
|
|
33
111
|
```bash
|
|
34
|
-
|
|
112
|
+
cd project-folder
|
|
113
|
+
npx @frakjs/frak init
|
|
35
114
|
```
|
|
36
115
|
|
|
37
|
-
|
|
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
|
-
|
|
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
|
-
|
|
131
|
+
```bash
|
|
132
|
+
npx @frakjs/frak
|
|
133
|
+
```
|
|
59
134
|
|
|
60
|
-
|
|
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
|
-
|
|
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
|
|
66
|
-
frak push # Sync files, confirm, deploy
|
|
140
|
+
npx @frakjs/frak diff
|
|
67
141
|
```
|
|
68
142
|
|
|
69
|
-
|
|
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
|
-
|
|
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
|
-
|
|
147
|
+
<!-- ROADMAP -->
|
|
148
|
+
## Roadmap
|
|
77
149
|
|
|
78
|
-
|
|
150
|
+
- [ ] Send an alert via Slack after push events
|
|
151
|
+
- [ ] Parallel deployments to multiple environments at once
|
|
79
152
|
|
|
80
|
-
|
|
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
|
-
|
|
155
|
+
<p align="right">(<a href="#readme-top">back to top</a>)</p>
|
|
85
156
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
- Additions and deletions in the patch
|
|
157
|
+
<!-- CONTRIBUTING -->
|
|
158
|
+
## Contributing
|
|
89
159
|
|
|
90
|
-
|
|
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
|
-
|
|
93
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
107
|
-
root: '/srv/www-dev',
|
|
108
|
-
},
|
|
109
|
-
|
|
110
|
-
production: {
|
|
111
|
-
root: '/srv/www-prod',
|
|
112
|
-
},
|
|
113
|
-
};
|
|
114
|
-
```
|
|
173
|
+
### Top contributors:
|
|
115
174
|
|
|
116
|
-
|
|
117
|
-
frak
|
|
118
|
-
|
|
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
|
-
|
|
179
|
+
<!-- LICENSE -->
|
|
180
|
+
## License
|
|
123
181
|
|
|
124
|
-
|
|
182
|
+
Distributed under the MIT License. See `LICENSE.txt` for more information.
|
|
125
183
|
|
|
126
|
-
|
|
127
|
-
DEBUG=1 frak push
|
|
128
|
-
```
|
|
184
|
+
<p align="right">(<a href="#readme-top">back to top</a>)</p>
|
|
129
185
|
|
|
130
|
-
|
|
186
|
+
<!-- CONTACT -->
|
|
187
|
+
## Contact
|
|
131
188
|
|
|
132
|
-
|
|
189
|
+
Frank Strube - [@strube](https://x.com/strube) - contact@frakjs.com
|
|
133
190
|
|
|
134
|
-
|
|
191
|
+
Project Link: [https://github.com/frakjs/frak](https://github.com/frakjs/frak)
|
|
135
192
|
|
|
136
|
-
|
|
193
|
+
<p align="right">(<a href="#readme-top">back to top</a>)</p>
|
|
137
194
|
|
|
138
|
-
|
|
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 {
|
|
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.
|
|
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
|
-
|
|
90
|
+
try {
|
|
91
|
+
const { output } = await rsync.exec('--dry-run');
|
|
95
92
|
|
|
96
|
-
|
|
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
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
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
|
-
|
|
136
|
-
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frakjs/frak",
|
|
3
|
-
"version": "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
|
-
"
|
|
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
|
|
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
|
|
32
|
+
export async function agree(string, { stdin = null, stdout = null } = {}) {
|
|
33
|
+
stdout = stdout ?? process.stdout;
|
|
34
|
+
stdin = stdin ?? process.stdin;
|
|
30
35
|
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
}
|
|
49
|
+
if (env in config) {
|
|
50
|
+
Object.assign(config, {...config[env]});
|
|
39
51
|
|
|
40
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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(([
|
|
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(([
|
|
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 =
|
|
67
|
-
} else if (action
|
|
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 =
|
|
74
|
-
} else if (action
|
|
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 =
|
|
80
|
-
} else if (action
|
|
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 =
|
|
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 =
|
|
97
|
+
diff = `diff -Naurp --label="a/${fileEscaped}" --label="b/${fileEscaped}" "${fileEscaped}" <(tar xzOf $tarball "${fileEscaped}" || cat /dev/null)`;
|
|
93
98
|
}
|
|
94
99
|
|
|
95
|
-
return
|
|
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.
|
|
111
|
+
'tarball=$(mktemp /tmp/frak-diff-tarball.XXXXXX);',
|
|
107
112
|
'cat > $tarball;',
|
|
108
113
|
`cd ${config.root};`,
|
|
109
|
-
|
|
110
|
-
|
|
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', ['-
|
|
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
|
|
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', (
|
|
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
|
|
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
package/.frakignore
DELETED
|
File without changes
|
package/eslint.config.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export default [{}]
|
package/frak.config.js
DELETED
/package/{LICENSE → LICENSE.txt}
RENAMED
|
File without changes
|