@frakjs/frak 0.2.0 → 0.3.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 +57 -46
- package/images/logos/Frak Logo - Green - Circle.png +0 -0
- package/package.json +6 -3
- 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
|
@@ -20,15 +20,11 @@ import * as date from '../src/date.js';
|
|
|
20
20
|
* @param {String} env
|
|
21
21
|
*/
|
|
22
22
|
async function tryLoadConfig(env) {
|
|
23
|
-
|
|
24
|
-
await loadConfig(env);
|
|
25
|
-
} catch (e) {
|
|
23
|
+
return await loadConfig(env).catch((e) => {
|
|
26
24
|
console.error(ansi.red(e.message));
|
|
27
25
|
|
|
28
26
|
process.exit(1);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
return;
|
|
27
|
+
});
|
|
32
28
|
}
|
|
33
29
|
|
|
34
30
|
/**
|
|
@@ -55,20 +51,22 @@ async function main(args) {
|
|
|
55
51
|
|
|
56
52
|
debug('parsed:', { command, options });
|
|
57
53
|
|
|
58
|
-
// Initialize Frak in the current directory
|
|
59
54
|
if (command === 'init') {
|
|
55
|
+
// Initialize Frak in the current directory
|
|
60
56
|
const configPath = path.join(process.cwd(), 'frak.config.js');
|
|
61
57
|
|
|
62
58
|
if (!existsSync(configPath)) {
|
|
63
|
-
writeFileSync(
|
|
59
|
+
writeFileSync(
|
|
60
|
+
configPath,
|
|
61
|
+
`export default ${JSON.stringify({ server: 'example.com', root: '/var/www/html' }, null, 4)}`
|
|
62
|
+
);
|
|
64
63
|
console.info(ansi.green('Configuration file created: frak.config.js'));
|
|
65
64
|
} else {
|
|
66
65
|
console.error(ansi.red('Frak is already initialized in this directory.'));
|
|
67
66
|
}
|
|
68
|
-
|
|
69
|
-
// Start a console session, or run the provided command on
|
|
70
|
-
// the remote server.
|
|
71
67
|
} else if (command === 'console') {
|
|
68
|
+
// Start a console session, or run the provided command on
|
|
69
|
+
// the remote server.
|
|
72
70
|
await tryLoadConfig(options.env);
|
|
73
71
|
|
|
74
72
|
if (!config.server) {
|
|
@@ -77,14 +75,16 @@ async function main(args) {
|
|
|
77
75
|
}
|
|
78
76
|
|
|
79
77
|
try {
|
|
80
|
-
const cmd = options.command
|
|
78
|
+
const cmd = options.command || '\\$SHELL -l -i';
|
|
81
79
|
|
|
82
|
-
execSync(`ssh -t ${config.server} -- cd ${config.root} \\&\\& ${cmd}`, {
|
|
80
|
+
execSync(`ssh -t ${config.server} -- cd ${config.root} \\&\\& ${cmd}`, {
|
|
81
|
+
stdio: 'inherit',
|
|
82
|
+
});
|
|
83
83
|
} catch (e) {
|
|
84
|
-
debug(e)
|
|
84
|
+
debug(e);
|
|
85
85
|
}
|
|
86
|
-
// Show the differences between local files and remote server
|
|
87
86
|
} else if (command === 'diff') {
|
|
87
|
+
// Show the differences between local files and remote server
|
|
88
88
|
await tryLoadConfig(options.env);
|
|
89
89
|
|
|
90
90
|
try {
|
|
@@ -98,9 +98,8 @@ async function main(args) {
|
|
|
98
98
|
|
|
99
99
|
console.error(e.message);
|
|
100
100
|
}
|
|
101
|
-
|
|
102
|
-
// Push or pull changes to remote server
|
|
103
101
|
} else if (command === 'push' || command === 'pull') {
|
|
102
|
+
// Push or pull changes to remote server
|
|
104
103
|
await tryLoadConfig(options.env);
|
|
105
104
|
|
|
106
105
|
// Execute dry-run
|
|
@@ -115,7 +114,9 @@ async function main(args) {
|
|
|
115
114
|
console.log('');
|
|
116
115
|
}
|
|
117
116
|
|
|
118
|
-
const proceed = await cli.agree(
|
|
117
|
+
const proceed = await cli.agree(
|
|
118
|
+
ansi.yellow('The above actions will be taken. Continue? (This cannot be undone): ')
|
|
119
|
+
);
|
|
119
120
|
|
|
120
121
|
if (!proceed) {
|
|
121
122
|
process.exit(1);
|
|
@@ -125,14 +126,13 @@ async function main(args) {
|
|
|
125
126
|
let patch = '';
|
|
126
127
|
|
|
127
128
|
if (command === 'push') {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
}
|
|
129
|
+
patch = await diff(output, { interactive: false })
|
|
130
|
+
/* node:coverage disable */
|
|
131
|
+
.catch((e) => {
|
|
132
|
+
console.error(e);
|
|
133
|
+
|
|
134
|
+
process.exit(1);
|
|
135
|
+
});
|
|
136
136
|
/* node:coverage enable */
|
|
137
137
|
}
|
|
138
138
|
|
|
@@ -145,7 +145,11 @@ async function main(args) {
|
|
|
145
145
|
// Place patch file in backup dir
|
|
146
146
|
if (command === 'push') {
|
|
147
147
|
const hash = sha256(patch).slice(0, 7);
|
|
148
|
-
const copy = spawn('ssh', [
|
|
148
|
+
const copy = spawn('ssh', [
|
|
149
|
+
config.server,
|
|
150
|
+
'--',
|
|
151
|
+
`cat > '${config.root || '.'}/${backupPath}/${hash}.patch'`,
|
|
152
|
+
]);
|
|
149
153
|
|
|
150
154
|
copy.stdout.on('data', (data) => debug('' + data));
|
|
151
155
|
copy.stderr.on('data', (data) => debug('' + data));
|
|
@@ -158,13 +162,12 @@ async function main(args) {
|
|
|
158
162
|
|
|
159
163
|
debug({ command: config.after });
|
|
160
164
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
/* node:coverage enable */
|
|
165
|
+
await ssh
|
|
166
|
+
.exec(config.after, {
|
|
167
|
+
server: config.server,
|
|
168
|
+
root: config.root,
|
|
169
|
+
})
|
|
170
|
+
.catch(debug);
|
|
168
171
|
}
|
|
169
172
|
|
|
170
173
|
// TODO Webhook
|
|
@@ -179,13 +182,16 @@ async function main(args) {
|
|
|
179
182
|
const grep = {
|
|
180
183
|
additions: `grep -e '^+' "$NAME"/*.patch | grep -v '^+++' | wc -l`,
|
|
181
184
|
deletions: `grep -e '^-' "$NAME"/*.patch | grep -v '^---' | wc -l`,
|
|
182
|
-
}
|
|
185
|
+
};
|
|
183
186
|
const ls = `stat --format='%Y %n' .backups/* | sort -r`;
|
|
184
187
|
|
|
185
|
-
const list = ssh.exec(
|
|
188
|
+
const list = ssh.exec(
|
|
189
|
+
`${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})'`,
|
|
190
|
+
{ quiet: true, server: config.server, root: config.root }
|
|
191
|
+
);
|
|
186
192
|
let backups = '';
|
|
187
193
|
|
|
188
|
-
list.stdout.on('data', data => backups += data);
|
|
194
|
+
list.stdout.on('data', (data) => (backups += data));
|
|
189
195
|
|
|
190
196
|
await list;
|
|
191
197
|
|
|
@@ -201,17 +207,22 @@ async function main(args) {
|
|
|
201
207
|
{
|
|
202
208
|
header: 'Changes',
|
|
203
209
|
width: 30,
|
|
204
|
-
}
|
|
210
|
+
},
|
|
205
211
|
]);
|
|
206
212
|
|
|
207
|
-
backups
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
213
|
+
backups
|
|
214
|
+
.trim()
|
|
215
|
+
.split(/\n/)
|
|
216
|
+
.forEach((backup) => {
|
|
217
|
+
const [timestamp, name, additions, deletions] = backup.split(delimeter);
|
|
218
|
+
table.row([
|
|
219
|
+
name.replace(/^.backups\//, ''),
|
|
220
|
+
date.relative(timestamp * 1000),
|
|
221
|
+
ansi.green(`${additions} addition${additions == 1 ? '' : 's'}`) +
|
|
222
|
+
', ' +
|
|
223
|
+
ansi.red(`${deletions} deletion${deletions == 1 ? '' : 's'}`),
|
|
224
|
+
]);
|
|
225
|
+
});
|
|
215
226
|
|
|
216
227
|
table.print();
|
|
217
228
|
} else {
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frakjs/frak",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "A commandline deployment tool for syncing files to remote servers.",
|
|
5
5
|
"main": "index.js",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": ">=12"
|
|
8
|
+
},
|
|
6
9
|
"bin": {
|
|
7
10
|
"frak": "bin/frak.js"
|
|
8
11
|
},
|
|
9
12
|
"type": "module",
|
|
10
13
|
"scripts": {
|
|
11
14
|
"eslint": "eslint",
|
|
12
|
-
"test": "node --
|
|
15
|
+
"test": "node --experimental-test-coverage tests/index.test.js"
|
|
13
16
|
},
|
|
14
17
|
"repository": {
|
|
15
18
|
"type": "git",
|
|
@@ -32,6 +35,6 @@
|
|
|
32
35
|
"globals": "^16.3.0"
|
|
33
36
|
},
|
|
34
37
|
"dependencies": {
|
|
35
|
-
"yaml": "^
|
|
38
|
+
"yaml": "^1.10.0"
|
|
36
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 * as 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
|
|