create-pmos 0.134.0 → 0.135.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/package.json +1 -1
- package/payload/KIT-VERSION +1 -1
- package/payload/planning/evals/eval-calibration.md +1 -1
- package/payload/planning/okrs/health-budgets.md +1 -1
- package/payload/state/discoveries.md +1 -1
- package/workspace/bin/notes-replay.js +235 -0
- package/workspace/c3-briefing-test.js +130 -0
- package/workspace/c3-foundation-test.js +159 -0
- package/workspace/c3-ib-items-test.js +105 -0
- package/workspace/c3-ib-more-test.js +120 -0
- package/workspace/c3-ib-triage-backlog-test.js +119 -0
- package/workspace/c3-ini-harness.js +66 -0
- package/workspace/c3-initiatives-test.js +140 -0
- package/workspace/c3-one-home-test.js +14 -1
- package/workspace/c3-reports-test.js +57 -18
- package/workspace/c3-settings-test.js +139 -0
- package/workspace/c3-work-test.js +42 -10
- package/workspace/fixtures/one-home-baseline.json +1 -9
- package/workspace/guide/briefing-cards.md +15 -3
- package/workspace/guide/briefing.md +12 -8
- package/workspace/guide/calendar-popup.md +2 -2
- package/workspace/guide/console-calendar.md +39 -10
- package/workspace/guide/index.md +1 -0
- package/workspace/guide/initiative-boards.md +65 -0
- package/workspace/guide/jira.md +3 -1
- package/workspace/guide/layout.md +2 -0
- package/workspace/guide/my-work.md +5 -3
- package/workspace/guide/notes.md +16 -1
- package/workspace/guide/reports-dashboards.md +29 -5
- package/workspace/guide/settings.md +68 -0
- package/workspace/guide/welcome.md +10 -10
- package/workspace/lib/calendar-cli.js +3 -2
- package/workspace/lib/calendar-layers.js +21 -10
- package/workspace/lib/calendar-tidy.js +230 -0
- package/workspace/lib/engine/boards.js +542 -0
- package/workspace/lib/engine/breakdown-ops.js +180 -0
- package/workspace/lib/engine/calendar-import.js +84 -0
- package/workspace/lib/engine/calendar-model.js +185 -0
- package/workspace/lib/engine/card-state.js +92 -0
- package/workspace/lib/engine/derived/derive.js +92 -0
- package/workspace/lib/engine/derived/plan.js +3 -0
- package/workspace/lib/engine/derived/trust.js +3 -0
- package/workspace/lib/engine/doc-health.js +142 -0
- package/workspace/lib/engine/doc-sheet.js +75 -0
- package/workspace/lib/engine/intake.js +78 -0
- package/workspace/lib/engine/outbox.js +86 -0
- package/workspace/lib/engine/plan-forecast.js +342 -0
- package/workspace/lib/engine/promises.js +29 -0
- package/workspace/lib/engine/records/index.js +114 -0
- package/workspace/lib/engine/releases.js +96 -0
- package/workspace/lib/engine/replies.js +99 -0
- package/workspace/lib/engine/reply-contract/reply-contract.js +372 -0
- package/workspace/lib/engine/report-families.js +175 -0
- package/workspace/lib/engine/roadmap-ops.js +150 -0
- package/workspace/lib/engine/sessions-list.js +27 -0
- package/workspace/lib/engine/signal-rules.js +95 -0
- package/workspace/lib/engine/signals/cards.js +57 -0
- package/workspace/lib/engine/signals/detectors.js +370 -0
- package/workspace/lib/engine/signals/facts-cli.js +8 -0
- package/workspace/lib/engine/signals/facts.js +197 -0
- package/workspace/lib/engine/signals/index.js +33 -0
- package/workspace/lib/engine/signals/kinds.js +110 -0
- package/workspace/lib/engine/signals/layout-paths.js +44 -0
- package/workspace/lib/engine/signals/options.js +24 -0
- package/workspace/lib/engine/signals/score.js +72 -0
- package/workspace/lib/engine/signals/select.js +47 -0
- package/workspace/lib/engine/step-planner.js +65 -0
- package/workspace/lib/engine/trust/claim-rules.json +93 -0
- package/workspace/lib/engine/trust/import-calendar.js +21 -0
- package/workspace/lib/engine/trust/import-guard.js +36 -0
- package/workspace/lib/engine/trust/ledger.js +53 -0
- package/workspace/lib/engine/trust/outbox.js +45 -0
- package/workspace/lib/engine/trust/plan.js +5 -0
- package/workspace/lib/engine/trust/trust.js +21 -0
- package/workspace/lib/engine/truth-table.js +57 -0
- package/workspace/lib/engine/workspace-lib.js +24 -0
- package/workspace/lib/factory-join.js +1 -1
- package/workspace/lib/facts-async.js +4 -4
- package/workspace/lib/headless.js +95 -0
- package/workspace/lib/import-overlay.js +2 -1
- package/workspace/lib/jira-status.js +30 -3
- package/workspace/lib/notes-capture.js +9 -1
- package/workspace/lib/notes-final.js +38 -13
- package/workspace/lib/notes-replay.js +125 -0
- package/workspace/lib/notes-screen.js +180 -0
- package/workspace/lib/notes-whisper.js +10 -3
- package/workspace/lib/outbox-store.js +10 -6
- package/workspace/lib/pmos-verbs.js +3 -1
- package/workspace/lib/proactive-cli.js +7 -0
- package/workspace/lib/proactive.js +15 -2
- package/workspace/lib/repo-script.js +36 -0
- package/workspace/lib/report-dashboards.js +5 -2
- package/workspace/lib/report-frame.js +122 -0
- package/workspace/lib/report-schedules.js +113 -0
- package/workspace/lib/route-modules.js +9 -2
- package/workspace/lib/routes-boards.js +734 -0
- package/workspace/lib/routes-brief-page.js +13 -4
- package/workspace/lib/routes-briefing.js +22 -11
- package/workspace/lib/routes-c3-init.js +4 -3
- package/workspace/lib/routes-c3-life.js +2 -1
- package/workspace/lib/routes-c3-reports.js +96 -13
- package/workspace/lib/routes-c3-strategy.js +4 -3
- package/workspace/lib/routes-c3-timeline.js +3 -2
- package/workspace/lib/routes-calendar-import.js +3 -2
- package/workspace/lib/routes-calendar-views.js +9 -4
- package/workspace/lib/routes-doc-health.js +2 -1
- package/workspace/lib/routes-doc-sheet.js +2 -1
- package/workspace/lib/routes-forecast.js +2 -1
- package/workspace/lib/routes-hooks.js +3 -2
- package/workspace/lib/routes-initiatives.js +2 -1
- package/workspace/lib/routes-intake.js +2 -1
- package/workspace/lib/routes-notes.js +55 -9
- package/workspace/lib/routes-outbox.js +23 -3
- package/workspace/lib/routes-pm-items.js +19 -6
- package/workspace/lib/routes-records.js +3 -2
- package/workspace/lib/routes-releases.js +2 -1
- package/workspace/lib/routes-report-facts.js +4 -3
- package/workspace/lib/routes-report-families.js +2 -1
- package/workspace/lib/routes-roadmap-ops.js +5 -4
- package/workspace/lib/routes-runs.js +6 -5
- package/workspace/lib/routes-settings.js +51 -0
- package/workspace/lib/routes-signals.js +3 -2
- package/workspace/lib/routes-stage-tools.js +220 -0
- package/workspace/lib/run-answers.js +2 -1
- package/workspace/lib/settings-store.js +226 -0
- package/workspace/lib/stage-tools.js +208 -0
- package/workspace/lib/vendor/scripts/signals/layout-paths.js +2 -1
- package/workspace/lib/vendor/scripts/workspace-lib.js +24 -0
- package/workspace/lib/work-evidence.js +73 -0
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/public/pm/briefing-deck.js +63 -17
- package/workspace/public/pm/briefing-model.js +36 -7
- package/workspace/public/pm/c3-ambient.js +100 -0
- package/workspace/public/pm/c3-brief-needs.js +26 -6
- package/workspace/public/pm/c3-brief-pulse.js +12 -4
- package/workspace/public/pm/c3-brief-today.js +56 -21
- package/workspace/public/pm/c3-brief.js +2 -1
- package/workspace/public/pm/c3-calendar.js +161 -42
- package/workspace/public/pm/c3-calpop.js +5 -5
- package/workspace/public/pm/c3-demo-boards.js +649 -0
- package/workspace/public/pm/c3-demo-brief-page.js +1 -1
- package/workspace/public/pm/c3-demo-cal4.js +3 -2
- package/workspace/public/pm/c3-demo-cal5.js +52 -0
- package/workspace/public/pm/c3-demo-data.js +17 -27
- package/workspace/public/pm/c3-demo-rep3.js +46 -5
- package/workspace/public/pm/c3-demo.js +2 -1
- package/workspace/public/pm/c3-dock.js +1 -1
- package/workspace/public/pm/c3-ib-docs.js +80 -0
- package/workspace/public/pm/c3-ib-initiatives.js +134 -0
- package/workspace/public/pm/c3-ib-items.js +201 -0
- package/workspace/public/pm/c3-ib-releases.js +51 -0
- package/workspace/public/pm/c3-ib-sprints.js +55 -0
- package/workspace/public/pm/c3-ib-triage.js +178 -0
- package/workspace/public/pm/c3-ii-tabs.js +240 -0
- package/workspace/public/pm/c3-ini-records.js +197 -0
- package/workspace/public/pm/c3-ini-timeline.js +278 -0
- package/workspace/public/pm/c3-ini-ui.js +229 -0
- package/workspace/public/pm/c3-ini-work.js +139 -0
- package/workspace/public/pm/c3-initiatives.css +218 -0
- package/workspace/public/pm/c3-initiatives.js +741 -0
- package/workspace/public/pm/c3-plan.js +2 -2
- package/workspace/public/pm/c3-reports.css +47 -47
- package/workspace/public/pm/c3-reports.js +223 -185
- package/workspace/public/pm/c3-settings.css +74 -0
- package/workspace/public/pm/c3-settings.js +703 -0
- package/workspace/public/pm/c3-shell.js +102 -36
- package/workspace/public/pm/c3-terminal-icons.js +2 -2
- package/workspace/public/pm/c3-terminal.js +2 -2
- package/workspace/public/pm/calendar-views.js +1 -1
- package/workspace/public/pm/components.css +319 -213
- package/workspace/public/pm/console3.css +99 -23
- package/workspace/public/pm/dest-notes.js +26 -4
- package/workspace/public/pm/notes-screen-capture.js +165 -0
- package/workspace/public/pm/notes-screen-view.js +98 -0
- package/workspace/public/pm/panel-trees.js +5 -2
- package/workspace/public/pm/pm-work-view.js +233 -146
- package/workspace/public/pm/shell-bell.js +3 -2
- package/workspace/public/pm/shell-recording.js +29 -8
- package/workspace/public/pm/tokens.css +2 -2
- package/workspace/public/pm/view-notes.js +46 -9
- package/workspace/public/pm/welcome.js +197 -89
- package/workspace/public/welcome.html +6 -4
- package/workspace/server.js +105 -6
- package/workspace/skills/day-check.skill +1 -1
- package/workspace/skills/jira-sync.skill +6 -4
- package/workspace/skills/morning-brief.skill +1 -1
- package/workspace/test-files.json +12 -3
- package/workspace/test-lists/c3-ib-items.json +9 -0
- package/workspace/test-lists/c3-ib-more.json +9 -0
- package/workspace/test-lists/c3-ib-triage-backlog.json +9 -0
- package/workspace/test-lists/c3-initiative-boards.json +6 -0
- package/workspace/test-lists/calendar-tidy-up.json +6 -0
- package/workspace/test-lists/console3-briefing.json +9 -0
- package/workspace/test-lists/console3-foundation.json +9 -0
- package/workspace/test-lists/console3-reports.json +3 -3
- package/workspace/test-lists/meeting-notes.json +10 -4
- package/workspace/test-lists/workspace-headless-jobs.json +5 -0
- package/workspace/test-lists/workspace-package-script-closure.json +6 -0
- package/workspace/lib/vendor/workspace/lib/actor.js +0 -91
- package/workspace/lib/vendor/workspace/lib/board-cache.js +0 -156
- package/workspace/lib/vendor/workspace/lib/console-state.js +0 -295
- package/workspace/lib/vendor/workspace/lib/factory-join.js +0 -158
- package/workspace/lib/vendor/workspace/lib/git-info.js +0 -306
- package/workspace/lib/vendor/workspace/lib/plan-items.js +0 -922
- package/workspace/lib/vendor/workspace/lib/plan-tree.js +0 -419
- package/workspace/lib/vendor/workspace/lib/review-gate.js +0 -39
- package/workspace/lib/vendor/workspace/lib/sink.js +0 -321
- package/workspace/lib/vendor/workspace/lib/state-root.js +0 -587
|
@@ -1,587 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
/* lib/state-root.js — WHERE THE PM'S RUNTIME STATE LIVES.
|
|
3
|
-
*
|
|
4
|
-
* ── The defect this deletes ────────────────────────────────────────────────────────────────────
|
|
5
|
-
*
|
|
6
|
-
* The workspace used to keep the PM's live runtime state at `workspace/state/`, INSIDE the
|
|
7
|
-
* application directory — the same directory the test suite runs from. Sixteen live tmux sessions,
|
|
8
|
-
* the workspace registry, eleven authored skills and a 15KB schedules.json sat one `rm -rf` away
|
|
9
|
-
* from every suite, every spawned child, and every stray shell command in the tree.
|
|
10
|
-
*
|
|
11
|
-
* Three rounds of guarding destructive verbs failed. Round 3 patched rmSync/rmdirSync/unlinkSync/
|
|
12
|
-
* renameSync; a verifier then destroyed a planted replica nine other ways — writeFileSync,
|
|
13
|
-
* truncateSync, openSync(…,'w'), copyFileSync, cpSync, createWriteStream, execSync('rm -rf'),
|
|
14
|
-
* `find -delete`, and renameSync's unchecked DESTINATION — with the isolation suite green each
|
|
15
|
-
* time. The enumeration of destructive verbs is unbounded, and a shell subprocess defeats any
|
|
16
|
-
* in-process fs patch by construction.
|
|
17
|
-
*
|
|
18
|
-
* So the fix is not another guard. The PM's state MOVES OUT of the application directory. A
|
|
19
|
-
* checkout's `workspace/state/` then holds nothing of the PM's; a test may delete all of it and
|
|
20
|
-
* lose nothing. Unreachable beats guarded: there is no verb that reaches a file that is not there.
|
|
21
|
-
*
|
|
22
|
-
* ── What this is, and what it is NOT ───────────────────────────────────────────────────────────
|
|
23
|
-
*
|
|
24
|
-
* THIS IS LOCAL-ONLY, GITIGNORED RUNTIME STATE: sessions/, workspaces.json, layouts/, scratchpad/,
|
|
25
|
-
* schedules.json, deliverables/, the PM's authored skills/, prefs.json, the per-port pid/log files
|
|
26
|
-
* and tmux.conf. Machine-written, churny, per-machine, never committed.
|
|
27
|
-
*
|
|
28
|
-
* THIS IS NOT D64'S FILE-MODE SYSTEM OF RECORD. `pmos/state/*.jsonl` + `planning/` are canonical
|
|
29
|
-
* records that live IN the repository that earned them, and `server.js#stateDirFor()` resolves
|
|
30
|
-
* those per-workspace, from the workspace's own root. Nothing here touches that path — see the
|
|
31
|
-
* SELF_FILE_MODE_STATE constant in server.js, which deliberately still points inside the checkout.
|
|
32
|
-
*
|
|
33
|
-
* ── Where it goes ──────────────────────────────────────────────────────────────────────────────
|
|
34
|
-
*
|
|
35
|
-
* PMOS_STATE_DIR an explicit override; wins over everything. Every test run sets it.
|
|
36
|
-
* darwin ~/Library/Application Support/pmos
|
|
37
|
-
* elsewhere $XDG_STATE_HOME/pmos, else ~/.local/state/pmos
|
|
38
|
-
*
|
|
39
|
-
* ── The migration ──────────────────────────────────────────────────────────────────────────────
|
|
40
|
-
*
|
|
41
|
-
* On first run the existing in-repo state is MOVED, not copied and not merged. Losing the PM's
|
|
42
|
-
* sixteen sessions would be a worse outcome than the bug this fixes, so the rules are strict:
|
|
43
|
-
*
|
|
44
|
-
* - it runs ONLY for the platform default location. An explicit PMOS_STATE_DIR — which is what
|
|
45
|
-
* a test sets — never migrates anything, because that would be a test moving the PM's live
|
|
46
|
-
* state into a scratch directory: the very accident this file exists to make impossible.
|
|
47
|
-
* - two populated directories are NEVER merged. If the destination already holds state, the
|
|
48
|
-
* legacy directory is left exactly as it is and the caller is told, so a human decides.
|
|
49
|
-
* - it is idempotent. After a move the legacy directory is recreated holding only a note, which
|
|
50
|
-
* does not count as populated, so a second run is a no-op. Proven by a test that runs it twice.
|
|
51
|
-
*/
|
|
52
|
-
const fs = require('fs');
|
|
53
|
-
const os = require('os');
|
|
54
|
-
const path = require('path');
|
|
55
|
-
|
|
56
|
-
const APP = 'pmos';
|
|
57
|
-
const NOTE_NAME = 'MOVED.txt';
|
|
58
|
-
|
|
59
|
-
/* ── THE ONLY TWO OUTCOMES THAT MEAN "SETTLED" ─────────────────────────────────────────────────
|
|
60
|
-
*
|
|
61
|
-
* This set is an ALLOW-list, and that direction is the whole point. `stateHealth` used to open
|
|
62
|
-
* `if (m.action !== 'conflict') return { ok: true }` — a DENY-list of one, which quietly declared
|
|
63
|
-
* every other outcome healthy. `migrateLegacy` already had a second failure outcome ('failed', the
|
|
64
|
-
* filesystem refusing the move), so a run whose move was refused reported `{ok:true}` on every
|
|
65
|
-
* surface, fell back nowhere, and showed the PM zero skills and zero schedules with nothing said —
|
|
66
|
-
* the same silent no-show as the conflict, with even the stderr line removed.
|
|
67
|
-
*
|
|
68
|
-
* So the question is inverted: an outcome is healthy only if it is NAMED here. A third failure mode
|
|
69
|
-
* added tomorrow is non-ok by default and says something, rather than being silently healthy until
|
|
70
|
-
* somebody notices the empty list. Adding an outcome to this set is the deliberate act; forgetting
|
|
71
|
-
* to is the safe one. */
|
|
72
|
-
const SETTLED_ACTIONS = new Set([
|
|
73
|
-
'none', // nothing to move: no legacy path, an explicit PMOS_STATE_DIR, or a legacy dir holding no state
|
|
74
|
-
'moved', // the contents are now at the per-user root and a note was left behind
|
|
75
|
-
]);
|
|
76
|
-
|
|
77
|
-
/* ── AND THE SAME DEFECT ONE FUNCTION UP: AN ERROR CONVERTED INTO A TRUSTED VALUE ──────────────
|
|
78
|
-
*
|
|
79
|
-
* Inverting the check at `stateHealth` classifies the OUTCOME correctly. It cannot help when the
|
|
80
|
-
* outcome itself was manufactured out of a swallowed error, which is what the first version of
|
|
81
|
-
* `isPopulated` did:
|
|
82
|
-
*
|
|
83
|
-
* let names = [];
|
|
84
|
-
* try { names = fs.readdirSync(dir); } catch { return false; } // ← the swallow
|
|
85
|
-
*
|
|
86
|
-
* A directory that could not be READ — a permission, an I/O error, a dangling network mount, a file
|
|
87
|
-
* sitting where the directory should be — came back as `false`, indistinguishable from a directory
|
|
88
|
-
* that is genuinely empty. `migrateLegacy` then reached a SETTLED outcome ('none'), `stateHealth`
|
|
89
|
-
* said `{ok:true}`, and every measured symptom of the original bug returned verbatim: zero skills,
|
|
90
|
-
* zero schedules, forty-five schedules that stop firing, and nothing anywhere saying why. An
|
|
91
|
-
* allow-list downstream is powerless against it, because by then the error is a legitimate answer.
|
|
92
|
-
*
|
|
93
|
-
* So the rule this file now holds: AN UNREADABLE DIRECTORY IS NOT AN EMPTY ONE. Reading a directory
|
|
94
|
-
* has THREE answers, not two, and "could not tell" travels all the way to the PM with its errno and
|
|
95
|
-
* its path — the same treatment 'failed' gets. There is no boolean in this module any more that a
|
|
96
|
-
* caller could mistake for an answer; `readState` is the only reader and it is total. */
|
|
97
|
-
|
|
98
|
-
/** Why a directory could not be LISTED — a different act from refusing a move, so a different table:
|
|
99
|
-
* "cannot write the destination" is the wrong sentence for a directory nobody can open. */
|
|
100
|
-
const WHY_UNREADABLE = {
|
|
101
|
-
EACCES: 'permission denied — this user cannot list that directory (check its mode and its parents)',
|
|
102
|
-
EPERM: 'the operation was not permitted — check ownership, a locked/immutable flag, or macOS privacy protection on that directory',
|
|
103
|
-
ENOTDIR: 'a component of the path is a FILE where a directory was expected',
|
|
104
|
-
ELOOP: 'a symlink loop in the path',
|
|
105
|
-
ENAMETOOLONG: 'the path is too long for this filesystem',
|
|
106
|
-
EIO: 'a low-level I/O error — the volume may be failing or a mount may have gone away',
|
|
107
|
-
ESTALE: 'a STALE file handle — the network mount this directory is on has been replaced or unmounted',
|
|
108
|
-
ENODEV: 'no such device — the volume this directory is on is not attached',
|
|
109
|
-
ENXIO: 'no such device or address — the volume this directory is on is not responding',
|
|
110
|
-
ETIMEDOUT: 'the filesystem TIMED OUT — a network mount is unreachable',
|
|
111
|
-
EHOSTDOWN: 'the host serving this network mount is down',
|
|
112
|
-
EHOSTUNREACH: 'the host serving this network mount is unreachable',
|
|
113
|
-
ECONNABORTED: 'the connection to this network mount was aborted',
|
|
114
|
-
EMFILE: 'this process has too many open files',
|
|
115
|
-
ENFILE: 'the system has too many open files',
|
|
116
|
-
EBUSY: 'the directory is busy — another process has it locked',
|
|
117
|
-
};
|
|
118
|
-
/** A sentence for an errno, never an empty string. Same contract as `whyRefused`. */
|
|
119
|
-
function whyUnreadable(code) {
|
|
120
|
-
if (code && Object.prototype.hasOwnProperty.call(WHY_UNREADABLE, code)) return WHY_UNREADABLE[code];
|
|
121
|
-
return code
|
|
122
|
-
? `the filesystem returned ${code} when this directory was listed, which this build has no plain-language translation for`
|
|
123
|
-
: 'the filesystem refused to list this directory without an error code';
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/* Why the filesystem refused a move, in words the PM can act on. Unlike a conflict — which the PM
|
|
127
|
-
* can resolve just by being told the two directories — a refusal cannot be resolved without knowing
|
|
128
|
-
* the CAUSE: a permission is fixed differently from a full disk, which is fixed differently from a
|
|
129
|
-
* read-only volume. An errno alone ("EACCES") is a code the PM has to go look up, so the code is
|
|
130
|
-
* kept AND translated. An unrecognised code still gets a sentence rather than silence. */
|
|
131
|
-
const WHY_REFUSED = {
|
|
132
|
-
EACCES: 'permission denied — this user cannot write the destination or one of its parent directories',
|
|
133
|
-
EPERM: 'the operation was not permitted — check ownership, a locked/immutable flag, or macOS privacy protection on the destination',
|
|
134
|
-
EROFS: 'the destination is on a READ-ONLY filesystem',
|
|
135
|
-
ENOSPC: 'the disk is FULL — no space left on the destination volume',
|
|
136
|
-
EDQUOT: 'this user\'s disk QUOTA on the destination volume is exhausted',
|
|
137
|
-
EXDEV: 'the two directories are on DIFFERENT filesystems and the cross-device copy also failed',
|
|
138
|
-
ENOENT: 'a path was not there — the destination directory could not be created, or a source entry vanished mid-move',
|
|
139
|
-
ENOTDIR: 'a component of one of the paths is a FILE where a directory was expected',
|
|
140
|
-
EISDIR: 'a directory was found where a file was expected',
|
|
141
|
-
ENOTEMPTY: 'a destination entry already exists and is not empty',
|
|
142
|
-
EEXIST: 'a destination entry already exists',
|
|
143
|
-
EBUSY: 'a file is IN USE by another process and could not be moved',
|
|
144
|
-
ELOOP: 'a symlink loop in one of the paths',
|
|
145
|
-
ENAMETOOLONG: 'a path is too long for this filesystem',
|
|
146
|
-
EMFILE: 'this process has too many open files',
|
|
147
|
-
ENFILE: 'the system has too many open files',
|
|
148
|
-
EIO: 'a low-level I/O error on the disk — the volume may be failing or disconnected',
|
|
149
|
-
};
|
|
150
|
-
/** A sentence for an errno, never an empty string: an unknown code still has to say something. */
|
|
151
|
-
function whyRefused(code) {
|
|
152
|
-
if (code && Object.prototype.hasOwnProperty.call(WHY_REFUSED, code)) return WHY_REFUSED[code];
|
|
153
|
-
return code
|
|
154
|
-
? `the filesystem returned ${code}, which this build has no plain-language translation for`
|
|
155
|
-
: 'the filesystem refused the move without an error code';
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
/**
|
|
159
|
-
* Did the migration leave the state root SETTLED? `true` only for an outcome named in
|
|
160
|
-
* SETTLED_ACTIONS. A null/absent migration record means no migration was attempted (a caller that
|
|
161
|
-
* passed none), which is settled; a record that HAS an action this build does not recognise is not.
|
|
162
|
-
*/
|
|
163
|
-
function isSettled(migration) {
|
|
164
|
-
if (migration == null) return true;
|
|
165
|
-
const a = migration.action;
|
|
166
|
-
return typeof a === 'string' && SETTLED_ACTIONS.has(a);
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
// Entries that do not make a directory "populated": the checked-in .gitignore that documents the
|
|
170
|
-
// layout, the note this migration leaves behind, and Finder's litter. Anything else is the PM's.
|
|
171
|
-
const NOT_STATE = new Set(['.gitignore', NOTE_NAME, '.DS_Store']);
|
|
172
|
-
// …and the same set is what the migration LEAVES BEHIND. `.gitignore` is a TRACKED file in this
|
|
173
|
-
// repository — it documents the directory's layout — so moving it out deletes a tracked file from
|
|
174
|
-
// the checkout and shows up as a spurious deletion in `git status`. It is not state; it does not
|
|
175
|
-
// travel. (Found the hard way: the first migration in this worktree carried it off.)
|
|
176
|
-
const NEVER_MOVE = NOT_STATE;
|
|
177
|
-
|
|
178
|
-
/** An explicit override, resolved to an absolute path — or null when unset/blank. */
|
|
179
|
-
function overrideDir(env = process.env) {
|
|
180
|
-
const v = env && env.PMOS_STATE_DIR;
|
|
181
|
-
const s = v == null ? '' : String(v).trim();
|
|
182
|
-
return s ? path.resolve(s) : null;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
/**
|
|
186
|
-
* The per-user runtime state directory for this machine. Pure: every input is injected, so the
|
|
187
|
-
* platform branches are assertable without pretending to be another OS.
|
|
188
|
-
*/
|
|
189
|
-
function defaultRoot({ env = process.env, platform = process.platform, home = os.homedir() } = {}) {
|
|
190
|
-
const forced = overrideDir(env);
|
|
191
|
-
if (forced) return forced;
|
|
192
|
-
if (platform === 'darwin') return path.join(home, 'Library', 'Application Support', APP);
|
|
193
|
-
const xdg = env && env.XDG_STATE_HOME;
|
|
194
|
-
if (xdg && path.isAbsolute(String(xdg))) return path.join(String(xdg), APP);
|
|
195
|
-
return path.join(home, '.local', 'state', APP);
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
/**
|
|
199
|
-
* THE ONLY DIRECTORY READ IN THIS MODULE, and it is total: it answers with a fact about what it
|
|
200
|
-
* managed to find out, never with a boolean that hides which of two very different things happened.
|
|
201
|
-
*
|
|
202
|
-
* { known: true, populated: true, names: [...] } it holds something of the PM's
|
|
203
|
-
* { known: true, populated: false, names: [...] } it is there and holds no state…
|
|
204
|
-
* { known: true, populated: false, absent: true } …or it is not there at all, which is the same
|
|
205
|
-
* fact for our purposes and the ordinary case
|
|
206
|
-
* { known: false, code, syscall, at, message } WE COULD NOT TELL — and nobody downstream may
|
|
207
|
-
* pretend that is the same as "empty"
|
|
208
|
-
*
|
|
209
|
-
* ENOENT is the one error that IS an answer: a directory that does not exist holds nothing, and that
|
|
210
|
-
* is every first run on every machine. Everything else — EACCES, EIO, ENOTDIR, ESTALE, a timed-out
|
|
211
|
-
* network mount — is a question this process cannot answer, and it is carried, not swallowed.
|
|
212
|
-
*
|
|
213
|
-
* There is deliberately NO `isPopulated(dir) -> boolean` any more. It existed, it swallowed, and the
|
|
214
|
-
* swallow put back every symptom of the bug this whole module was written to end. If you are about
|
|
215
|
-
* to re-add one for convenience, that is the regression.
|
|
216
|
-
*/
|
|
217
|
-
function readState(dir) {
|
|
218
|
-
if (!dir) return { known: true, populated: false, absent: true, names: [] };
|
|
219
|
-
let names;
|
|
220
|
-
try { names = fs.readdirSync(dir); }
|
|
221
|
-
catch (e) {
|
|
222
|
-
if (e && e.code === 'ENOENT') return { known: true, populated: false, absent: true, names: [] };
|
|
223
|
-
return {
|
|
224
|
-
known: false, populated: null, names: null,
|
|
225
|
-
code: (e && e.code) || null,
|
|
226
|
-
syscall: (e && e.syscall) || 'readdir',
|
|
227
|
-
at: (e && e.path) || dir,
|
|
228
|
-
message: (e && e.message) || String(e),
|
|
229
|
-
};
|
|
230
|
-
}
|
|
231
|
-
return { known: true, populated: names.some((n) => !NOT_STATE.has(n)), names };
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
/**
|
|
235
|
-
* Does a path exist? THREE answers, for the same reason `readState` has three: `fs.existsSync`
|
|
236
|
-
* returns `false` both for "not there" and for "this process could not find out", and a caller that
|
|
237
|
-
* treats the second as the first is the swallow again, wearing a different function's name.
|
|
238
|
-
* 'yes' | 'no' | 'unknown'
|
|
239
|
-
*/
|
|
240
|
-
function presence(p) {
|
|
241
|
-
if (!p) return 'no';
|
|
242
|
-
try { fs.statSync(p); return 'yes'; }
|
|
243
|
-
catch (e) { return e && e.code === 'ENOENT' ? 'no' : 'unknown'; }
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
function noteText(target, at) {
|
|
247
|
-
return [
|
|
248
|
-
'This directory is no longer where the PMOS workspace keeps runtime state.',
|
|
249
|
-
'',
|
|
250
|
-
`Everything that was here was MOVED on ${at} to:`,
|
|
251
|
-
` ${target}`,
|
|
252
|
-
'',
|
|
253
|
-
'It moved because this directory sits inside the application directory the test suite runs',
|
|
254
|
-
'from, which put the PM\'s live sessions, workspace registry and authored skills within reach',
|
|
255
|
-
'of every test. Set PMOS_STATE_DIR to override the location.',
|
|
256
|
-
'',
|
|
257
|
-
].join('\n');
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
/**
|
|
261
|
-
* Move an in-repo `workspace/state/` to the per-user location, once.
|
|
262
|
-
*
|
|
263
|
-
* Returns `{ action, from, to, reason }` and never throws for an expected condition:
|
|
264
|
-
* 'none' nothing to move (legacy absent or holding no state)
|
|
265
|
-
* 'conflict' BOTH directories hold state — nothing touched, a human decides
|
|
266
|
-
* 'moved' the legacy directory's contents are now at `to`
|
|
267
|
-
* 'failed' the move was attempted and the filesystem refused; nothing was deleted
|
|
268
|
-
* 'unreadable' one of the two directories could not be LISTED, so whether it holds state is
|
|
269
|
-
* unknown. Nothing was attempted. This is the outcome that used to be reported as
|
|
270
|
-
* 'none' — a settled, healthy, entirely fictitious "there was nothing to move".
|
|
271
|
-
*/
|
|
272
|
-
function migrateLegacy({ legacy, target, now = new Date() } = {}) {
|
|
273
|
-
const from = legacy ? path.resolve(legacy) : null;
|
|
274
|
-
const to = target ? path.resolve(target) : null;
|
|
275
|
-
if (!from || !to || from === to) return { action: 'none', from, to, reason: 'same-or-missing-path' };
|
|
276
|
-
const unreadable = (r, which) => ({
|
|
277
|
-
action: 'unreadable', from, to,
|
|
278
|
-
reason: r.code || 'unreadable',
|
|
279
|
-
which, // 'legacy' | 'target' — which of the two we are blind to
|
|
280
|
-
code: r.code || null,
|
|
281
|
-
syscall: r.syscall || 'readdir',
|
|
282
|
-
at: r.at || null,
|
|
283
|
-
message: r.message || null,
|
|
284
|
-
});
|
|
285
|
-
const src = readState(from);
|
|
286
|
-
if (!src.known) return unreadable(src, 'legacy');
|
|
287
|
-
if (!src.populated) return { action: 'none', from, to, reason: 'legacy-holds-no-state' };
|
|
288
|
-
const dst = readState(to);
|
|
289
|
-
if (!dst.known) return unreadable(dst, 'target');
|
|
290
|
-
if (dst.populated) return { action: 'conflict', from, to, reason: 'both-populated' };
|
|
291
|
-
|
|
292
|
-
try {
|
|
293
|
-
fs.mkdirSync(path.dirname(to), { recursive: true });
|
|
294
|
-
// Entry-by-entry, so an EMPTY destination that already exists (a bare mkdir, a .gitignore) is
|
|
295
|
-
// filled rather than refused, and so a cross-device legacy directory still moves.
|
|
296
|
-
fs.mkdirSync(to, { recursive: true });
|
|
297
|
-
// `src.names` — the listing `readState` already took, and the only one it will ever have taken
|
|
298
|
-
// successfully, so a re-read that failed here could not be mistaken for an empty directory.
|
|
299
|
-
for (const name of src.names) {
|
|
300
|
-
if (NEVER_MOVE.has(name)) continue;
|
|
301
|
-
const src = path.join(from, name);
|
|
302
|
-
const dst = path.join(to, name);
|
|
303
|
-
if (fs.existsSync(dst)) continue; // the .gitignore case; never overwrite
|
|
304
|
-
try { fs.renameSync(src, dst); }
|
|
305
|
-
catch (e) {
|
|
306
|
-
if (e && e.code === 'EXDEV') { fs.cpSync(src, dst, { recursive: true }); fs.rmSync(src, { recursive: true, force: true }); }
|
|
307
|
-
else throw e;
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
// Leave the legacy directory in place holding only the note: a checkout keeps its (now scratch)
|
|
311
|
-
// state/ directory, and the note explains the move to whoever looks there next. The note is in
|
|
312
|
-
// NOT_STATE, so this exact directory reads as unpopulated on the next run — the idempotence.
|
|
313
|
-
//
|
|
314
|
-
// AUDITED SWALLOW, KEPT — but it no longer converts an error into a value anyone trusts. The
|
|
315
|
-
// note is advisory: after the loop the legacy directory holds only NOT_STATE entries, so it
|
|
316
|
-
// reads as unpopulated on the next run whether or not the note landed, and 'moved' is the
|
|
317
|
-
// honest outcome either way. What was wrong was throwing the reason away, so the fact is
|
|
318
|
-
// CARRIED on the result instead of vanishing — a human who looks at a bare `state/` with no
|
|
319
|
-
// explanation in it can now find out why there is none.
|
|
320
|
-
let noteWritten = true, noteError = null;
|
|
321
|
-
try { fs.writeFileSync(path.join(from, NOTE_NAME), noteText(to, now.toISOString())); }
|
|
322
|
-
catch (e) { noteWritten = false; noteError = (e && e.code) || (e && e.message) || String(e); }
|
|
323
|
-
return { action: 'moved', from, to, reason: 'legacy-relocated', noteWritten, noteError };
|
|
324
|
-
} catch (e) {
|
|
325
|
-
// CARRY THE CAUSE, not just the fact. A refusal the PM cannot diagnose is a refusal they cannot
|
|
326
|
-
// fix, and this is the one non-ok outcome where being told the two directories is not enough.
|
|
327
|
-
return {
|
|
328
|
-
action: 'failed', from, to,
|
|
329
|
-
reason: (e && e.code) || 'move-failed',
|
|
330
|
-
code: (e && e.code) || null,
|
|
331
|
-
syscall: (e && e.syscall) || null,
|
|
332
|
-
at: (e && e.path) || null, // the exact path the filesystem refused, when it named one
|
|
333
|
-
message: (e && e.message) || String(e),
|
|
334
|
-
};
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
/**
|
|
339
|
-
* THE call site: resolve the runtime state root, migrating an in-repo legacy directory the first
|
|
340
|
-
* time, and create it. `legacy` is `workspace/state/` — passed in rather than derived here so this
|
|
341
|
-
* module never has an opinion about where the application happens to be installed.
|
|
342
|
-
*
|
|
343
|
-
* The migration is skipped whenever the location was chosen explicitly (PMOS_STATE_DIR), because a
|
|
344
|
-
* chosen location is a scratch location and moving the PM's state into it is the accident, not the
|
|
345
|
-
* fix.
|
|
346
|
-
*/
|
|
347
|
-
function resolveStateRoot({ env = process.env, platform = process.platform, home = os.homedir(), legacy = null } = {}) {
|
|
348
|
-
const root = defaultRoot({ env, platform, home });
|
|
349
|
-
const chosen = !!overrideDir(env);
|
|
350
|
-
const migration = (!chosen && legacy)
|
|
351
|
-
? migrateLegacy({ legacy, target: root })
|
|
352
|
-
: { action: 'none', from: legacy, to: root, reason: chosen ? 'explicit-state-dir' : 'no-legacy-path' };
|
|
353
|
-
/* THE THIRD SWALLOW IN THIS FILE, and the one with the best-sounding excuse. It used to read
|
|
354
|
-
*
|
|
355
|
-
* try { fs.mkdirSync(root, { recursive: true }); } catch { } // the caller's first write
|
|
356
|
-
* // will report it
|
|
357
|
-
*
|
|
358
|
-
* — a promise about a caller. The callers are the stores, and what a store does when its
|
|
359
|
-
* directory is not there is return an empty list: no skills, no schedules, no registry, and a
|
|
360
|
-
* migration record still saying 'none' so `stateHealth` says {ok:true}. That is the identical
|
|
361
|
-
* silent no-show, entered through the one door that has nothing to do with the migration at all.
|
|
362
|
-
* The error is kept and reported. */
|
|
363
|
-
let rootError = null;
|
|
364
|
-
try { fs.mkdirSync(root, { recursive: true }); }
|
|
365
|
-
catch (e) {
|
|
366
|
-
rootError = {
|
|
367
|
-
code: (e && e.code) || null,
|
|
368
|
-
syscall: (e && e.syscall) || 'mkdir',
|
|
369
|
-
at: (e && e.path) || root,
|
|
370
|
-
message: (e && e.message) || String(e),
|
|
371
|
-
};
|
|
372
|
-
}
|
|
373
|
-
return { root, migration, chosen, rootError };
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
/* ── WHAT AN UNSETTLED STATE ROOT MUST NOT MEAN: "YOU HAVE NOTHING" ────────────────────────────
|
|
377
|
-
*
|
|
378
|
-
* This section was written for the CONFLICT outcome and then, for one round, applied only to it —
|
|
379
|
-
* which is how the identical defect survived in the 'failed' outcome with even the stderr line
|
|
380
|
-
* removed. Read every "conflict" below as "any outcome not in SETTLED_ACTIONS": both functions now
|
|
381
|
-
* key off `isSettled`, so a refused move degrades exactly like a conflict, and so does whatever
|
|
382
|
-
* outcome gets added next.
|
|
383
|
-
*
|
|
384
|
-
* `migrateLegacy` refuses to merge two populated directories, and that refusal is right — only a
|
|
385
|
-
* human can decide which sixteen sessions are the real sixteen. What was wrong was what happened
|
|
386
|
-
* NEXT. Every store resolved unconditionally against the per-user root with no fallback, so on a
|
|
387
|
-
* machine where the per-user root existed and held (say) sessions/ and workspaces.json but NO
|
|
388
|
-
* skills/ and NO schedules.json — which is exactly the state an earlier agent round left on the
|
|
389
|
-
* PM's machine — the PM opened the workspace and saw ZERO skills and ZERO schedules. Their eleven
|
|
390
|
-
* skills and forty-five schedules were on disk the whole time, one directory away.
|
|
391
|
-
*
|
|
392
|
-
* And a schedule that never loads never fires. That is the same silent no-show this whole
|
|
393
|
-
* initiative exists to end, arriving through a different door: no error on any surface, a green
|
|
394
|
-
* empty list, and forty-five scheduled runs that simply stop happening. The server did print a
|
|
395
|
-
* conflict to stderr — but `pmos` detaches into a log nobody reads, so in practice it printed to
|
|
396
|
-
* nobody.
|
|
397
|
-
*
|
|
398
|
-
* The two functions below are the answer, and the shape of the answer is a product decision:
|
|
399
|
-
*
|
|
400
|
-
* 1. resolveStore — a READ-AND-WRITE fallback, per store, ONLY where the per-user root has no
|
|
401
|
-
* copy at all. If the per-user root has its own `skills/`, there are two copies, the fallback
|
|
402
|
-
* does not happen and a human decides: a fallback that let the PM edit one copy while the
|
|
403
|
-
* other was authoritative would be a worse trap than the empty list. Where the per-user root
|
|
404
|
-
* has NOTHING, there is exactly one copy of that store on the machine, and reading and writing
|
|
405
|
-
* the single copy you found cannot fork anything. So the PM's skills load, their schedules
|
|
406
|
-
* load AND FIRE, and a fire can still stamp lastRun on the same file it read.
|
|
407
|
-
*
|
|
408
|
-
* Under a 'failed' move the same argument holds with one extra wrinkle: the move may have got
|
|
409
|
-
* part-way. The rule handles it unchanged — a store that DID land in the per-user root is read
|
|
410
|
-
* from there, a store that did not is read from where it stayed, and neither is duplicated.
|
|
411
|
-
*
|
|
412
|
-
* 2. stateHealth — the fallback is never SILENT. It is a discriminated {ok:false} carried on
|
|
413
|
-
* every surface that lists these stores (see server.js: /api/skills, /api/schedules,
|
|
414
|
-
* /api/workspaces, /api/console) and written into the first line of every new terminal pane.
|
|
415
|
-
* It names BOTH directories and says which stores are being read from the old one, so the PM
|
|
416
|
-
* is told what to resolve rather than shown a calm list that happens to be right today.
|
|
417
|
-
*
|
|
418
|
-
* A TEST RUN NEVER REACHES ANY OF THIS. A test names PMOS_STATE_DIR, which makes the location
|
|
419
|
-
* `chosen`, which means migrateLegacy is never called and `action` is never 'conflict'. So no
|
|
420
|
-
* suite can fall back into the PM's `workspace/state/` — the hermeticity this file bought stands
|
|
421
|
-
* exactly as it did.
|
|
422
|
-
*/
|
|
423
|
-
|
|
424
|
-
/**
|
|
425
|
-
* Where ONE named store lives, given a migration result.
|
|
426
|
-
* Returns `{ path, fellBack }` — `fellBack` true only when this store is being read from the
|
|
427
|
-
* legacy root because the per-user root holds no copy of it.
|
|
428
|
-
*/
|
|
429
|
-
function resolveStore({ root, legacy, name, migration } = {}) {
|
|
430
|
-
const at = root && name ? path.join(root, name) : null;
|
|
431
|
-
/* THE FOURTH SWALLOW. This used to be two `try { fs.existsSync(x) } catch { }` pairs, and
|
|
432
|
-
* `existsSync` is itself a swallow with an API: it answers `false` for a path this process could
|
|
433
|
-
* not stat, so "I could not look" arrived as "it is not there". Read one way that silently
|
|
434
|
-
* DUPLICATES a store (the per-user copy is unreadable, so we fall back and the PM edits the copy
|
|
435
|
-
* nobody reads); read the other it silently HIDES one. `presence()` gives the third answer, and
|
|
436
|
-
* the rule for it is the conservative one in both directions: do not fall back on a guess. The
|
|
437
|
-
* root is already unsettled whenever this code runs, so the PM is already being told something is
|
|
438
|
-
* wrong — this only refuses to make it worse behind their back. */
|
|
439
|
-
// ANY unsettled outcome falls back, not just 'conflict'. When the filesystem REFUSES the move the
|
|
440
|
-
// PM's stores are still sitting in the legacy directory exactly as they were, and resolving them
|
|
441
|
-
// to a per-user root that does not hold them is the identical empty-list failure — it just has no
|
|
442
|
-
// stderr line to go with it. A partially-completed 'failed' move is handled by the same rule as
|
|
443
|
-
// everything else below: a copy in the per-user root wins, so an entry that DID move is read from
|
|
444
|
-
// where it landed and an entry that did not is read from where it stayed.
|
|
445
|
-
if (!at || !legacy || isSettled(migration)) return { path: at, fellBack: false, unreadable: false };
|
|
446
|
-
// A copy in the per-user root wins and no fallback happens: two copies is the ambiguous case, and
|
|
447
|
-
// silently preferring either one is how a PM ends up editing the copy nobody reads. An UNKNOWN
|
|
448
|
-
// per-user copy is treated as present for exactly that reason — a guess that forks the PM's state
|
|
449
|
-
// is the worse of the two mistakes.
|
|
450
|
-
const here = presence(at);
|
|
451
|
-
if (here === 'yes') return { path: at, fellBack: false, unreadable: false };
|
|
452
|
-
if (here === 'unknown') return { path: at, fellBack: false, unreadable: true };
|
|
453
|
-
const old = path.join(legacy, name);
|
|
454
|
-
const there = presence(old);
|
|
455
|
-
if (there === 'no') return { path: at, fellBack: false, unreadable: false };
|
|
456
|
-
if (there === 'unknown') return { path: at, fellBack: false, unreadable: true };
|
|
457
|
-
return { path: old, fellBack: true, unreadable: false };
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
/**
|
|
461
|
-
* The one fact every surface repeats. `{ok:true}` ONLY for an outcome named in SETTLED_ACTIONS;
|
|
462
|
-
* every other outcome is a discriminated failure naming BOTH directories and the stores still being
|
|
463
|
-
* read from the old one. `fellBack` is a list of store names (e.g. ['skills', 'schedules.json']).
|
|
464
|
-
*
|
|
465
|
-
* The `kind` is the discriminant a surface may branch on:
|
|
466
|
-
* 'unmigrated' two populated directories — a human picks which is real
|
|
467
|
-
* 'failed' the filesystem refused the move; `code`/`why`/`at` say WHY, because unlike a
|
|
468
|
-
* conflict this one cannot be resolved from the two directory names alone
|
|
469
|
-
* 'unreadable' one of the two directories could not be LISTED, so whether it holds state is
|
|
470
|
-
* unknown. The kind that used to be reported as a healthy 'none'.
|
|
471
|
-
* 'unusable-root' the state root could not be created at all — nowhere to read, nowhere to write
|
|
472
|
-
* 'unknown' an outcome this build does not recognise. It exists so that a future third
|
|
473
|
-
* failure mode is LOUD by default. `{ok:true}` is never the fallthrough.
|
|
474
|
-
*
|
|
475
|
-
* Every kind carries the same keys, so a consumer that only reads `ok`/`error` — which is what
|
|
476
|
-
* panel 1, the pane banner and all four API payloads do — needs no change when a kind is added.
|
|
477
|
-
*/
|
|
478
|
-
function stateHealth({ migration, fellBack = [], rootError = null } = {}) {
|
|
479
|
-
// FIRST, because it outranks everything else: if the state root itself could not be created there
|
|
480
|
-
// is nowhere to read from and nowhere to write to, and every store will render an empty list
|
|
481
|
-
// whatever the migration says. This used to be a swallowed mkdir in resolveStateRoot.
|
|
482
|
-
if (rootError) {
|
|
483
|
-
const code = rootError.code || null;
|
|
484
|
-
const why = whyRefused(code);
|
|
485
|
-
return {
|
|
486
|
-
ok: false, kind: 'unusable-root',
|
|
487
|
-
reason: code || 'state-root-not-created',
|
|
488
|
-
legacy: (migration && migration.from) || null,
|
|
489
|
-
target: (migration && migration.to) || rootError.at || null,
|
|
490
|
-
reading_from_legacy: (fellBack || []).filter(Boolean),
|
|
491
|
-
code, why, at: rootError.at || null, detail: rootError.message || null,
|
|
492
|
-
error: 'the workspace runtime state directory COULD NOT BE CREATED, so nothing can be read or '
|
|
493
|
-
+ `written and every list will render empty: ${why}`
|
|
494
|
-
+ (code ? ` (${code}${rootError.syscall ? ' on ' + rootError.syscall : ''})` : '')
|
|
495
|
-
+ (rootError.at ? ` at ${rootError.at}` : '') + '. '
|
|
496
|
-
+ 'Fix the cause above, or set PMOS_STATE_DIR to a directory this user can create and write.',
|
|
497
|
-
};
|
|
498
|
-
}
|
|
499
|
-
if (migration == null) return { ok: true }; // no migration was attempted; there is nothing to report
|
|
500
|
-
const m = migration;
|
|
501
|
-
if (isSettled(m)) return { ok: true };
|
|
502
|
-
|
|
503
|
-
const action = typeof m.action === 'string' && m.action ? m.action : '(no action)';
|
|
504
|
-
const from = m.from || '(unknown)';
|
|
505
|
-
const to = m.to || '(unknown)';
|
|
506
|
-
const names = (fellBack || []).filter(Boolean);
|
|
507
|
-
// The same sentence in every kind: what is still working, and therefore what is NOT.
|
|
508
|
-
const stillRunning = names.length
|
|
509
|
-
? `Until then ${names.join(' and ')} are being read from the OLD directory, so nothing `
|
|
510
|
-
+ 'silently stops running. '
|
|
511
|
-
: 'Anything the per-user directory does not hold is unavailable until this is resolved. ';
|
|
512
|
-
const where = `In the checkout: ${from}. Per-user: ${to}. `;
|
|
513
|
-
const base = {
|
|
514
|
-
ok: false,
|
|
515
|
-
reason: m.reason || action,
|
|
516
|
-
legacy: m.from || null,
|
|
517
|
-
target: m.to || null,
|
|
518
|
-
reading_from_legacy: names.slice(),
|
|
519
|
-
};
|
|
520
|
-
|
|
521
|
-
if (m.action === 'conflict') {
|
|
522
|
-
return Object.assign(base, {
|
|
523
|
-
kind: 'unmigrated',
|
|
524
|
-
error: 'the workspace state is UNMIGRATED — two populated state directories, so nothing was '
|
|
525
|
-
+ 'moved or merged and a human has to decide which is real. '
|
|
526
|
-
+ stillRunning + where
|
|
527
|
-
+ 'Keep one, remove the other, or set PMOS_STATE_DIR to the one you want.',
|
|
528
|
-
});
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
if (m.action === 'failed') {
|
|
532
|
-
const code = m.code || (m.reason && m.reason !== 'move-failed' ? m.reason : null);
|
|
533
|
-
const why = whyRefused(code);
|
|
534
|
-
return Object.assign(base, {
|
|
535
|
-
kind: 'failed',
|
|
536
|
-
code: code || null,
|
|
537
|
-
why,
|
|
538
|
-
at: m.at || null,
|
|
539
|
-
detail: m.message || null,
|
|
540
|
-
error: 'the workspace state COULD NOT BE MOVED out of the checkout — the filesystem refused '
|
|
541
|
-
+ `the move: ${why}`
|
|
542
|
-
+ (code ? ` (${code}${m.syscall ? ' on ' + m.syscall : ''})` : '')
|
|
543
|
-
+ (m.at ? ` at ${m.at}` : '') + '. Nothing was deleted. '
|
|
544
|
-
+ stillRunning + where
|
|
545
|
-
+ 'Fix the cause above and restart the workspace, or set PMOS_STATE_DIR to a directory this '
|
|
546
|
-
+ 'user can write.',
|
|
547
|
-
});
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
if (m.action === 'unreadable') {
|
|
551
|
-
const code = m.code || null;
|
|
552
|
-
const why = whyUnreadable(code);
|
|
553
|
-
const which = m.which === 'target' ? 'the PER-USER state directory' : 'the state directory in the checkout';
|
|
554
|
-
return Object.assign(base, {
|
|
555
|
-
kind: 'unreadable',
|
|
556
|
-
which: m.which || null,
|
|
557
|
-
code: code || null,
|
|
558
|
-
why,
|
|
559
|
-
at: m.at || null,
|
|
560
|
-
detail: m.message || null,
|
|
561
|
-
// The sentence has to say the thing the old code got WRONG, not just report an errno: this is
|
|
562
|
-
// NOT "you have nothing". Nobody knows yet whether there is anything, and that is the point.
|
|
563
|
-
error: `${which} COULD NOT BE READ, so whether it holds the workspace's state is UNKNOWN — `
|
|
564
|
-
+ 'this is NOT the same as it being empty, and nothing was moved, merged or deleted: '
|
|
565
|
-
+ why
|
|
566
|
-
+ (code ? ` (${code}${m.syscall ? ' on ' + m.syscall : ''})` : '')
|
|
567
|
-
+ (m.at ? ` at ${m.at}` : '') + '. '
|
|
568
|
-
+ stillRunning + where
|
|
569
|
-
+ 'Make that directory readable by this user and restart the workspace, or set '
|
|
570
|
-
+ 'PMOS_STATE_DIR to a directory this user can read.',
|
|
571
|
-
});
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
// The default is to SAY SOMETHING. A migration outcome nobody has thought of yet reaches here.
|
|
575
|
-
return Object.assign(base, {
|
|
576
|
-
kind: 'unknown',
|
|
577
|
-
error: 'the workspace state root is NOT SETTLED — the migration reported '
|
|
578
|
-
+ `\`${action}\`, an outcome this build does not recognise, so it is treated as unresolved. `
|
|
579
|
-
+ stillRunning + where
|
|
580
|
-
+ 'Report this, or set PMOS_STATE_DIR to the directory you want used.',
|
|
581
|
-
});
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
module.exports = { APP, NOTE_NAME, NOT_STATE, SETTLED_ACTIONS,
|
|
585
|
-
WHY_REFUSED, whyRefused, WHY_UNREADABLE, whyUnreadable, isSettled,
|
|
586
|
-
overrideDir, defaultRoot, readState, presence, migrateLegacy, resolveStateRoot, resolveStore,
|
|
587
|
-
stateHealth };
|