@troylar/flightdeck 2.11.1 → 2.12.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/CHANGELOG.md +23 -0
- package/README.md +17 -3
- package/bin/flightdeck.js +121 -31
- package/dist/flightdeck.html +773 -57
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,29 @@
|
|
|
2
2
|
|
|
3
3
|
<!-- Generated by build.py from src/releases.json. Edit that file, not this one. -->
|
|
4
4
|
|
|
5
|
+
## 2.12.0 · Nov 1, 2026
|
|
6
|
+
|
|
7
|
+
AI assistant files, from the app.
|
|
8
|
+
|
|
9
|
+
### New
|
|
10
|
+
|
|
11
|
+
- **See and update AI assistant files without a command line** (Settings). Each file says where it stands. Update follows flightdeck install's rules: a copy is kept first, a file your team changed is never replaced, and one that changes while updating is left alone and named. A save in the instant between the last check and the write can still be overwritten, so close editors on these files first. Update never changes the app file itself. *Where:* Settings › AI assistant files
|
|
12
|
+
- **Show and Copy any file, and merge requests** (Settings). Show and Copy give any file as this version ships it, for an AI assistant that can't open the folder. A file waiting to merge also has Copy merge request, and I merged it, which puts the waiting copy away beside the replaced files. *Where:* Settings › AI assistant files
|
|
13
|
+
- **Keep skills in another folder** (Settings). In another folder saves and updates the skills in a folder your AI assistant reads, with their own record there, like flightdeck skills --to. *Where:* Settings › AI assistant files
|
|
14
|
+
|
|
15
|
+
### Improved
|
|
16
|
+
|
|
17
|
+
- **Folder health judges these files by the installer's rules** (Folder health). Folder health and flightdeck install now agree on each file. Its AI assistants items open AI assistant files. *Where:* Settings › Folder health
|
|
18
|
+
|
|
19
|
+
## 2.11.2 · Oct 31, 2026
|
|
20
|
+
|
|
21
|
+
Duplicated tabs and Conditions.
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
|
|
25
|
+
- **A duplicated tab always gets its own identity** (Runs). A tab duplicated on a slow computer takes its own identity even when the first window answers late, and follows the recording in the first window. *Where:* Duplicating a browser tab with a run open
|
|
26
|
+
- **Conditions moves as soon as a newer draft arrives** (Editor). When a newer draft gives the selected step an Only when, Conditions comes out of More options right away. *Where:* The builder
|
|
27
|
+
|
|
5
28
|
## 2.11.1 · Oct 31, 2026
|
|
6
29
|
|
|
7
30
|
Odd names in a folder, and notes that share an ID.
|
package/README.md
CHANGED
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
## Quick start
|
|
26
26
|
|
|
27
27
|
1. `npx @troylar/flightdeck open` opens the app in your browser (needs Node 18 or later).
|
|
28
|
-
2. No Node? Save [flightdeck.html](https://unpkg.com/@troylar/flightdeck@2.
|
|
28
|
+
2. No Node? Save [flightdeck.html](https://unpkg.com/@troylar/flightdeck@2.12.0/dist/flightdeck.html) from the npm package as a file (right-click, **Save link as…**) and open that file in Edge or Chrome.
|
|
29
29
|
3. Choose **Look around the examples** to try it, or **Start your team** to set up your team's folder.
|
|
30
30
|
|
|
31
31
|
<p align="center"><a href="https://github.com/troylar/flightdeck/blob/main/docs/images/run-light.png" title="Open in GitHub's image viewer"><picture><source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/troylar/flightdeck/main/docs/images/run-dark.png"><img src="https://raw.githubusercontent.com/troylar/flightdeck/main/docs/images/run-light.png" alt="A release in progress on the run screen: 7/15 answered in the header with Details, Screen share, Receipt and More; a strip saying you're recording, with Pause and Hand off and the run's details; Phases with each gate's state; the checklist with Next up pinned at its top; and the selected step, whose text shows the run's tag (Start the production deploy job v2026.10.16), with the values to copy, Record the result, Done, Done differently and Blocked" width="1000"></picture></a></p>
|
|
@@ -373,7 +373,7 @@ Thousands of runs open and refresh without stalling. Long lists show a page at a
|
|
|
373
373
|
|
|
374
374
|
## Skills for your AI assistant
|
|
375
375
|
|
|
376
|
-
`flightdeck install` puts these in the team folder's `.flightdeck/skills/`, with an `AGENTS.md` that any assistant reads. They're plain `SKILL.md` files, so any assistant that supports skills can use them. Open the team folder with your assistant and ask for one by name:
|
|
376
|
+
**Settings → AI assistant files** in the app (or `flightdeck install` in a terminal) puts these in the team folder's `.flightdeck/skills/`, with an `AGENTS.md` that any assistant reads. They're plain `SKILL.md` files, so any assistant that supports skills can use them. Open the team folder with your assistant and ask for one by name:
|
|
377
377
|
|
|
378
378
|
| Skill | Does |
|
|
379
379
|
|---|---|
|
|
@@ -388,6 +388,20 @@ Thousands of runs open and refresh without stalling. Long lists show a page at a
|
|
|
388
388
|
|
|
389
389
|
In `AGENTS.md`, only the part between the `flightdeck:begin` and `flightdeck:end` markers is Flightdeck's; your Team notes below it are never touched. A file your team changed is left alone on update, and the new default waits in `.flightdeck/updates/<version>/` with a prompt for your assistant to merge it.
|
|
390
390
|
|
|
391
|
+
### AI assistant files, from the app
|
|
392
|
+
|
|
393
|
+
**Settings → AI assistant files** installs and updates `AGENTS.md` and the skills without a command line. Each file shows where it stands:
|
|
394
|
+
|
|
395
|
+
| Status | Means |
|
|
396
|
+
|---|---|
|
|
397
|
+
| Up to date | The same as this version ships |
|
|
398
|
+
| Update ready | An older default nobody changed; **Update** replaces it, after keeping a copy |
|
|
399
|
+
| Not there yet, or Deleted, can be added again | **Update** adds it |
|
|
400
|
+
| You changed it | Never replaced |
|
|
401
|
+
| Waiting to merge | Your team changed it and the default changed too; the new default waits in `.flightdeck/updates/<version>/` |
|
|
402
|
+
|
|
403
|
+
**Show** and **Copy** give any file as this version ships it, for an assistant that can't open the folder. **Copy merge request** copies the prompt for your assistant, and **I merged it** puts the waiting copy away beside the replaced files once it's merged. **In another folder** does the same for an assistant that reads skills from its own folder (`flightdeck skills --to`). The app and the command share the same rules for these files and the same record (`.flightdeck/installed.json`), so they never disagree; the command also updates `flightdeck.html` itself, which the app leaves alone. Folder health's **AI assistants** items open this screen.
|
|
404
|
+
|
|
391
405
|
## Safe by design
|
|
392
406
|
|
|
393
407
|
<p align="center"><a href="https://github.com/troylar/flightdeck/blob/main/docs/images/safe-light.png" title="Open in GitHub's image viewer"><picture><source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/troylar/flightdeck/main/docs/images/safe-dark.png"><img src="https://raw.githubusercontent.com/troylar/flightdeck/main/docs/images/safe-light.png" alt="Safe by design: nothing leaves your machine; no server or accounts; saves that check first; built for OneDrive; a careful command; accessible." width="1000"></picture></a></p>
|
|
@@ -402,7 +416,7 @@ Roles prevent ordinary mistakes, not attacks: anyone who can open the team folde
|
|
|
402
416
|
|
|
403
417
|
## Updating and when something goes wrong
|
|
404
418
|
|
|
405
|
-
Run `flightdeck install <folder>` again with the new version (or `npx @troylar/flightdeck@<version> install <folder>`). It updates what's still a Flightdeck default and keeps what your team changed. [Updating Flightdeck](https://github.com/troylar/flightdeck/blob/main/docs/updating.md) walks through backups, mixed versions and [rolling back](https://github.com/troylar/flightdeck/blob/main/docs/updating.md#rolling-back).
|
|
419
|
+
Run `flightdeck install <folder>` again with the new version (or `npx @troylar/flightdeck@<version> install <folder>`). It updates what's still a Flightdeck default and keeps what your team changed. Once the new `flightdeck.html` is in the folder, **Settings → AI assistant files** can update `AGENTS.md` and the skills from the app the same way. [Updating Flightdeck](https://github.com/troylar/flightdeck/blob/main/docs/updating.md) walks through backups, mixed versions and [rolling back](https://github.com/troylar/flightdeck/blob/main/docs/updating.md#rolling-back).
|
|
406
420
|
|
|
407
421
|
When the app shows a warning, **Settings → When something goes wrong** explains it in the app; the same guide is [online](https://github.com/troylar/flightdeck/blob/main/docs/recovery.md), covering conflict copies, lost folder access, OneDrive copies and restoring from a backup.
|
|
408
422
|
|
package/bin/flightdeck.js
CHANGED
|
@@ -186,27 +186,40 @@ function appStatus(dir, file, html, mine, force) {
|
|
|
186
186
|
A copy still waiting in .flightdeck/updates/<any version>/ keeps a file at merge until someone deletes the copy.
|
|
187
187
|
With --force, kept and merge files are replaced too, after a copy is saved. */
|
|
188
188
|
function classify(t, files, base, force) {
|
|
189
|
-
|
|
189
|
+
const items = files.map((f) => {
|
|
190
190
|
const parts = f.rel.split('/'), now = readInside(t.dir, parts), want = hash(managed(f.key, f.text));
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
191
|
+
return { key: f.key, rel: f.rel, baseKey: f.baseKey, parts, text: f.text, now, want, have: now === null ? null : hash(managed(f.key, now)), staged: staged(t, f.rel) };
|
|
192
|
+
});
|
|
193
|
+
const why = decide(items.map((i) => ({ key: i.key, baseKey: i.baseKey, want: i.want, have: i.have, staged: i.staged.map((x) => ({ version: x.version, hash: hash(managed(i.key, x.text)) })) })), t.record, base, t.version, force);
|
|
194
|
+
return items.map((i, n) => Object.assign(i, why[n], { key: i.key }));
|
|
195
|
+
}
|
|
196
|
+
/* The decision alone, from hashes: the same as PF.contract.classify (test/assistant-files.test.js checks they agree), plus
|
|
197
|
+
forced, which only --force gives. */
|
|
198
|
+
function decide(items, record, base, version, force) {
|
|
199
|
+
const rf = (record && record.files) || {};
|
|
200
|
+
return items.map((it) => {
|
|
201
|
+
const out = { key: it.key, status: '' }, st = it.staged || [];
|
|
202
|
+
if (it.have === null || it.have === undefined) { out.status = rf[it.key] ? 'missing' : 'new'; return out; }
|
|
203
|
+
if (st.length && !force) {
|
|
204
|
+
out.status = 'merge'; out.waiting = st.map((x) => x.version).join(', ');
|
|
205
|
+
out.stage = it.have !== it.want && !st.some((x) => x.version === version || x.hash === it.want);
|
|
206
|
+
return out;
|
|
207
|
+
}
|
|
208
|
+
if (it.have === it.want) out.status = 'current';
|
|
209
|
+
else if ((base[it.baseKey || baselineKey(it.key)] || {})[it.have]) out.status = 'update';
|
|
210
|
+
else if (force) out.status = 'forced';
|
|
211
|
+
else if (rf[it.key] === it.want) out.status = 'kept';
|
|
212
|
+
else out.status = 'merge';
|
|
213
|
+
return out;
|
|
203
214
|
});
|
|
204
215
|
}
|
|
205
216
|
/* The versions under the updates folder, and which of them hold a copy of rel still waiting to be merged. */
|
|
217
|
+
/* Oldest first, dev last: the order install and the app both list waiting versions in. */
|
|
218
|
+
function byVersion(a, b) { return a === b ? 0 : a === 'dev' ? 1 : b === 'dev' ? -1 : cmp(a, b); }
|
|
206
219
|
function versionsStaged(t) {
|
|
207
220
|
if (t.versions) return t.versions;
|
|
208
221
|
const home = folders(t.dir, t.updates, false);
|
|
209
|
-
t.versions = home ? fs.readdirSync(home).filter((v) => /^(\d+\.\d+\.\d+|dev)$/.test(v) && folders(t.dir, t.updates.concat(v), false)) : [];
|
|
222
|
+
t.versions = home ? fs.readdirSync(home).filter((v) => /^(\d+\.\d+\.\d+|dev)$/.test(v) && folders(t.dir, t.updates.concat(v), false)).sort(byVersion) : [];
|
|
210
223
|
return t.versions;
|
|
211
224
|
}
|
|
212
225
|
function staged(t, rel) {
|
|
@@ -230,12 +243,19 @@ function installedNewer(r) {
|
|
|
230
243
|
/* The folder's record, checked field by field. Anyone can edit it, so a record that isn't exactly what this command writes is
|
|
231
244
|
ignored (and said so), and only entries for files this version installs, with a well-formed hash, are read. */
|
|
232
245
|
function readRecord(t, keys) {
|
|
233
|
-
|
|
234
|
-
|
|
246
|
+
t.recordText = readInside(t.dir, t.recordAt);
|
|
247
|
+
const r = parseRecord(t.recordText, keys);
|
|
248
|
+
if (r.newer) fail(path.join(t.dir, ...t.recordAt) + ' ' + r.newer + ' Nothing was changed. Use the newer Flightdeck package.');
|
|
249
|
+
return r;
|
|
250
|
+
}
|
|
251
|
+
/* The record's text as install reads it: the same as PF.contract.readRecord (test/assistant-files.test.js checks they agree). */
|
|
252
|
+
function parseRecord(text, keys) {
|
|
253
|
+
const none = { files: {} };
|
|
254
|
+
if (text === null || text === undefined) return none;
|
|
235
255
|
let r = null;
|
|
236
|
-
try { r = JSON.parse(text); } catch (e) { /*
|
|
256
|
+
try { r = JSON.parse(text); } catch (e) { /* ignored below */ }
|
|
237
257
|
const newer = installedNewer(r);
|
|
238
|
-
if (newer)
|
|
258
|
+
if (newer) return { newer };
|
|
239
259
|
const ok = r && typeof r === 'object' && !Array.isArray(r) && r.format === 'flightdeck-installed/1' && typeof r.version === 'string' && /^(\d+\.\d+\.\d+|dev)$/.test(r.version)
|
|
240
260
|
&& typeof r.at === 'string' && (r.by === 'cli' || r.by === 'app') && r.files && typeof r.files === 'object' && !Array.isArray(r.files)
|
|
241
261
|
&& Object.keys(r.files).every((k) => typeof r.files[k] === 'string' && /^[0-9a-f]{64}$/.test(r.files[k]));
|
|
@@ -248,19 +268,21 @@ function readRecord(t, keys) {
|
|
|
248
268
|
function stamp(d) { const p = (n) => String(n).padStart(2, '0'); return d.getFullYear() + '-' + p(d.getMonth() + 1) + '-' + p(d.getDate()) + ' ' + p(d.getHours()) + p(d.getMinutes()); }
|
|
249
269
|
function fineStamp(d) { const p = (n, w) => String(n).padStart(w || 2, '0'); return stamp(d).replace(' ', '-') + p(d.getSeconds()) + '-' + p(d.getMilliseconds(), 3); }
|
|
250
270
|
|
|
251
|
-
function mergeReadme(version, items, t) {
|
|
252
|
-
|
|
271
|
+
function mergeReadme(version, items, t) { return mergeReadmeText(version, items, t.updates, t.home); }
|
|
272
|
+
/* The same text as PF.contract.mergeReadme (test/assistant-files.test.js checks they agree). */
|
|
273
|
+
function mergeReadmeText(version, items, updates, home) {
|
|
274
|
+
const here = updates.concat(version).join('/');
|
|
253
275
|
return `# Flightdeck ${version}: files to merge
|
|
254
276
|
|
|
255
277
|
Flightdeck ${version} changed these files, and your team changed them too, so the update left your versions in place:
|
|
256
278
|
|
|
257
279
|
${items.map((i) => '- `' + i.rel + '`' + (i.key === AGENTS ? ' (only the part between the flightdeck markers)' : '')).join('\n')}
|
|
258
280
|
|
|
259
|
-
The new default of each file is in this folder (\`${here}/\`), at the same path it has in ${
|
|
281
|
+
The new default of each file is in this folder (\`${here}/\`), at the same path it has in ${home}.
|
|
260
282
|
|
|
261
283
|
## Ask your AI assistant to merge them
|
|
262
284
|
|
|
263
|
-
Open your AI assistant in ${
|
|
285
|
+
Open your AI assistant in ${home}, and give it this:
|
|
264
286
|
|
|
265
287
|
> Flightdeck ${version} put new default versions of some files in \`${here}/\`; its README.md lists them. For each one, compare the new default with our file at the same path. Keep our own changes, and bring in what the new default adds or changes. In AGENTS.md, change only the part between \`<!-- flightdeck:begin -->\` and \`<!-- flightdeck:end -->\`; anything that is ours belongs under Team notes, below the end marker. Show me each change before you save it. When a file is merged, delete its copy here. When only README.md is left, delete this folder.
|
|
266
288
|
|
|
@@ -268,14 +290,61 @@ Then run the same flightdeck command with \`--check\`: exit code 0 means nothing
|
|
|
268
290
|
`;
|
|
269
291
|
}
|
|
270
292
|
|
|
293
|
+
/* What an install would write has to be something the app can read back (PF.contract.MAX_BYTES): over the limit, nothing is
|
|
294
|
+
touched. The same check and words as Settings, AI assistant files (afRoom). */
|
|
295
|
+
const CONTRACT_MAX = 8 * 1048576;
|
|
296
|
+
function roomFor(t, items) {
|
|
297
|
+
const why = (name, n, then) => name + ' would be too big for Flightdeck to read back (' + (Math.ceil(n / 1048576 * 100) / 100) + ' MB, over ' + (CONTRACT_MAX / 1048576) + ' MB). Nothing was changed. ' + then;
|
|
298
|
+
for (const i of items) {
|
|
299
|
+
if (['current', 'kept', 'merge'].includes(i.status)) continue;
|
|
300
|
+
const n = Buffer.byteLength(merged(i), 'utf8');
|
|
301
|
+
if (n > CONTRACT_MAX) fail(why(i.rel, n, 'Shorten what your team added to it, then try again.'));
|
|
302
|
+
}
|
|
303
|
+
const rec = nextRecord(t.record, items, t.version);
|
|
304
|
+
if (!rec) return;
|
|
305
|
+
const size = Buffer.byteLength(JSON.stringify({ format: 'flightdeck-installed/1', version: t.version, at: new Date().toISOString(), by: 'cli', files: rec }, null, 2) + '\n', 'utf8');
|
|
306
|
+
if (size > CONTRACT_MAX) fail(why(t.recordAt.join('/'), size, 'Remove the entries for files Flightdeck no longer ships, then try again.'));
|
|
307
|
+
}
|
|
308
|
+
/* Something in the way of where a file goes: the same words as PF.contract.outcomeWords. Every destination is looked at before
|
|
309
|
+
anything is written, so a predictable refusal never leaves an update half done. */
|
|
310
|
+
function blockedWhy(rel, want) { return rel + (want === 'file' ? ' is a folder, where Flightdeck needs a file.' : ' is a file, where Flightdeck needs a folder.') + ' Nothing was changed. Move it, then try again.'; }
|
|
311
|
+
function clearFor(dir, parts) {
|
|
312
|
+
let cur = dir;
|
|
313
|
+
for (let i = 0; i < parts.length; i++) {
|
|
314
|
+
cur = path.join(cur, parts[i]);
|
|
315
|
+
const st = lstatOrNull(cur), rel = parts.slice(0, i + 1).join('/');
|
|
316
|
+
if (!st) return;
|
|
317
|
+
if (st.isSymbolicLink()) fail('refusing to write through a link: ' + cur);
|
|
318
|
+
if (i === parts.length - 1 ? !st.isFile() : !st.isDirectory()) fail(blockedWhy(rel, i === parts.length - 1 ? 'file' : 'folder'));
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
function destinationsClear(t, items, more) {
|
|
322
|
+
const dests = (more || []).slice();
|
|
323
|
+
for (const i of items) {
|
|
324
|
+
if (['current', 'kept'].includes(i.status)) continue;
|
|
325
|
+
if (i.status === 'merge') {
|
|
326
|
+
if (i.waiting && !i.stage) continue;
|
|
327
|
+
dests.push(t.updates.concat(t.version, i.rel.split('/')), t.updates.concat(t.version, 'README.md'), t.updates.concat(t.version, 'replaced', 'x'));
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
dests.push(i.parts);
|
|
331
|
+
if (i.now !== null && i.now !== merged(i)) dests.push(t.updates.concat(t.version, 'replaced', 'x'));
|
|
332
|
+
}
|
|
333
|
+
if (nextRecord(t.record, items, t.version)) dests.push(t.recordAt);
|
|
334
|
+
dests.forEach((parts) => clearFor(t.dir, parts));
|
|
335
|
+
}
|
|
271
336
|
/* install and skills: one routine for the team folder, and for skills --to <dir>. */
|
|
272
337
|
function contract(t, files, opts) {
|
|
273
338
|
const base = baselines(opts.html), now = new Date();
|
|
339
|
+
if (!opts.check || opts.room) clearFor(t.dir, t.updates.concat(t.version, 'x'));
|
|
274
340
|
inside(t.dir, t.updates.concat(t.version, 'replaced'));
|
|
275
341
|
t.record = readRecord(t, files.map((f) => f.key));
|
|
276
342
|
t.changed = [];
|
|
277
343
|
const items = classify(t, files, base, opts.force);
|
|
344
|
+
if (!opts.check || opts.room) { roomFor(t, items); destinationsClear(t, items, opts.more); }
|
|
278
345
|
if (opts.check) return items;
|
|
346
|
+
/* The merge list as it was before anything is written: the only text it may replace. */
|
|
347
|
+
const readmeAt = t.updates.concat(t.version, 'README.md'), readmeWas = items.some((i) => i.status === 'merge' && (!i.waiting || i.stage)) ? readInside(t.dir, readmeAt) : null;
|
|
279
348
|
for (const i of items) {
|
|
280
349
|
if (['current', 'kept'].includes(i.status) || (i.status === 'merge' && i.waiting && !i.stage)) {
|
|
281
350
|
if (opts.force) i.staged.forEach((x) => fileAway(t, x));
|
|
@@ -293,13 +362,34 @@ function contract(t, files, opts) {
|
|
|
293
362
|
}
|
|
294
363
|
}
|
|
295
364
|
const merges = items.filter((i) => i.status === 'merge' && !i.changed);
|
|
296
|
-
if (merges.some((i) => !i.waiting || i.stage))
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
365
|
+
if (merges.some((i) => !i.waiting || i.stage)) {
|
|
366
|
+
const next = mergeReadme(t.version, merges.filter((i) => !i.waiting || i.stage || i.staged.some((x) => x.version === t.version)), t);
|
|
367
|
+
try {
|
|
368
|
+
if (readmeWas !== null && readmeWas !== next) keepCopy(t, ['merge-README.md'], readmeWas);
|
|
369
|
+
safeWrite(t.dir, readmeAt, next, readmeWas);
|
|
370
|
+
} catch (e) {
|
|
371
|
+
if (!(e instanceof Changed)) throw e;
|
|
372
|
+
t.changed.push(readmeAt.join('/'));
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
const rec = nextRecord(t.record, items, t.version);
|
|
376
|
+
if (!rec) return items;
|
|
377
|
+
try {
|
|
378
|
+
safeWrite(t.dir, t.recordAt, JSON.stringify({ format: 'flightdeck-installed/1', version: t.version, at: now.toISOString(), by: 'cli', files: rec }, null, 2) + '\n', t.recordText);
|
|
379
|
+
t.recordWritten = true;
|
|
380
|
+
} catch (e) {
|
|
381
|
+
if (!(e instanceof Changed)) throw e;
|
|
382
|
+
t.changed.push(t.recordAt.join('/'));
|
|
383
|
+
}
|
|
301
384
|
return items;
|
|
302
385
|
}
|
|
386
|
+
/* The record's files after an update, or null when it would be written unchanged: the same as PF.contract.nextRecord. */
|
|
387
|
+
function nextRecord(record, items, version) {
|
|
388
|
+
const rec = Object.assign({}, record.all || {});
|
|
389
|
+
items.forEach((i) => { if (!i.changed) rec[i.key] = i.want; });
|
|
390
|
+
if (!record.ignored && record.version === version && JSON.stringify(rec) === JSON.stringify(record.all)) return null;
|
|
391
|
+
return rec;
|
|
392
|
+
}
|
|
303
393
|
/* Every file install replaces, the app included, is first copied to a folder of its own under
|
|
304
394
|
.flightdeck/updates/<version>/replaced/, made new for this run (never one an earlier run used), and the copy is read back.
|
|
305
395
|
No checked copy, no replacement. */
|
|
@@ -330,8 +420,8 @@ function report(items, check, out) {
|
|
|
330
420
|
}
|
|
331
421
|
/* What install says after the file list: an ignored record, and files left alone because they changed while it ran. */
|
|
332
422
|
function notes(t, out) {
|
|
333
|
-
if (t.record && t.record.ignored) out.push('Ignored ' + t.recordAt.join('/') + ': it isn\'t a record this version of Flightdeck wrote, so it wasn\'t trusted. A new one was written.');
|
|
334
|
-
if (t.changed && t.changed.length) out.push('Left as they are, because they changed while
|
|
423
|
+
if (t.record && t.record.ignored) out.push('Ignored ' + t.recordAt.join('/') + ': it isn\'t a record this version of Flightdeck wrote, so it wasn\'t trusted.' + (t.recordWritten ? ' A new one was written.' : ''));
|
|
424
|
+
if (t.changed && t.changed.length) out.push('Left as they are, because they changed while updating: ' + t.changed.join(', ') + '. Those weren\'t replaced; check them again.');
|
|
335
425
|
}
|
|
336
426
|
function exitFor(statuses) {
|
|
337
427
|
if (statuses.includes('merge')) return EXIT.merge;
|
|
@@ -1724,7 +1814,7 @@ function main(argv) {
|
|
|
1724
1814
|
return { out: out.join('\n'), code };
|
|
1725
1815
|
}
|
|
1726
1816
|
/* Everything is checked before anything is written, so a refusal (a link, say) leaves the folder as it was. */
|
|
1727
|
-
if (!appOnly) contract(t, files, { html, force, check: true });
|
|
1817
|
+
if (!appOnly) contract(t, files, { html, force, check: true, room: true, more: [[NAME, appS], [OLD, oldS]].filter(([file, st]) => st.status !== 'current' && (file !== OLD || keepOld)).map(([file]) => [file]) });
|
|
1728
1818
|
t.changed = [];
|
|
1729
1819
|
[[NAME, appS], [OLD, oldS]].forEach(([file, st]) => {
|
|
1730
1820
|
if (st.status === 'current' || (file === OLD && !keepOld)) return;
|
|
@@ -1808,4 +1898,4 @@ if (require.main === module) {
|
|
|
1808
1898
|
if (r && typeof r.then === 'function') r.then(done, failed); else done(r);
|
|
1809
1899
|
} catch (e) { failed(e); }
|
|
1810
1900
|
}
|
|
1811
|
-
module.exports = { _hooks, main, installedNewer, versionOf, cmp, skillFiles, norm, region, managed, merged, hash, baselineKey, examples, readAdapter, loadEngine, readData, requestData, writeRequest, reportData, writeOnce, EXIT };
|
|
1901
|
+
module.exports = { _hooks, main, installedNewer, parseRecord, decide, mergeReadmeText, nextRecord, baselinesOf: baselines, versionOf, cmp, skillFiles, norm, region, managed, merged, hash, baselineKey, examples, readAdapter, loadEngine, readData, requestData, writeRequest, reportData, writeOnce, EXIT };
|