@troylar/flightdeck 2.11.2 → 2.12.1
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 +28 -0
- package/README.md +18 -3
- package/bin/flightdeck.js +132 -47
- package/dist/flightdeck.html +916 -56
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,34 @@
|
|
|
2
2
|
|
|
3
3
|
<!-- Generated by build.py from src/releases.json. Edit that file, not this one. -->
|
|
4
4
|
|
|
5
|
+
## 2.12.1 · Nov 2, 2026
|
|
6
|
+
|
|
7
|
+
Get your AI assistant started, and let it read all its instructions.
|
|
8
|
+
|
|
9
|
+
### New
|
|
10
|
+
|
|
11
|
+
- **Copy instructions for your assistant** (Settings). One button copies a first prompt for your assistant. It says to read AGENTS.md to the end, says where the skills are, and asks what you want done. Open the team folder itself as the workspace, in Agent mode, then paste it. *Where:* Settings › AI assistant files
|
|
12
|
+
- **Download skills ZIP** (Settings). Download the skills as a ZIP, made from what's already in the app file, with no network and no folder needed. It holds the same files flightdeck skills --to writes. What each skill is for, and what to ask for: docs/skills.md. *Where:* Settings › AI assistant files
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- **AGENTS.md is short enough to be read to the end** (AI assistants). AGENTS.md is under 28 KiB, the safety rules come first, and the long formats moved to .flightdeck/skills/_flightdeck/file-formats.md. Every rule stays. Update it with flightdeck install or Settings; Team notes stay as they are. Keep Team notes to about 4 KiB.
|
|
17
|
+
- **A folder set up by a newer version is left alone** (AI assistants). flightdeck install, Settings and Folder health leave a folder a newer version set up alone, in the same words. An old default an older app left waiting is named, with its path, never merged or counted; it's safe to delete by hand. *Where:* Settings › AI assistant files
|
|
18
|
+
|
|
19
|
+
## 2.12.0 · Nov 1, 2026
|
|
20
|
+
|
|
21
|
+
AI assistant files, from the app.
|
|
22
|
+
|
|
23
|
+
### New
|
|
24
|
+
|
|
25
|
+
- **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
|
|
26
|
+
- **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
|
|
27
|
+
- **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
|
|
28
|
+
|
|
29
|
+
### Improved
|
|
30
|
+
|
|
31
|
+
- **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
|
|
32
|
+
|
|
5
33
|
## 2.11.2 · Oct 31, 2026
|
|
6
34
|
|
|
7
35
|
Duplicated tabs and Conditions.
|
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.1/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
|
|
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 itself as the workspace (in Agent mode, for Copilot in VS Code) and start with **Copy instructions for your assistant** on that screen. Then say what you need, or ask for a skill by name. [Skills for your AI assistant](https://github.com/troylar/flightdeck/blob/main/docs/skills.md) lists each one by job, with what to ask for:
|
|
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. **Copy instructions for your assistant** copies the first prompt to paste, and **Download skills ZIP** gives you the skills as a file, with no folder and no network. 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
|
|
|
@@ -463,6 +477,7 @@ Flightdeck was called Preflight before 1.9.0. Team folders, runs and published v
|
|
|
463
477
|
|---|---|---|
|
|
464
478
|
| [Checklist format](https://github.com/troylar/flightdeck/blob/main/docs/map-format.md) | Anyone writing checklists, and their AI assistant | Every mark, field and step type, and what stops a checklist from publishing |
|
|
465
479
|
| [Flightdeck and OneDrive](https://github.com/troylar/flightdeck/blob/main/docs/onedrive.md) | Whoever sets up the team folder | What keeps runs safe on a synced folder, and what it can't promise |
|
|
480
|
+
| [Skills for your AI assistant](https://github.com/troylar/flightdeck/blob/main/docs/skills.md) | Anyone working with an AI assistant | Starting one, what to ask for, each skill by job |
|
|
466
481
|
| [Updating Flightdeck](https://github.com/troylar/flightdeck/blob/main/docs/updating.md) | Whoever keeps the team's copy current | A step-by-step update, mixed versions and rolling back |
|
|
467
482
|
| [When something goes wrong](https://github.com/troylar/flightdeck/blob/main/docs/recovery.md) | Anyone who sees a warning | What each problem means and what to do, step by step |
|
|
468
483
|
| [Evidence packs](https://github.com/troylar/flightdeck/blob/main/docs/evidence.md) | Anyone who needs proof of a run | What's in a pack and how to check it |
|
package/bin/flightdeck.js
CHANGED
|
@@ -186,27 +186,48 @@ 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
|
+
/* An old copy an older app left (PF.contract.classifyCopies, the app's own rule, from the texts as read: a byte-order mark
|
|
194
|
+
kept): named, never merged, counted or removed. */
|
|
195
|
+
const K = contractRules();
|
|
196
|
+
items.forEach((i) => {
|
|
197
|
+
const all = i.staged, said = JSON.parse(JSON.stringify(K.classifyCopies({ key: i.key, baseKey: i.baseKey, shipped: i.text, version: t.version, base, active: i.now, copies: all.map((x) => ({ version: x.version, text: x.text })) })));
|
|
198
|
+
i.old = all.filter((x, n) => said[n] === 'old');
|
|
199
|
+
i.staged = all.filter((x, n) => said[n] !== 'old');
|
|
200
|
+
});
|
|
201
|
+
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);
|
|
202
|
+
return items.map((i, n) => Object.assign(i, why[n], { key: i.key }));
|
|
203
|
+
}
|
|
204
|
+
/* The decision alone, from hashes: the same as PF.contract.classify (test/assistant-files.test.js checks they agree), plus
|
|
205
|
+
forced, which only --force gives. */
|
|
206
|
+
function decide(items, record, base, version, force) {
|
|
207
|
+
const rf = (record && record.files) || {};
|
|
208
|
+
return items.map((it) => {
|
|
209
|
+
const out = { key: it.key, status: '' }, st = it.staged || [];
|
|
210
|
+
if (it.have === null || it.have === undefined) { out.status = rf[it.key] ? 'missing' : 'new'; return out; }
|
|
211
|
+
if (st.length && !force) {
|
|
212
|
+
out.status = 'merge'; out.waiting = st.map((x) => x.version).join(', ');
|
|
213
|
+
out.stage = it.have !== it.want && !st.some((x) => x.version === version || x.hash === it.want);
|
|
214
|
+
return out;
|
|
215
|
+
}
|
|
216
|
+
if (it.have === it.want) out.status = 'current';
|
|
217
|
+
else if ((base[it.baseKey || baselineKey(it.key)] || {})[it.have]) out.status = 'update';
|
|
218
|
+
else if (force) out.status = 'forced';
|
|
219
|
+
else if (rf[it.key] === it.want) out.status = 'kept';
|
|
220
|
+
else out.status = 'merge';
|
|
221
|
+
return out;
|
|
203
222
|
});
|
|
204
223
|
}
|
|
205
224
|
/* The versions under the updates folder, and which of them hold a copy of rel still waiting to be merged. */
|
|
225
|
+
/* Oldest first, dev last: the order install and the app both list waiting versions in. */
|
|
226
|
+
function byVersion(a, b) { return a === b ? 0 : a === 'dev' ? 1 : b === 'dev' ? -1 : cmp(a, b); }
|
|
206
227
|
function versionsStaged(t) {
|
|
207
228
|
if (t.versions) return t.versions;
|
|
208
229
|
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)) : [];
|
|
230
|
+
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
231
|
return t.versions;
|
|
211
232
|
}
|
|
212
233
|
function staged(t, rel) {
|
|
@@ -221,46 +242,41 @@ function merged(item) {
|
|
|
221
242
|
}
|
|
222
243
|
/* A record written by a newer Flightdeck (flightdeck-installed/2 on, or a must-understand feature): never read for meaning and
|
|
223
244
|
never rewritten. The same verdict as PF.contract.cleanInstalled (test/cli.test.js checks they agree). */
|
|
224
|
-
function installedNewer(r) {
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
}
|
|
245
|
+
function installedNewer(r, mine) { return contractRules().recordNewer(r, mine); }
|
|
246
|
+
/* The app's own contract rules (PF.contract), from this package's app file, so a verdict the app, Folder health and this
|
|
247
|
+
command all reach is one function, never a copy. */
|
|
248
|
+
let engineRules = null;
|
|
249
|
+
function contractRules() { return (engineRules = engineRules || loadEngine().PF.contract); }
|
|
230
250
|
/* 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
251
|
ignored (and said so), and only entries for files this version installs, with a well-formed hash, are read. */
|
|
232
252
|
function readRecord(t, keys) {
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
const newer = installedNewer(r);
|
|
238
|
-
if (newer) fail(path.join(t.dir, ...t.recordAt) + ' ' + newer + ' Nothing was changed. Use the newer Flightdeck package.');
|
|
239
|
-
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
|
-
&& typeof r.at === 'string' && (r.by === 'cli' || r.by === 'app') && r.files && typeof r.files === 'object' && !Array.isArray(r.files)
|
|
241
|
-
&& Object.keys(r.files).every((k) => typeof r.files[k] === 'string' && /^[0-9a-f]{64}$/.test(r.files[k]));
|
|
242
|
-
if (!ok) return Object.assign(none, { ignored: true });
|
|
243
|
-
/* files: what this command may consult; all: every entry, kept as it is for files this command doesn't handle. */
|
|
244
|
-
const files = {};
|
|
245
|
-
keys.forEach((k) => { if (Object.prototype.hasOwnProperty.call(r.files, k)) files[k] = r.files[k]; });
|
|
246
|
-
return { version: r.version, files, all: Object.assign({}, r.files) };
|
|
253
|
+
t.recordText = readInside(t.dir, t.recordAt);
|
|
254
|
+
const r = parseRecord(t.recordText, keys, t.version);
|
|
255
|
+
if (r.newer) fail(contractRules().outcomeWords({ state: 'newer', newer: r.newer }, { home: t.home, updates: t.updates, recordAt: t.recordAt.join('/'), version: t.version }).text);
|
|
256
|
+
return r;
|
|
247
257
|
}
|
|
258
|
+
/* The record's text as install reads it: the same as PF.contract.readRecord (test/assistant-files.test.js checks they agree). */
|
|
259
|
+
/* The record's text as install reads it: the app's own PF.contract.readRecord, so the app and this command parse it the same way
|
|
260
|
+
(copied out of the engine's context, so it compares as a plain object here). */
|
|
261
|
+
function parseRecord(text, keys, mine) { return JSON.parse(JSON.stringify(contractRules().readRecord(text === undefined ? null : text, keys, mine))); }
|
|
248
262
|
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
263
|
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
264
|
|
|
251
|
-
function mergeReadme(version, items, t) {
|
|
252
|
-
|
|
265
|
+
function mergeReadme(version, items, t) { return mergeReadmeText(version, items, t.updates, t.home); }
|
|
266
|
+
/* The same text as PF.contract.mergeReadme (test/assistant-files.test.js checks they agree). */
|
|
267
|
+
function mergeReadmeText(version, items, updates, home) {
|
|
268
|
+
const here = updates.concat(version).join('/');
|
|
253
269
|
return `# Flightdeck ${version}: files to merge
|
|
254
270
|
|
|
255
271
|
Flightdeck ${version} changed these files, and your team changed them too, so the update left your versions in place:
|
|
256
272
|
|
|
257
273
|
${items.map((i) => '- `' + i.rel + '`' + (i.key === AGENTS ? ' (only the part between the flightdeck markers)' : '')).join('\n')}
|
|
258
274
|
|
|
259
|
-
The new default of each file is in this folder (\`${here}/\`), at the same path it has in ${
|
|
275
|
+
The new default of each file is in this folder (\`${here}/\`), at the same path it has in ${home}.
|
|
260
276
|
|
|
261
277
|
## Ask your AI assistant to merge them
|
|
262
278
|
|
|
263
|
-
Open your AI assistant in ${
|
|
279
|
+
Open your AI assistant in ${home}, and give it this:
|
|
264
280
|
|
|
265
281
|
> 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
282
|
|
|
@@ -268,14 +284,61 @@ Then run the same flightdeck command with \`--check\`: exit code 0 means nothing
|
|
|
268
284
|
`;
|
|
269
285
|
}
|
|
270
286
|
|
|
287
|
+
/* What an install would write has to be something the app can read back (PF.contract.MAX_BYTES): over the limit, nothing is
|
|
288
|
+
touched. The same check and words as Settings, AI assistant files (afRoom). */
|
|
289
|
+
const CONTRACT_MAX = 8 * 1048576;
|
|
290
|
+
function roomFor(t, items) {
|
|
291
|
+
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;
|
|
292
|
+
for (const i of items) {
|
|
293
|
+
if (['current', 'kept', 'merge'].includes(i.status)) continue;
|
|
294
|
+
const n = Buffer.byteLength(merged(i), 'utf8');
|
|
295
|
+
if (n > CONTRACT_MAX) fail(why(i.rel, n, 'Shorten what your team added to it, then try again.'));
|
|
296
|
+
}
|
|
297
|
+
const rec = nextRecord(t.record, items, t.version);
|
|
298
|
+
if (!rec) return;
|
|
299
|
+
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');
|
|
300
|
+
if (size > CONTRACT_MAX) fail(why(t.recordAt.join('/'), size, 'Remove the entries for files Flightdeck no longer ships, then try again.'));
|
|
301
|
+
}
|
|
302
|
+
/* Something in the way of where a file goes: the same words as PF.contract.outcomeWords. Every destination is looked at before
|
|
303
|
+
anything is written, so a predictable refusal never leaves an update half done. */
|
|
304
|
+
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.'; }
|
|
305
|
+
function clearFor(dir, parts) {
|
|
306
|
+
let cur = dir;
|
|
307
|
+
for (let i = 0; i < parts.length; i++) {
|
|
308
|
+
cur = path.join(cur, parts[i]);
|
|
309
|
+
const st = lstatOrNull(cur), rel = parts.slice(0, i + 1).join('/');
|
|
310
|
+
if (!st) return;
|
|
311
|
+
if (st.isSymbolicLink()) fail('refusing to write through a link: ' + cur);
|
|
312
|
+
if (i === parts.length - 1 ? !st.isFile() : !st.isDirectory()) fail(blockedWhy(rel, i === parts.length - 1 ? 'file' : 'folder'));
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
function destinationsClear(t, items, more) {
|
|
316
|
+
const dests = (more || []).slice();
|
|
317
|
+
for (const i of items) {
|
|
318
|
+
if (['current', 'kept'].includes(i.status)) continue;
|
|
319
|
+
if (i.status === 'merge') {
|
|
320
|
+
if (i.waiting && !i.stage) continue;
|
|
321
|
+
dests.push(t.updates.concat(t.version, i.rel.split('/')), t.updates.concat(t.version, 'README.md'), t.updates.concat(t.version, 'replaced', 'x'));
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
dests.push(i.parts);
|
|
325
|
+
if (i.now !== null && i.now !== merged(i)) dests.push(t.updates.concat(t.version, 'replaced', 'x'));
|
|
326
|
+
}
|
|
327
|
+
if (nextRecord(t.record, items, t.version)) dests.push(t.recordAt);
|
|
328
|
+
dests.forEach((parts) => clearFor(t.dir, parts));
|
|
329
|
+
}
|
|
271
330
|
/* install and skills: one routine for the team folder, and for skills --to <dir>. */
|
|
272
331
|
function contract(t, files, opts) {
|
|
273
332
|
const base = baselines(opts.html), now = new Date();
|
|
333
|
+
if (!opts.check || opts.room) clearFor(t.dir, t.updates.concat(t.version, 'x'));
|
|
274
334
|
inside(t.dir, t.updates.concat(t.version, 'replaced'));
|
|
275
335
|
t.record = readRecord(t, files.map((f) => f.key));
|
|
276
336
|
t.changed = [];
|
|
277
337
|
const items = classify(t, files, base, opts.force);
|
|
338
|
+
if (!opts.check || opts.room) { roomFor(t, items); destinationsClear(t, items, opts.more); }
|
|
278
339
|
if (opts.check) return items;
|
|
340
|
+
/* The merge list as it was before anything is written: the only text it may replace. */
|
|
341
|
+
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
342
|
for (const i of items) {
|
|
280
343
|
if (['current', 'kept'].includes(i.status) || (i.status === 'merge' && i.waiting && !i.stage)) {
|
|
281
344
|
if (opts.force) i.staged.forEach((x) => fileAway(t, x));
|
|
@@ -293,13 +356,34 @@ function contract(t, files, opts) {
|
|
|
293
356
|
}
|
|
294
357
|
}
|
|
295
358
|
const merges = items.filter((i) => i.status === 'merge' && !i.changed);
|
|
296
|
-
if (merges.some((i) => !i.waiting || i.stage))
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
359
|
+
if (merges.some((i) => !i.waiting || i.stage)) {
|
|
360
|
+
const next = mergeReadme(t.version, merges.filter((i) => !i.waiting || i.stage || i.staged.some((x) => x.version === t.version)), t);
|
|
361
|
+
try {
|
|
362
|
+
if (readmeWas !== null && readmeWas !== next) keepCopy(t, ['merge-README.md'], readmeWas);
|
|
363
|
+
safeWrite(t.dir, readmeAt, next, readmeWas);
|
|
364
|
+
} catch (e) {
|
|
365
|
+
if (!(e instanceof Changed)) throw e;
|
|
366
|
+
t.changed.push(readmeAt.join('/'));
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
const rec = nextRecord(t.record, items, t.version);
|
|
370
|
+
if (!rec) return items;
|
|
371
|
+
try {
|
|
372
|
+
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);
|
|
373
|
+
t.recordWritten = true;
|
|
374
|
+
} catch (e) {
|
|
375
|
+
if (!(e instanceof Changed)) throw e;
|
|
376
|
+
t.changed.push(t.recordAt.join('/'));
|
|
377
|
+
}
|
|
301
378
|
return items;
|
|
302
379
|
}
|
|
380
|
+
/* The record's files after an update, or null when it would be written unchanged: the same as PF.contract.nextRecord. */
|
|
381
|
+
function nextRecord(record, items, version) {
|
|
382
|
+
const rec = Object.assign({}, record.all || {});
|
|
383
|
+
items.forEach((i) => { if (!i.changed) rec[i.key] = i.want; });
|
|
384
|
+
if (!record.ignored && record.version === version && JSON.stringify(rec) === JSON.stringify(record.all)) return null;
|
|
385
|
+
return rec;
|
|
386
|
+
}
|
|
303
387
|
/* Every file install replaces, the app included, is first copied to a folder of its own under
|
|
304
388
|
.flightdeck/updates/<version>/replaced/, made new for this run (never one an earlier run used), and the copy is read back.
|
|
305
389
|
No checked copy, no replacement. */
|
|
@@ -327,11 +411,12 @@ const LABEL = { current: 'up to date', update: 'updated', new: 'added', missing:
|
|
|
327
411
|
const CHECK_LABEL = { update: 'update available', new: 'will be added', missing: 'will be added again', merge: 'needs merge', forced: 'will be replaced (--force)', kept: 'kept (your team changed it)' };
|
|
328
412
|
function report(items, check, out) {
|
|
329
413
|
items.filter((i) => i.status !== 'current').forEach((i) => out.push(' ' + (i.changed ? 'changed during install' : (check ? CHECK_LABEL : LABEL)[i.status]).padEnd(28) + i.rel + (i.waiting ? ' (still waiting in updates/' + i.waiting + ')' : '')));
|
|
414
|
+
items.filter((i) => i.old && i.old.length).forEach((i) => out.push(' ' + 'old copy, nothing to merge'.padEnd(28) + i.rel + ' (left in updates/' + i.old.map((x) => x.version).join(', ') + ' by an older Flightdeck; safe to delete by hand)'));
|
|
330
415
|
}
|
|
331
416
|
/* What install says after the file list: an ignored record, and files left alone because they changed while it ran. */
|
|
332
417
|
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
|
|
418
|
+
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.' : ''));
|
|
419
|
+
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
420
|
}
|
|
336
421
|
function exitFor(statuses) {
|
|
337
422
|
if (statuses.includes('merge')) return EXIT.merge;
|
|
@@ -1724,7 +1809,7 @@ function main(argv) {
|
|
|
1724
1809
|
return { out: out.join('\n'), code };
|
|
1725
1810
|
}
|
|
1726
1811
|
/* 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 });
|
|
1812
|
+
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
1813
|
t.changed = [];
|
|
1729
1814
|
[[NAME, appS], [OLD, oldS]].forEach(([file, st]) => {
|
|
1730
1815
|
if (st.status === 'current' || (file === OLD && !keepOld)) return;
|
|
@@ -1808,4 +1893,4 @@ if (require.main === module) {
|
|
|
1808
1893
|
if (r && typeof r.then === 'function') r.then(done, failed); else done(r);
|
|
1809
1894
|
} catch (e) { failed(e); }
|
|
1810
1895
|
}
|
|
1811
|
-
module.exports = { _hooks, main, installedNewer, versionOf, cmp, skillFiles, norm, region, managed, merged, hash, baselineKey, examples, readAdapter, loadEngine, readData, requestData, writeRequest, reportData, writeOnce, EXIT };
|
|
1896
|
+
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 };
|