kryptheon-night 0.1.7 → 0.1.8
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/bin/kryptheon-night.js +14 -1
- package/package.json +6 -4
- package/scan.js +32 -7
- package/store.js +317 -0
package/bin/kryptheon-night.js
CHANGED
|
@@ -26,6 +26,7 @@ const path = require('path');
|
|
|
26
26
|
const { Client } = require('pg');
|
|
27
27
|
|
|
28
28
|
const scanner = require('../scan.js');
|
|
29
|
+
const store = require('../store.js');
|
|
29
30
|
const recheck = require('../recheck.js');
|
|
30
31
|
const intro = require('../intro.js');
|
|
31
32
|
const trouble = require('../trouble.js');
|
|
@@ -485,7 +486,19 @@ async function main() {
|
|
|
485
486
|
//
|
|
486
487
|
// The flag still works, because anything scripted may be passing it, and
|
|
487
488
|
// now it changes nothing: with an earlier run here, this always compares.
|
|
488
|
-
|
|
489
|
+
let file;
|
|
490
|
+
try {
|
|
491
|
+
file = scanner.lastRunFile(line);
|
|
492
|
+
} catch (err) {
|
|
493
|
+
// Nowhere to keep this run means nothing to compare the next one against,
|
|
494
|
+
// and a re-check with nothing to compare against is not an answer.
|
|
495
|
+
fail('');
|
|
496
|
+
fail(' Kryptheon keeps its last run for this project in ' + store.dirFor(process.cwd()));
|
|
497
|
+
fail(' and could not write there: ' + err.message);
|
|
498
|
+
fail(' Set KRYPTHEON_HOME to a folder you can write to, and run this again.');
|
|
499
|
+
fail('');
|
|
500
|
+
process.exit(2);
|
|
501
|
+
}
|
|
489
502
|
const before = scanner.loadLastRun(file);
|
|
490
503
|
if (asked.recheck && !before) {
|
|
491
504
|
// Asked for explicitly and impossible, which is worth saying. Arriving
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kryptheon-night",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
4
4
|
"description": "Attacks a copy of your Supabase database and tells you in plain English what got in.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "commonjs",
|
|
@@ -24,15 +24,17 @@
|
|
|
24
24
|
"README.md",
|
|
25
25
|
"installer.js",
|
|
26
26
|
"sqlengine.js",
|
|
27
|
-
"engine.sql"
|
|
27
|
+
"engine.sql",
|
|
28
|
+
"store.js"
|
|
28
29
|
],
|
|
29
30
|
"engines": {
|
|
30
31
|
"node": ">=18.0.0"
|
|
31
32
|
},
|
|
32
33
|
"scripts": {
|
|
33
|
-
"check": "node package.check.js && node trouble.check.js && node intro.check.js && node finding.check.js && node outside.check.js && node recheck.check.js && node untouched.check.js && node blocked.check.js && node orphans.check.js && node guests.check.js && node usable.check.js && node twin.check.js && node engines.check.js && node external.check.js && node schema.check.js && node collision.check.js && node tamper.check.js && node orphan.check.js && node shapes.check.js && node verdicts.check.js && node shop.check.js && node loop.check.js",
|
|
34
|
-
"check:dry": "node package.check.js && node trouble.check.js && node intro.check.js && node finding.check.js && node recheck.check.js && node outside.check.js",
|
|
34
|
+
"check": "node package.check.js && node store.check.js && node trouble.check.js && node intro.check.js && node finding.check.js && node outside.check.js && node recheck.check.js && node untouched.check.js && node blocked.check.js && node orphans.check.js && node guests.check.js && node usable.check.js && node twin.check.js && node engines.check.js && node external.check.js && node schema.check.js && node collision.check.js && node tamper.check.js && node orphan.check.js && node shapes.check.js && node verdicts.check.js && node shop.check.js && node loop.check.js",
|
|
35
|
+
"check:dry": "node package.check.js && node store.check.js && node trouble.check.js && node intro.check.js && node finding.check.js && node recheck.check.js && node outside.check.js",
|
|
35
36
|
"check:package": "node package.check.js",
|
|
37
|
+
"check:store": "node store.check.js",
|
|
36
38
|
"check:trouble": "node trouble.check.js",
|
|
37
39
|
"check:intro": "node intro.check.js",
|
|
38
40
|
"check:report": "node finding.check.js",
|
package/scan.js
CHANGED
|
@@ -21,11 +21,26 @@ const collision = require('./collision.js');
|
|
|
21
21
|
const tamper = require('./tamper.js');
|
|
22
22
|
const orphan = require('./orphan.js');
|
|
23
23
|
const recheck = require('./recheck.js');
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
|
|
24
|
+
const store = require('./store.js');
|
|
25
|
+
|
|
26
|
+
// Where the last run is kept so the next one has something to compare against:
|
|
27
|
+
// the project's store under ~/.kryptheon, not a temp folder, because a
|
|
28
|
+
// re-check a week later has to find it - and not the project, because a saved
|
|
29
|
+
// run lists one app's tables and exactly where it is weak, which is the last
|
|
30
|
+
// thing that should ever be committed. It used to be .kryptheon-last.json in
|
|
31
|
+
// the project; the store moves one it finds there, once, and says so.
|
|
32
|
+
//
|
|
33
|
+
// Throws when the store cannot be made: a run that could not be saved would
|
|
34
|
+
// leave the next re-check nothing to hold anyone to.
|
|
35
|
+
function lastRunFile(say) {
|
|
36
|
+
const opened = store.open(process.cwd());
|
|
37
|
+
const lines = store.migrationLines(opened);
|
|
38
|
+
if (lines.length && say) {
|
|
39
|
+
say('');
|
|
40
|
+
lines.forEach(say);
|
|
41
|
+
}
|
|
42
|
+
return opened.nightLast;
|
|
43
|
+
}
|
|
29
44
|
|
|
30
45
|
const line = (text) => process.stdout.write(text + '\n');
|
|
31
46
|
|
|
@@ -530,7 +545,17 @@ async function main() {
|
|
|
530
545
|
process.exit(2);
|
|
531
546
|
}
|
|
532
547
|
|
|
533
|
-
|
|
548
|
+
let file;
|
|
549
|
+
try {
|
|
550
|
+
file = lastRunFile(line);
|
|
551
|
+
} catch (err) {
|
|
552
|
+
// 2, not 1: nothing was attacked, so nothing "got through".
|
|
553
|
+
console.error('');
|
|
554
|
+
console.error(' There is nowhere to keep this run (' + err.message + ').');
|
|
555
|
+
console.error(' Set KRYPTHEON_HOME to a folder you can write to, and run this again.');
|
|
556
|
+
console.error('');
|
|
557
|
+
process.exit(2);
|
|
558
|
+
}
|
|
534
559
|
const before = again ? loadLastRun(file) : null;
|
|
535
560
|
if (again && !before) {
|
|
536
561
|
// Running a fresh scan and calling it a re-check would report every
|
|
@@ -595,6 +620,6 @@ module.exports = {
|
|
|
595
620
|
saveRun: saveRun,
|
|
596
621
|
sweepOldCopies: sweepOldCopies,
|
|
597
622
|
notTestedLines: notTestedLines,
|
|
598
|
-
|
|
623
|
+
lastRunFile: lastRunFile,
|
|
599
624
|
ABANDONED_AFTER: ABANDONED_AFTER,
|
|
600
625
|
};
|
package/store.js
ADDED
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
// Where Kryptheon keeps what it remembers about a project.
|
|
2
|
+
//
|
|
3
|
+
// It used to be the project itself: kryptheon-baselines.json,
|
|
4
|
+
// kryptheon-history.jsonl, test-results/ and .kryptheon-last.json all appeared
|
|
5
|
+
// next to the person's own code. A plain landing page picked up six or seven
|
|
6
|
+
// files that way, and an AI assistant that commits "everything that changed"
|
|
7
|
+
// commits them too - the repo grows because Kryptheon is watching it.
|
|
8
|
+
//
|
|
9
|
+
// So every record now lives in one folder per project, outside it:
|
|
10
|
+
//
|
|
11
|
+
// ~/.kryptheon/projects/<folder-name>-<hash>/ (KRYPTHEON_HOME moves ~/.kryptheon)
|
|
12
|
+
// project.json which folder this store belongs to
|
|
13
|
+
// baselines.json was kryptheon-baselines.json
|
|
14
|
+
// history.jsonl was kryptheon-history.jsonl
|
|
15
|
+
// test-results/ was test-results/ (Playwright's screenshots of a failure)
|
|
16
|
+
// night-last.json was .kryptheon-last.json (kryptheon-night)
|
|
17
|
+
//
|
|
18
|
+
// Only tests/ - the recordings the person made - stays in the project.
|
|
19
|
+
//
|
|
20
|
+
// The hash is of the project's full path, so two folders that happen to share
|
|
21
|
+
// a name are never mixed. kryptheon-night carries a copy of this file and must
|
|
22
|
+
// land in the same folder for the same project; both packages check the ids
|
|
23
|
+
// below against the same fixed examples, so the two copies cannot drift apart.
|
|
24
|
+
//
|
|
25
|
+
// Records an older version left in a project are moved here the first time a
|
|
26
|
+
// project is opened, and only moved: nothing is thrown away, and when both
|
|
27
|
+
// places already hold a file the project's copy is kept in parked/ rather than
|
|
28
|
+
// overwriting what is here.
|
|
29
|
+
|
|
30
|
+
const fs = require('fs');
|
|
31
|
+
const os = require('os');
|
|
32
|
+
const path = require('path');
|
|
33
|
+
const crypto = require('crypto');
|
|
34
|
+
|
|
35
|
+
const HOME_ENV = 'KRYPTHEON_HOME';
|
|
36
|
+
|
|
37
|
+
const FILES = {
|
|
38
|
+
project: 'project.json',
|
|
39
|
+
baselines: 'baselines.json',
|
|
40
|
+
history: 'history.jsonl',
|
|
41
|
+
testResults: 'test-results',
|
|
42
|
+
nightLast: 'night-last.json',
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// What older versions wrote into the project, and where each one goes now.
|
|
46
|
+
// append - history: lines the store does not have yet are added to it
|
|
47
|
+
// park - both exist: the project's copy goes to parked/, the store's stays
|
|
48
|
+
const LEGACY = [
|
|
49
|
+
{ from: 'kryptheon-baselines.json', to: FILES.baselines, onBoth: 'park' },
|
|
50
|
+
{ from: 'kryptheon-history.jsonl', to: FILES.history, onBoth: 'append' },
|
|
51
|
+
{ from: '.kryptheon-last.json', to: FILES.nightLast, onBoth: 'park' },
|
|
52
|
+
{ from: 'test-results', to: FILES.testResults, onBoth: 'park', dir: true },
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
// A project with a Playwright config of its own owns its test-results/ -
|
|
56
|
+
// Playwright's default output folder. Moving that would take the person's own
|
|
57
|
+
// screenshots away, so it is left exactly where it is.
|
|
58
|
+
const OWN_PLAYWRIGHT = /^playwright\.config\.(js|cjs|mjs|ts|cts|mts)$/;
|
|
59
|
+
|
|
60
|
+
/** ~/.kryptheon, or wherever KRYPTHEON_HOME points. */
|
|
61
|
+
function home(env) {
|
|
62
|
+
const source = env || process.env;
|
|
63
|
+
const set = source[HOME_ENV];
|
|
64
|
+
if (set && String(set).trim()) return path.resolve(String(set).trim());
|
|
65
|
+
return path.join(os.homedir(), '.kryptheon');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** The project folder as one exact path, whatever form it was given in. */
|
|
69
|
+
function canonicalRoot(dir) {
|
|
70
|
+
const resolved = path.resolve(dir || process.cwd());
|
|
71
|
+
try {
|
|
72
|
+
return fs.realpathSync.native(resolved);
|
|
73
|
+
} catch (err) {
|
|
74
|
+
return resolved;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* <folder-name>-<12 hex>. Pure: the same path always gives the same id.
|
|
80
|
+
*
|
|
81
|
+
* Windows paths are compared without regard to case, because C:\Users\a and
|
|
82
|
+
* c:\users\a are the same folder there and must be the same project.
|
|
83
|
+
*/
|
|
84
|
+
function projectId(root, platform) {
|
|
85
|
+
const plat = platform || process.platform;
|
|
86
|
+
const P = plat === 'win32' ? path.win32 : path.posix;
|
|
87
|
+
// resolve() also drops a trailing separator and turns / into \ on Windows.
|
|
88
|
+
let key = P.resolve(String(root));
|
|
89
|
+
if (plat === 'win32') key = key.toLowerCase();
|
|
90
|
+
const hash = crypto.createHash('sha256').update(key, 'utf8').digest('hex').slice(0, 12);
|
|
91
|
+
const base = P.basename(key) || 'project';
|
|
92
|
+
const name = base.toLowerCase().replace(/[^a-z0-9._-]+/g, '-').replace(/^[-.]+|-+$/g, '').slice(0, 40) || 'project';
|
|
93
|
+
return name + '-' + hash;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* The store folder for a project. Only reads: nothing is created or moved, so
|
|
98
|
+
* the fixture and the reporter can ask for it just by being loaded.
|
|
99
|
+
*/
|
|
100
|
+
function dirFor(dir, env) {
|
|
101
|
+
const root = canonicalRoot(dir);
|
|
102
|
+
const short = path.join(home(env), 'projects', projectId(root));
|
|
103
|
+
const owner = readProject(path.join(short, FILES.project));
|
|
104
|
+
if (!owner || !owner.root || sameRoot(owner.root, root)) return short;
|
|
105
|
+
// Two different folders with the same short hash. Astronomically rare, but
|
|
106
|
+
// the one thing a store must never do is hold two projects, so the second
|
|
107
|
+
// one gets the full hash instead.
|
|
108
|
+
const full = crypto.createHash('sha256').update(root, 'utf8').digest('hex');
|
|
109
|
+
return short.replace(/-[0-9a-f]{12}$/, '-' + full);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Every file in a project's store, by name. Only reads, like dirFor. */
|
|
113
|
+
function pathsFor(dir, env) {
|
|
114
|
+
return pathsIn(dirFor(dir, env));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function pathsIn(dir) {
|
|
118
|
+
return {
|
|
119
|
+
dir: dir,
|
|
120
|
+
project: path.join(dir, FILES.project),
|
|
121
|
+
baselines: path.join(dir, FILES.baselines),
|
|
122
|
+
history: path.join(dir, FILES.history),
|
|
123
|
+
testResults: path.join(dir, FILES.testResults),
|
|
124
|
+
nightLast: path.join(dir, FILES.nightLast),
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function exists(file) {
|
|
129
|
+
try {
|
|
130
|
+
fs.lstatSync(file);
|
|
131
|
+
return true;
|
|
132
|
+
} catch (err) {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function hasOwnPlaywright(root) {
|
|
138
|
+
try {
|
|
139
|
+
return fs.readdirSync(root).some((name) => OWN_PLAYWRIGHT.test(name));
|
|
140
|
+
} catch (err) {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// rename is the whole move when both sides are on one drive. Across drives it
|
|
146
|
+
// fails with EXDEV, and then it is copy, check the copy, and only then remove.
|
|
147
|
+
function move(from, to, isDir) {
|
|
148
|
+
try {
|
|
149
|
+
fs.renameSync(from, to);
|
|
150
|
+
return;
|
|
151
|
+
} catch (err) {
|
|
152
|
+
// Anything else - a file held open by an editor, say - is reported and the
|
|
153
|
+
// record stays where it is, whole.
|
|
154
|
+
if (err.code !== 'EXDEV') throw err;
|
|
155
|
+
}
|
|
156
|
+
if (isDir) {
|
|
157
|
+
fs.cpSync(from, to, { recursive: true, errorOnExist: true, force: false });
|
|
158
|
+
fs.rmSync(from, { recursive: true, force: true });
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
const tmp = to + '.moving';
|
|
162
|
+
fs.copyFileSync(from, tmp);
|
|
163
|
+
if (fs.statSync(tmp).size !== fs.statSync(from).size) {
|
|
164
|
+
fs.rmSync(tmp, { force: true });
|
|
165
|
+
throw new Error('the copy of ' + from + ' came out a different size');
|
|
166
|
+
}
|
|
167
|
+
fs.renameSync(tmp, to);
|
|
168
|
+
fs.rmSync(from, { force: true });
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Lines of the project's history the store does not have yet, in their order.
|
|
172
|
+
// Checked line by line so moving twice - say the project file could not be
|
|
173
|
+
// removed the first time - never writes a run down twice.
|
|
174
|
+
function appendHistory(from, to) {
|
|
175
|
+
const have = new Set(fs.readFileSync(to, 'utf8').split('\n').filter((l) => l.trim()));
|
|
176
|
+
const extra = fs.readFileSync(from, 'utf8').split('\n').filter((l) => l.trim() && !have.has(l));
|
|
177
|
+
if (extra.length) {
|
|
178
|
+
const current = fs.readFileSync(to, 'utf8');
|
|
179
|
+
const lead = current.length && !current.endsWith('\n') ? '\n' : '';
|
|
180
|
+
fs.appendFileSync(to, lead + extra.join('\n') + '\n', 'utf8');
|
|
181
|
+
}
|
|
182
|
+
fs.rmSync(from, { force: true });
|
|
183
|
+
return extra.length;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function stamp(now) {
|
|
187
|
+
return (now || new Date()).toISOString().replace(/[:.]/g, '-');
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Moves what an older version left in the project into the store.
|
|
192
|
+
* Returns { moved: [names], parked: [{ name, to }], appended: [{ name, lines }],
|
|
193
|
+
* kept: [{ name, why }], failed: [{ name, why }] }.
|
|
194
|
+
*/
|
|
195
|
+
function migrate(root, dir, options) {
|
|
196
|
+
const out = { moved: [], parked: [], appended: [], kept: [], failed: [] };
|
|
197
|
+
const ownPlaywright = hasOwnPlaywright(root);
|
|
198
|
+
for (const item of LEGACY) {
|
|
199
|
+
const from = path.join(root, item.from);
|
|
200
|
+
if (!exists(from)) continue;
|
|
201
|
+
const label = item.from + (item.dir ? '/' : '');
|
|
202
|
+
if (item.dir && ownPlaywright) {
|
|
203
|
+
out.kept.push({ name: label, why: 'this project has its own Playwright config, so it is theirs' });
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
const to = path.join(dir, item.to);
|
|
207
|
+
try {
|
|
208
|
+
if (!exists(to)) {
|
|
209
|
+
move(from, to, item.dir);
|
|
210
|
+
out.moved.push(label);
|
|
211
|
+
} else if (item.onBoth === 'append') {
|
|
212
|
+
out.appended.push({ name: label, lines: appendHistory(from, to) });
|
|
213
|
+
} else {
|
|
214
|
+
const parkedDir = path.join(dir, 'parked');
|
|
215
|
+
fs.mkdirSync(parkedDir, { recursive: true });
|
|
216
|
+
const parked = path.join(parkedDir, stamp(options && options.now) + '-' + item.from.replace(/^\./, ''));
|
|
217
|
+
move(from, parked, item.dir);
|
|
218
|
+
out.parked.push({ name: label, to: parked });
|
|
219
|
+
}
|
|
220
|
+
} catch (err) {
|
|
221
|
+
out.failed.push({ name: label, why: err.message });
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return out;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* The store for the project in `dir` (default: the current folder), created if
|
|
229
|
+
* it is not there yet, with anything an older version left in the project moved
|
|
230
|
+
* into it. Safe to call again: once moved, there is nothing left to move.
|
|
231
|
+
*
|
|
232
|
+
* Throws when the store cannot be created. A record that silently cannot be
|
|
233
|
+
* written makes every run look like the first one - which reads as "nothing to
|
|
234
|
+
* compare against, all fine" - so that is never swallowed here.
|
|
235
|
+
*/
|
|
236
|
+
function open(dir, options) {
|
|
237
|
+
const env = (options && options.env) || process.env;
|
|
238
|
+
const root = canonicalRoot(dir);
|
|
239
|
+
const storeDir = dirFor(root, env);
|
|
240
|
+
fs.mkdirSync(storeDir, { recursive: true });
|
|
241
|
+
const paths = pathsIn(storeDir);
|
|
242
|
+
if (!exists(paths.project)) {
|
|
243
|
+
fs.writeFileSync(paths.project, JSON.stringify({
|
|
244
|
+
root: root,
|
|
245
|
+
name: path.basename(root),
|
|
246
|
+
id: path.basename(storeDir),
|
|
247
|
+
createdAt: new Date().toISOString(),
|
|
248
|
+
}, null, 2) + '\n', 'utf8');
|
|
249
|
+
}
|
|
250
|
+
return Object.assign({ root: root, id: path.basename(storeDir) }, paths, {
|
|
251
|
+
migration: migrate(root, storeDir, options),
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function sameRoot(a, b, platform) {
|
|
256
|
+
const plat = platform || process.platform;
|
|
257
|
+
const norm = (p) => {
|
|
258
|
+
const r = path.resolve(String(p)).replace(/[\\/]+$/, '');
|
|
259
|
+
return plat === 'win32' ? r.split('/').join('\\').toLowerCase() : r;
|
|
260
|
+
};
|
|
261
|
+
return norm(a) === norm(b);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function readProject(file) {
|
|
265
|
+
try {
|
|
266
|
+
let raw = fs.readFileSync(file, 'utf8');
|
|
267
|
+
if (raw.charCodeAt(0) === 0xfeff) raw = raw.slice(1);
|
|
268
|
+
const parsed = JSON.parse(raw);
|
|
269
|
+
return parsed && typeof parsed === 'object' ? parsed : null;
|
|
270
|
+
} catch (err) {
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** What to tell the person about a migration, or nothing when nothing moved. */
|
|
276
|
+
function migrationLines(store) {
|
|
277
|
+
const m = store && store.migration;
|
|
278
|
+
if (!m) return [];
|
|
279
|
+
const lines = [];
|
|
280
|
+
const movedNames = m.moved.concat(m.appended.map((a) => a.name)).concat(m.parked.map((p) => p.name));
|
|
281
|
+
if (movedNames.length) {
|
|
282
|
+
lines.push(' Kryptheon now keeps its records outside your project, so they can never');
|
|
283
|
+
lines.push(' end up in your repo. Moved out of this folder:');
|
|
284
|
+
for (const name of movedNames) lines.push(' ' + name);
|
|
285
|
+
lines.push(' They are now in:');
|
|
286
|
+
lines.push(' ' + store.dir);
|
|
287
|
+
for (const p of m.parked) {
|
|
288
|
+
lines.push(' ' + p.name + ' was already there too, so this folder\'s copy is kept aside in:');
|
|
289
|
+
lines.push(' ' + p.to);
|
|
290
|
+
}
|
|
291
|
+
lines.push('');
|
|
292
|
+
}
|
|
293
|
+
for (const k of m.kept) {
|
|
294
|
+
lines.push(' Left ' + k.name + ' where it is: ' + k.why + '.');
|
|
295
|
+
lines.push('');
|
|
296
|
+
}
|
|
297
|
+
for (const f of m.failed) {
|
|
298
|
+
lines.push(' Could not move ' + f.name + ' out of this folder: ' + f.why);
|
|
299
|
+
lines.push(' It is still here. Close anything that has it open and run this again.');
|
|
300
|
+
lines.push('');
|
|
301
|
+
}
|
|
302
|
+
return lines;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
module.exports = {
|
|
306
|
+
HOME_ENV: HOME_ENV,
|
|
307
|
+
FILES: FILES,
|
|
308
|
+
LEGACY: LEGACY,
|
|
309
|
+
home: home,
|
|
310
|
+
canonicalRoot: canonicalRoot,
|
|
311
|
+
projectId: projectId,
|
|
312
|
+
dirFor: dirFor,
|
|
313
|
+
pathsFor: pathsFor,
|
|
314
|
+
open: open,
|
|
315
|
+
migrate: migrate,
|
|
316
|
+
migrationLines: migrationLines,
|
|
317
|
+
};
|