@frakjs/frak 0.2.0 → 0.4.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/.prettierrc +8 -0
- package/bin/frak.js +77 -47
- package/images/logos/Frak Logo - Green - Circle.png +0 -0
- package/package.json +38 -35
- package/src/cli.js +10 -10
- package/src/config.js +4 -9
- package/src/date.js +6 -6
- package/src/debug.js +1 -1
- package/src/diff.js +77 -57
- package/src/rsync.js +40 -25
- package/src/ssh.js +3 -3
package/.prettierrc
ADDED
package/bin/frak.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { execSync, spawn } from 'node:child_process';
|
|
4
|
-
import { writeFileSync, existsSync } from 'node:fs';
|
|
4
|
+
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
|
|
5
5
|
import path from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
6
7
|
import * as ansi from '../src/ansi.js';
|
|
7
8
|
import * as cli from '../src/cli.js';
|
|
8
9
|
import config, { load as loadConfig } from '../src/config.js';
|
|
@@ -14,21 +15,20 @@ import * as rsync from '../src/rsync.js';
|
|
|
14
15
|
import * as ssh from '../src/ssh.js';
|
|
15
16
|
import * as date from '../src/date.js';
|
|
16
17
|
|
|
18
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
19
|
+
const __dirname = path.dirname(__filename);
|
|
20
|
+
|
|
17
21
|
/**
|
|
18
22
|
* Try to load config. If it fails, print error message in red.
|
|
19
23
|
*
|
|
20
24
|
* @param {String} env
|
|
21
25
|
*/
|
|
22
26
|
async function tryLoadConfig(env) {
|
|
23
|
-
|
|
24
|
-
await loadConfig(env);
|
|
25
|
-
} catch (e) {
|
|
27
|
+
return await loadConfig(env).catch((e) => {
|
|
26
28
|
console.error(ansi.red(e.message));
|
|
27
29
|
|
|
28
30
|
process.exit(1);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
return;
|
|
31
|
+
});
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
/**
|
|
@@ -43,6 +43,19 @@ function usage() {
|
|
|
43
43
|
console.log(' pull Download the remote file(s)');
|
|
44
44
|
console.log(' push Deploy website via rsync');
|
|
45
45
|
console.log(' backups:list List backups on the server');
|
|
46
|
+
console.log('');
|
|
47
|
+
console.log('Options:');
|
|
48
|
+
console.log(' --help Display the command usage');
|
|
49
|
+
console.log(' --version Show the current version');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Print the current version
|
|
54
|
+
*/
|
|
55
|
+
function version() {
|
|
56
|
+
const { version } = JSON.parse(readFileSync(path.join(__dirname, '../package.json')));
|
|
57
|
+
|
|
58
|
+
console.log('Version:', version);
|
|
46
59
|
}
|
|
47
60
|
|
|
48
61
|
/**
|
|
@@ -55,20 +68,22 @@ async function main(args) {
|
|
|
55
68
|
|
|
56
69
|
debug('parsed:', { command, options });
|
|
57
70
|
|
|
58
|
-
// Initialize Frak in the current directory
|
|
59
71
|
if (command === 'init') {
|
|
72
|
+
// Initialize Frak in the current directory
|
|
60
73
|
const configPath = path.join(process.cwd(), 'frak.config.js');
|
|
61
74
|
|
|
62
75
|
if (!existsSync(configPath)) {
|
|
63
|
-
writeFileSync(
|
|
76
|
+
writeFileSync(
|
|
77
|
+
configPath,
|
|
78
|
+
`export default ${JSON.stringify({ server: 'example.com', root: '/var/www/html' }, null, 4)}`
|
|
79
|
+
);
|
|
64
80
|
console.info(ansi.green('Configuration file created: frak.config.js'));
|
|
65
81
|
} else {
|
|
66
82
|
console.error(ansi.red('Frak is already initialized in this directory.'));
|
|
67
83
|
}
|
|
68
|
-
|
|
69
|
-
// Start a console session, or run the provided command on
|
|
70
|
-
// the remote server.
|
|
71
84
|
} else if (command === 'console') {
|
|
85
|
+
// Start a console session, or run the provided command on
|
|
86
|
+
// the remote server.
|
|
72
87
|
await tryLoadConfig(options.env);
|
|
73
88
|
|
|
74
89
|
if (!config.server) {
|
|
@@ -77,14 +92,16 @@ async function main(args) {
|
|
|
77
92
|
}
|
|
78
93
|
|
|
79
94
|
try {
|
|
80
|
-
const cmd = options.command
|
|
95
|
+
const cmd = options.command || '\\$SHELL -l -i';
|
|
81
96
|
|
|
82
|
-
execSync(`ssh -t ${config.server} -- cd ${config.root} \\&\\& ${cmd}`, {
|
|
97
|
+
execSync(`ssh -t ${config.server} -- cd ${config.root} \\&\\& ${cmd}`, {
|
|
98
|
+
stdio: 'inherit',
|
|
99
|
+
});
|
|
83
100
|
} catch (e) {
|
|
84
|
-
debug(e)
|
|
101
|
+
debug(e);
|
|
85
102
|
}
|
|
86
|
-
// Show the differences between local files and remote server
|
|
87
103
|
} else if (command === 'diff') {
|
|
104
|
+
// Show the differences between local files and remote server
|
|
88
105
|
await tryLoadConfig(options.env);
|
|
89
106
|
|
|
90
107
|
try {
|
|
@@ -98,9 +115,8 @@ async function main(args) {
|
|
|
98
115
|
|
|
99
116
|
console.error(e.message);
|
|
100
117
|
}
|
|
101
|
-
|
|
102
|
-
// Push or pull changes to remote server
|
|
103
118
|
} else if (command === 'push' || command === 'pull') {
|
|
119
|
+
// Push or pull changes to remote server
|
|
104
120
|
await tryLoadConfig(options.env);
|
|
105
121
|
|
|
106
122
|
// Execute dry-run
|
|
@@ -115,7 +131,9 @@ async function main(args) {
|
|
|
115
131
|
console.log('');
|
|
116
132
|
}
|
|
117
133
|
|
|
118
|
-
const proceed = await cli.agree(
|
|
134
|
+
const proceed = await cli.agree(
|
|
135
|
+
ansi.yellow('The above actions will be taken. Continue? (This cannot be undone): ')
|
|
136
|
+
);
|
|
119
137
|
|
|
120
138
|
if (!proceed) {
|
|
121
139
|
process.exit(1);
|
|
@@ -125,14 +143,13 @@ async function main(args) {
|
|
|
125
143
|
let patch = '';
|
|
126
144
|
|
|
127
145
|
if (command === 'push') {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
}
|
|
146
|
+
patch = await diff(output, { interactive: false })
|
|
147
|
+
/* node:coverage disable */
|
|
148
|
+
.catch((e) => {
|
|
149
|
+
console.error(e);
|
|
150
|
+
|
|
151
|
+
process.exit(1);
|
|
152
|
+
});
|
|
136
153
|
/* node:coverage enable */
|
|
137
154
|
}
|
|
138
155
|
|
|
@@ -145,7 +162,11 @@ async function main(args) {
|
|
|
145
162
|
// Place patch file in backup dir
|
|
146
163
|
if (command === 'push') {
|
|
147
164
|
const hash = sha256(patch).slice(0, 7);
|
|
148
|
-
const copy = spawn('ssh', [
|
|
165
|
+
const copy = spawn('ssh', [
|
|
166
|
+
config.server,
|
|
167
|
+
'--',
|
|
168
|
+
`cat > '${config.root || '.'}/${backupPath}/${hash}.patch'`,
|
|
169
|
+
]);
|
|
149
170
|
|
|
150
171
|
copy.stdout.on('data', (data) => debug('' + data));
|
|
151
172
|
copy.stderr.on('data', (data) => debug('' + data));
|
|
@@ -158,13 +179,12 @@ async function main(args) {
|
|
|
158
179
|
|
|
159
180
|
debug({ command: config.after });
|
|
160
181
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
/* node:coverage enable */
|
|
182
|
+
await ssh
|
|
183
|
+
.exec(config.after, {
|
|
184
|
+
server: config.server,
|
|
185
|
+
root: config.root,
|
|
186
|
+
})
|
|
187
|
+
.catch(debug);
|
|
168
188
|
}
|
|
169
189
|
|
|
170
190
|
// TODO Webhook
|
|
@@ -179,13 +199,16 @@ async function main(args) {
|
|
|
179
199
|
const grep = {
|
|
180
200
|
additions: `grep -e '^+' "$NAME"/*.patch | grep -v '^+++' | wc -l`,
|
|
181
201
|
deletions: `grep -e '^-' "$NAME"/*.patch | grep -v '^---' | wc -l`,
|
|
182
|
-
}
|
|
202
|
+
};
|
|
183
203
|
const ls = `stat --format='%Y %n' .backups/* | sort -r`;
|
|
184
204
|
|
|
185
|
-
const list = ssh.exec(
|
|
205
|
+
const list = ssh.exec(
|
|
206
|
+
`${ls} | xargs -I {} sh -c 'TIMESTAMP=$(echo {} | cut -d" " -f1); NAME=$(echo {} | cut -d" " -f2-); echo $TIMESTAMP${delimeter}$NAME${delimeter}$(${grep.additions})${delimeter}$(${grep.deletions})'`,
|
|
207
|
+
{ quiet: true, server: config.server, root: config.root }
|
|
208
|
+
);
|
|
186
209
|
let backups = '';
|
|
187
210
|
|
|
188
|
-
list.stdout.on('data', data => backups += data);
|
|
211
|
+
list.stdout.on('data', (data) => (backups += data));
|
|
189
212
|
|
|
190
213
|
await list;
|
|
191
214
|
|
|
@@ -201,19 +224,26 @@ async function main(args) {
|
|
|
201
224
|
{
|
|
202
225
|
header: 'Changes',
|
|
203
226
|
width: 30,
|
|
204
|
-
}
|
|
227
|
+
},
|
|
205
228
|
]);
|
|
206
229
|
|
|
207
|
-
backups
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
230
|
+
backups
|
|
231
|
+
.trim()
|
|
232
|
+
.split(/\n/)
|
|
233
|
+
.forEach((backup) => {
|
|
234
|
+
const [timestamp, name, additions, deletions] = backup.split(delimeter);
|
|
235
|
+
table.row([
|
|
236
|
+
name.replace(/^.backups\//, ''),
|
|
237
|
+
date.relative(timestamp * 1000),
|
|
238
|
+
ansi.green(`${additions} addition${additions == 1 ? '' : 's'}`) +
|
|
239
|
+
', ' +
|
|
240
|
+
ansi.red(`${deletions} deletion${deletions == 1 ? '' : 's'}`),
|
|
241
|
+
]);
|
|
242
|
+
});
|
|
215
243
|
|
|
216
244
|
table.print();
|
|
245
|
+
} else if (command === '--version' || command === 'version') {
|
|
246
|
+
version();
|
|
217
247
|
} else {
|
|
218
248
|
usage();
|
|
219
249
|
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,37 +1,40 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
"
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
"
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
"
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
2
|
+
"name": "@frakjs/frak",
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "A commandline deployment tool for syncing files to remote servers.",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": ">=12"
|
|
8
|
+
},
|
|
9
|
+
"bin": {
|
|
10
|
+
"frak": "bin/frak.js"
|
|
11
|
+
},
|
|
12
|
+
"type": "module",
|
|
13
|
+
"scripts": {
|
|
14
|
+
"eslint": "eslint",
|
|
15
|
+
"test": "node --experimental-test-coverage tests/index.test.js"
|
|
16
|
+
},
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/frakjs/frak.git"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"rsync",
|
|
23
|
+
"deployment",
|
|
24
|
+
"ssh"
|
|
25
|
+
],
|
|
26
|
+
"author": "Frank Strube <contact@frakjs.com>",
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"bugs": {
|
|
29
|
+
"url": "https://github.com/frakjs/frak/issues"
|
|
30
|
+
},
|
|
31
|
+
"homepage": "https://github.com/frakjs/frak#readme",
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@eslint/js": "^9.33.0",
|
|
34
|
+
"eslint": "^9.33.0",
|
|
35
|
+
"globals": "^16.3.0"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"yaml": "^1.10.0"
|
|
39
|
+
}
|
|
37
40
|
}
|
package/src/cli.js
CHANGED
|
@@ -30,13 +30,13 @@ export function parse(args) {
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
export async function agree(string, { stdin = null, stdout = null } = {}) {
|
|
33
|
-
stdout = stdout
|
|
34
|
-
stdin = stdin
|
|
33
|
+
stdout = stdout || process.stdout;
|
|
34
|
+
stdin = stdin || process.stdin;
|
|
35
35
|
|
|
36
36
|
stdout.write(string);
|
|
37
37
|
|
|
38
38
|
const answer = await new Promise((resolve) => {
|
|
39
|
-
stdin.on('data', data => resolve(data));
|
|
39
|
+
stdin.on('data', (data) => resolve(data));
|
|
40
40
|
});
|
|
41
41
|
|
|
42
42
|
return ['y', 'yes'].includes(`${answer}`.trim().toLowerCase());
|
|
@@ -56,8 +56,8 @@ export class Table {
|
|
|
56
56
|
const { border, header } = this.columns.reduce(({ header, border }, column, i) => {
|
|
57
57
|
const width = ansi.strip(column.header).length;
|
|
58
58
|
const padding = Math.max(0, column.width - width - 2);
|
|
59
|
-
const paddingString =
|
|
60
|
-
const borderString =
|
|
59
|
+
const paddingString = new Array(padding + 1).join(' ');
|
|
60
|
+
const borderString = new Array(column.width + 1).join('-');
|
|
61
61
|
|
|
62
62
|
return {
|
|
63
63
|
border: `${border || ''}+${borderString}${i === this.columns.length - 1 ? '+' : ''}`,
|
|
@@ -69,17 +69,17 @@ export class Table {
|
|
|
69
69
|
console.log(header);
|
|
70
70
|
console.log(ansi.gray(border));
|
|
71
71
|
|
|
72
|
-
this.rows.forEach(r => {
|
|
72
|
+
this.rows.forEach((r) => {
|
|
73
73
|
const rowString = r.reduce((row, cell, i) => {
|
|
74
|
-
const width = ansi.strip(cell).length
|
|
74
|
+
const width = ansi.strip(cell).length;
|
|
75
75
|
const padding = Math.max(0, this.columns[i].width - width - 2);
|
|
76
|
-
const paddingString =
|
|
76
|
+
const paddingString = new Array(padding + 1).join(' ');
|
|
77
77
|
|
|
78
|
-
return `${row || ''}${ansi.gray('|')} ${cell}${paddingString} ${i === r.length - 1 ? ansi.gray('|') : ''}
|
|
78
|
+
return `${row || ''}${ansi.gray('|')} ${cell}${paddingString} ${i === r.length - 1 ? ansi.gray('|') : ''}`;
|
|
79
79
|
}, '');
|
|
80
80
|
|
|
81
81
|
console.log(rowString);
|
|
82
82
|
console.log(ansi.gray(border));
|
|
83
|
-
})
|
|
83
|
+
});
|
|
84
84
|
}
|
|
85
85
|
}
|
package/src/config.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import
|
|
3
|
+
import yaml from 'yaml';
|
|
4
4
|
|
|
5
5
|
// Cached config object. The config is stored in here so it can
|
|
6
6
|
// be loaded once and imported across all modules.
|
|
@@ -43,18 +43,14 @@ export async function load(env = null) {
|
|
|
43
43
|
if (path.basename(configPath) === 'frak.config.js') {
|
|
44
44
|
Object.assign(config, (await import(configPath)).default);
|
|
45
45
|
} else if (path.basename(configPath) === '.frak') {
|
|
46
|
-
Object.assign(config, parse(fs.readFileSync(configPath).toString('utf-8')));
|
|
46
|
+
Object.assign(config, yaml.parse(fs.readFileSync(configPath).toString('utf-8')));
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
if (env in config) {
|
|
50
|
-
Object.assign(config, {...config[env]});
|
|
50
|
+
Object.assign(config, { ...config[env] });
|
|
51
51
|
|
|
52
52
|
delete config[env];
|
|
53
|
-
|
|
54
|
-
return config;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
if (env) {
|
|
53
|
+
} else if (env) {
|
|
58
54
|
throw new Error(`Missing configuration for environment "${env}"!`);
|
|
59
55
|
}
|
|
60
56
|
|
|
@@ -84,4 +80,3 @@ export function reset() {
|
|
|
84
80
|
}
|
|
85
81
|
|
|
86
82
|
export default config;
|
|
87
|
-
|
package/src/date.js
CHANGED
|
@@ -29,28 +29,28 @@ export function relative(date) {
|
|
|
29
29
|
value = 'just';
|
|
30
30
|
unit = 'now';
|
|
31
31
|
agoOrFromNow = '';
|
|
32
|
-
// Less than a minute
|
|
33
32
|
} else if (seconds < 60) {
|
|
33
|
+
// Less than a minute
|
|
34
34
|
value = Math.floor(seconds);
|
|
35
35
|
unit = plural('second', value);
|
|
36
|
-
// Less than an hour
|
|
37
36
|
} else if (seconds < 60 * 60) {
|
|
37
|
+
// Less than an hour
|
|
38
38
|
value = Math.floor(seconds / 60);
|
|
39
39
|
unit = plural('minute', value);
|
|
40
|
-
// Less than a day
|
|
41
40
|
} else if (seconds < 60 * 60 * 24) {
|
|
41
|
+
// Less than a day
|
|
42
42
|
value = Math.floor(seconds / 60 / 60);
|
|
43
43
|
unit = plural('hour', value);
|
|
44
|
-
// Less than a week
|
|
45
44
|
} else if (seconds < 60 * 60 * 24 * 7) {
|
|
45
|
+
// Less than a week
|
|
46
46
|
value = Math.floor(seconds / 60 / 60 / 24);
|
|
47
47
|
unit = plural('day', value);
|
|
48
|
-
// Less than a month
|
|
49
48
|
} else if (seconds < 60 * 60 * 24 * 30) {
|
|
49
|
+
// Less than a month
|
|
50
50
|
value = Math.floor(seconds / 60 / 60 / 24 / 7);
|
|
51
51
|
unit = plural('week', value);
|
|
52
|
-
// Less than a year
|
|
53
52
|
} else if (seconds < 60 * 60 * 24 * 365) {
|
|
53
|
+
// Less than a year
|
|
54
54
|
value = Math.floor(seconds / 60 / 60 / 24 / 30);
|
|
55
55
|
unit = plural('month', value);
|
|
56
56
|
} else {
|
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, [
|
|
8
|
+
console.log.apply(console, [ansi.bold(ansi.cyan('DEBUG:')), ...arguments]);
|
|
9
9
|
}
|
|
10
10
|
}
|
package/src/diff.js
CHANGED
|
@@ -16,7 +16,7 @@ export default async function diff(dryRunOutput, options) {
|
|
|
16
16
|
const changes = dryRunOutput
|
|
17
17
|
.split(/\r?\n/)
|
|
18
18
|
.filter(Boolean)
|
|
19
|
-
.map(line => {
|
|
19
|
+
.map((line) => {
|
|
20
20
|
// Split line into action and file, preserving whitespace for filenames
|
|
21
21
|
const [action, ...parts] = line.split(/\s+/);
|
|
22
22
|
|
|
@@ -26,7 +26,7 @@ export default async function diff(dryRunOutput, options) {
|
|
|
26
26
|
if (action.match(/^cL.+/)) {
|
|
27
27
|
const index = parts.indexOf('->');
|
|
28
28
|
|
|
29
|
-
debug({ parts })
|
|
29
|
+
debug({ parts });
|
|
30
30
|
|
|
31
31
|
file = parts.slice(0, index).join(' ');
|
|
32
32
|
target = parts.slice(index + 1).join(' ');
|
|
@@ -38,12 +38,14 @@ export default async function diff(dryRunOutput, options) {
|
|
|
38
38
|
})
|
|
39
39
|
.filter(([, file]) => file.slice(-1) !== '/');
|
|
40
40
|
|
|
41
|
-
debug({ changes })
|
|
41
|
+
debug({ changes });
|
|
42
42
|
|
|
43
43
|
let proceed = true;
|
|
44
44
|
|
|
45
45
|
if (changes.length >= 50 && options.interactive) {
|
|
46
|
-
proceed = await cli.agree(
|
|
46
|
+
proceed = await cli.agree(
|
|
47
|
+
ansi.yellow(`${changes.length} file(s) will be compared. Continue? `)
|
|
48
|
+
);
|
|
47
49
|
}
|
|
48
50
|
|
|
49
51
|
if (!proceed) {
|
|
@@ -52,53 +54,57 @@ export default async function diff(dryRunOutput, options) {
|
|
|
52
54
|
|
|
53
55
|
const files = changes.map(([, file]) => file);
|
|
54
56
|
|
|
55
|
-
const diffs = changes
|
|
56
|
-
|
|
57
|
+
const diffs = changes
|
|
58
|
+
.sort((a, b) => a[1].localeCompare(b[1]))
|
|
59
|
+
.map((change) => {
|
|
60
|
+
const [action, file] = change;
|
|
57
61
|
|
|
58
|
-
|
|
59
|
-
|
|
62
|
+
// Escape double quotes in file
|
|
63
|
+
const fileEscaped = file.replace(/"/g, '\\"');
|
|
60
64
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
65
|
+
const header = [
|
|
66
|
+
file.match(/"/)
|
|
67
|
+
? `diff --git "a/${fileEscaped}" "b/${fileEscaped}"`
|
|
68
|
+
: `diff --git a/${file} b/${file}`,
|
|
69
|
+
];
|
|
64
70
|
|
|
65
|
-
|
|
71
|
+
let diff = '';
|
|
66
72
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
73
|
+
if (action === '*deleting') {
|
|
74
|
+
header.push('deleted file mode 100644');
|
|
75
|
+
header.push('index 0000000..0000000');
|
|
70
76
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
77
|
+
diff = `diff -Naurp --label="a/${fileEscaped}" --label=/dev/null "${fileEscaped}" /dev/null`;
|
|
78
|
+
} else if (action.match(/^<f\++/)) {
|
|
79
|
+
const { mode } = statSync(file);
|
|
74
80
|
|
|
75
|
-
|
|
76
|
-
|
|
81
|
+
header.push(`new file mode ${mode.toString(8)}`);
|
|
82
|
+
header.push('index 0000000..0000000');
|
|
77
83
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
84
|
+
diff = `diff -Naurp --label=/dev/null --label="b/${fileEscaped}" /dev/null <(tar xzOf $tarball "${fileEscaped}" || cat /dev/null)`;
|
|
85
|
+
} else if (action.match(/^cL\++/)) {
|
|
86
|
+
const target = change[2];
|
|
81
87
|
|
|
82
|
-
|
|
83
|
-
|
|
88
|
+
header.push('new file mode 120000');
|
|
89
|
+
header.push('index 0000000..0000000');
|
|
84
90
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
91
|
+
diff = `diff -Naurp --label=/dev/null --label="b/${fileEscaped}" /dev/null <(printf %s "${target}")`;
|
|
92
|
+
} else if (action.match(/^cL.+/)) {
|
|
93
|
+
const target = change[2];
|
|
88
94
|
|
|
89
|
-
|
|
95
|
+
header.push('index 0000000..0000000');
|
|
90
96
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
97
|
+
diff = `diff -Naurp --label="a/${fileEscaped}" --label="b/${fileEscaped}" <(printf %s "$(readlink "${fileEscaped}")") <(printf %s "${target}")`;
|
|
98
|
+
} else {
|
|
99
|
+
const { mode } = statSync(file);
|
|
94
100
|
|
|
95
|
-
|
|
101
|
+
header.push(`index 0000000..0000000 ${mode.toString(8)}`);
|
|
96
102
|
|
|
97
|
-
|
|
98
|
-
|
|
103
|
+
diff = `diff -Naurp --label="a/${fileEscaped}" --label="b/${fileEscaped}" "${fileEscaped}" <(tar xzOf $tarball "${fileEscaped}" || cat /dev/null)`;
|
|
104
|
+
}
|
|
99
105
|
|
|
100
|
-
|
|
101
|
-
|
|
106
|
+
return `printf "%s\\n" '${header.join('\n')}'; ${diff}`;
|
|
107
|
+
});
|
|
102
108
|
|
|
103
109
|
if (diffs.length > 0) {
|
|
104
110
|
debug({ diffs, files });
|
|
@@ -111,9 +117,10 @@ export default async function diff(dryRunOutput, options) {
|
|
|
111
117
|
'tarball=$(mktemp /tmp/frak-diff-tarball.XXXXXX);',
|
|
112
118
|
'cat > $tarball;',
|
|
113
119
|
`cd ${config.root};`,
|
|
114
|
-
diffs.join('; '),
|
|
120
|
+
diffs.join('; '),
|
|
121
|
+
';',
|
|
115
122
|
'rm $tarball',
|
|
116
|
-
])
|
|
123
|
+
]);
|
|
117
124
|
tar.stdout.pipe(ssh.stdin);
|
|
118
125
|
|
|
119
126
|
let less;
|
|
@@ -125,22 +132,35 @@ export default async function diff(dryRunOutput, options) {
|
|
|
125
132
|
if (options.interactive && options.colorize) {
|
|
126
133
|
const colorize = spawn('sed', [
|
|
127
134
|
// headers
|
|
128
|
-
'-e',
|
|
129
|
-
|
|
130
|
-
'-e',
|
|
131
|
-
|
|
132
|
-
'-e',
|
|
133
|
-
|
|
135
|
+
'-e',
|
|
136
|
+
`s/^\\(diff --git .*\\)/${ansi.bold('\\1')}/`,
|
|
137
|
+
'-e',
|
|
138
|
+
`s/^\\(index .*\\)/${ansi.bold('\\1')}/`,
|
|
139
|
+
'-e',
|
|
140
|
+
`s/^\\(new file mode .*\\)/${ansi.bold('\\1')}/`,
|
|
141
|
+
'-e',
|
|
142
|
+
`s/^\\(deleted file mode .*\\)/${ansi.bold('\\1')}/`,
|
|
143
|
+
'-e',
|
|
144
|
+
`s/^\\(--- .*\\)/${ansi.bold('\\1')}/`,
|
|
145
|
+
'-e',
|
|
146
|
+
`s/^\\(+++ .*\\)/${ansi.bold('\\1')}/`,
|
|
134
147
|
// additions
|
|
135
|
-
'-e',
|
|
136
|
-
|
|
148
|
+
'-e',
|
|
149
|
+
`s/^\\(+[^+].*\\)/${ansi.green('\\1')}/`,
|
|
150
|
+
'-e',
|
|
151
|
+
`s/^\\(+\\)$/${ansi.green('\\1')}/`,
|
|
137
152
|
// deletions
|
|
138
|
-
'-e',
|
|
139
|
-
|
|
153
|
+
'-e',
|
|
154
|
+
`s/^\\(-[^-].*\\)/${ansi.red('\\1')}/`,
|
|
155
|
+
'-e',
|
|
156
|
+
`s/^\\(-\\)$/${ansi.red('\\1')}/`,
|
|
140
157
|
// markers
|
|
141
|
-
'-e',
|
|
142
|
-
|
|
143
|
-
'-e',
|
|
158
|
+
'-e',
|
|
159
|
+
`s/^\\(@@ -[0-9][0-9]*,[0-9][0-9]* +[0-9][0-9]*,[0-9][0-9]* .*@@\\)/${ansi.blue('\\1')}/`,
|
|
160
|
+
'-e',
|
|
161
|
+
`s/^\\(@@ -[0-9][0-9]*,[0-9][0-9]* +[0-9][0-9]* .*@@\\)/${ansi.blue('\\1')}/`,
|
|
162
|
+
'-e',
|
|
163
|
+
`s/^\\(@@ -[0-9][0-9]* +[0-9][0-9]*,[0-9][0-9]* .*@@\\)/${ansi.blue('\\1')}/`,
|
|
144
164
|
]);
|
|
145
165
|
ssh.stdout.pipe(colorize.stdin);
|
|
146
166
|
colorize.stdout.pipe(less.stdin);
|
|
@@ -153,19 +173,19 @@ export default async function diff(dryRunOutput, options) {
|
|
|
153
173
|
less.on('exit', (code) => {
|
|
154
174
|
if (code === 0) {
|
|
155
175
|
resolve();
|
|
156
|
-
/* node:coverage disable */
|
|
157
176
|
} else {
|
|
177
|
+
/* node :coverage disable */
|
|
158
178
|
reject(`exited with code ${code}`);
|
|
179
|
+
/* node:coverage enable */
|
|
159
180
|
}
|
|
160
|
-
/* node:coverage enable */
|
|
161
181
|
});
|
|
162
182
|
});
|
|
163
|
-
/* node:coverage disable */
|
|
164
183
|
} else {
|
|
184
|
+
/* node:coverage disable */
|
|
165
185
|
return await new Promise((resolve) => {
|
|
166
186
|
let diff = '';
|
|
167
187
|
|
|
168
|
-
ssh.stdout.on('data', (data) => diff += data);
|
|
188
|
+
ssh.stdout.on('data', (data) => (diff += data));
|
|
169
189
|
|
|
170
190
|
ssh.stderr.pipe(process.stderr);
|
|
171
191
|
|
|
@@ -173,8 +193,8 @@ export default async function diff(dryRunOutput, options) {
|
|
|
173
193
|
resolve(diff);
|
|
174
194
|
});
|
|
175
195
|
});
|
|
196
|
+
/* node:coverage enable */
|
|
176
197
|
}
|
|
177
|
-
/* node:coverage enable */
|
|
178
198
|
} else if (options.interactive) {
|
|
179
199
|
console.log(ansi.yellow('No changes detected.'));
|
|
180
200
|
}
|
package/src/rsync.js
CHANGED
|
@@ -32,23 +32,24 @@ export function filters() {
|
|
|
32
32
|
|
|
33
33
|
// merge path
|
|
34
34
|
if (options.path) {
|
|
35
|
-
const paths = options.path
|
|
36
|
-
.
|
|
37
|
-
.map(p =>
|
|
38
|
-
.map(p =>
|
|
35
|
+
const paths = options.path
|
|
36
|
+
.split(/((?<!\\) )+/)
|
|
37
|
+
.map((p) => p.replace(/\\ /g, ' '))
|
|
38
|
+
.map((p) => path.resolve(p))
|
|
39
|
+
.map((p) => p) // TODO glob // paths.map! { |p| p[/[*?]/] ? Dir.glob(p) : p }
|
|
39
40
|
.flat(Infinity)
|
|
40
41
|
// make paths relative to .frak file
|
|
41
|
-
.map(p => p.replace(path.dirname(locateConfig()), ''));
|
|
42
|
+
.map((p) => p.replace(path.dirname(locateConfig()), ''));
|
|
42
43
|
|
|
43
44
|
// include all parent directories to fix recursive rsync from excluding all
|
|
44
|
-
paths.forEach(p => {
|
|
45
|
+
paths.forEach((p) => {
|
|
45
46
|
includes.push(p);
|
|
46
47
|
|
|
47
48
|
while (path.dirname(p) !== '/') {
|
|
48
49
|
excludes.push(`${path.dirname(p)}/*`);
|
|
49
50
|
|
|
50
51
|
p = path.dirname(p);
|
|
51
|
-
includes.push(p)
|
|
52
|
+
includes.push(p);
|
|
52
53
|
}
|
|
53
54
|
});
|
|
54
55
|
|
|
@@ -60,8 +61,8 @@ export function filters() {
|
|
|
60
61
|
filters.push('.e .frakignore');
|
|
61
62
|
}
|
|
62
63
|
|
|
63
|
-
includes.forEach(include => filters.push(`+ ${include}`));
|
|
64
|
-
excludes.forEach(include => filters.push(`- ${include}`));
|
|
64
|
+
includes.forEach((include) => filters.push(`+ ${include}`));
|
|
65
|
+
excludes.forEach((include) => filters.push(`- ${include}`));
|
|
65
66
|
|
|
66
67
|
return filters;
|
|
67
68
|
}
|
|
@@ -90,35 +91,45 @@ export function exec(...options) {
|
|
|
90
91
|
}
|
|
91
92
|
|
|
92
93
|
let backup = [];
|
|
93
|
-
let backupName =
|
|
94
|
-
|
|
95
|
-
|
|
94
|
+
let backupName =
|
|
95
|
+
'name' in commandOptions
|
|
96
|
+
? `${commandOptions.name}_${new Date().toISOString()}`
|
|
97
|
+
: new Date().toISOString();
|
|
96
98
|
|
|
97
99
|
// Figure out backup options
|
|
98
|
-
backup = [
|
|
99
|
-
'--exclude=.backups/',
|
|
100
|
-
];
|
|
100
|
+
backup = ['--exclude=.backups/'];
|
|
101
101
|
|
|
102
102
|
// Keep the same backup path for all rsync commands
|
|
103
103
|
if (backupPath === null) {
|
|
104
|
-
backupPath = `.backups/${backupName}
|
|
104
|
+
backupPath = `.backups/${backupName}`;
|
|
105
105
|
}
|
|
106
106
|
|
|
107
107
|
// This should only happen on push
|
|
108
108
|
if (command === 'push') {
|
|
109
|
-
backup = [...backup, '--backup', `--backup-dir
|
|
109
|
+
backup = [...backup, '--backup', `--backup-dir=${backupPath}`];
|
|
110
110
|
}
|
|
111
111
|
|
|
112
112
|
const args = [
|
|
113
113
|
'rsync',
|
|
114
114
|
[
|
|
115
|
-
'--archive',
|
|
116
|
-
'--
|
|
115
|
+
'--archive',
|
|
116
|
+
'--no-group',
|
|
117
|
+
'--no-owner',
|
|
118
|
+
'--no-perms',
|
|
119
|
+
'--no-times',
|
|
120
|
+
'--human-readable',
|
|
121
|
+
'--compress',
|
|
122
|
+
'--checksum',
|
|
123
|
+
'--itemize-changes',
|
|
124
|
+
'--recursive',
|
|
125
|
+
'--delete',
|
|
126
|
+
process.env.DEBUG ? '--rsh=ssh' : '--rsh=ssh -q',
|
|
117
127
|
`--rsync-path=${config['rsync_path'] ? config['rsync_path'] : 'rsync'}`,
|
|
118
128
|
'--filter=. -',
|
|
119
129
|
...backup,
|
|
120
130
|
...options,
|
|
121
|
-
source,
|
|
131
|
+
source,
|
|
132
|
+
destination,
|
|
122
133
|
],
|
|
123
134
|
];
|
|
124
135
|
|
|
@@ -126,17 +137,21 @@ export function exec(...options) {
|
|
|
126
137
|
debug(args.flat().join(' '));
|
|
127
138
|
debug('filters:', filters());
|
|
128
139
|
|
|
129
|
-
let output = ''
|
|
140
|
+
let output = '',
|
|
141
|
+
stderr = '';
|
|
130
142
|
|
|
131
143
|
const rsync = spawn.apply(null, args);
|
|
132
144
|
|
|
133
|
-
const echo = spawn('echo', [
|
|
145
|
+
const echo = spawn('echo', [filters().join('\n')]);
|
|
134
146
|
|
|
135
147
|
echo.stdout.pipe(rsync.stdin);
|
|
136
148
|
|
|
137
149
|
rsync.stdout.on('data', (data) => {
|
|
138
150
|
output += `${data}`;
|
|
139
151
|
});
|
|
152
|
+
rsync.stderr.on('data', (data) => {
|
|
153
|
+
stderr += `${data}`;
|
|
154
|
+
});
|
|
140
155
|
|
|
141
156
|
/* node:coverage disable */
|
|
142
157
|
rsync.stderr.on('data', (data) => {
|
|
@@ -151,11 +166,11 @@ export function exec(...options) {
|
|
|
151
166
|
output,
|
|
152
167
|
backupPath,
|
|
153
168
|
});
|
|
154
|
-
/* node:coverage disable */
|
|
155
169
|
} else {
|
|
156
|
-
|
|
170
|
+
/* node:coverage disable */
|
|
171
|
+
reject(`rsync exited with code ${code}: ${stderr}`);
|
|
172
|
+
/* node:coverage enable */
|
|
157
173
|
}
|
|
158
|
-
/* node:coverage enable */
|
|
159
174
|
});
|
|
160
175
|
});
|
|
161
176
|
}
|
package/src/ssh.js
CHANGED
|
@@ -6,7 +6,7 @@ export function exec(command, options = {}) {
|
|
|
6
6
|
|
|
7
7
|
const cmd = `cd ${options.root || '.'}; sh`;
|
|
8
8
|
|
|
9
|
-
const ssh = spawn('ssh', [
|
|
9
|
+
const ssh = spawn('ssh', [options.server, '--', cmd]);
|
|
10
10
|
|
|
11
11
|
// const echo = spawn('echo', [ command ]);
|
|
12
12
|
|
|
@@ -31,11 +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 */
|
|
35
34
|
} else {
|
|
35
|
+
/* node:coverage disable */
|
|
36
36
|
reject(`exited with code ${code}`);
|
|
37
|
+
/* node:coverage enable */
|
|
37
38
|
}
|
|
38
|
-
/* node:coverage enable */
|
|
39
39
|
});
|
|
40
40
|
});
|
|
41
41
|
|