create-pmos 0.15.0 → 0.16.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/README.md +65 -0
- package/package.json +1 -1
- package/payload/KIT-VERSION +2 -2
- package/workspace/README.md +50 -13
- package/workspace/bin/pmos.js +204 -51
- package/workspace/design-parity.js +358 -0
- package/workspace/doctor.js +54 -0
- package/workspace/lib/find-browser.js +60 -0
- package/workspace/lib/four-panel.js +345 -0
- package/workspace/lib/git-info.js +28 -1
- package/workspace/lib/mobile-nav.js +32 -2
- package/workspace/lib/server-discovery.js +127 -0
- package/workspace/package-lock.json +29 -2
- package/workspace/package.json +13 -5
- package/workspace/public/index.html +79 -409
- package/workspace/public/pm/app.js +242 -0
- package/workspace/public/pm/data.js +905 -0
- package/workspace/public/pm/dom.js +116 -0
- package/workspace/public/pm/fixture.js +979 -0
- package/workspace/public/pm/mobile.css +200 -0
- package/workspace/public/pm/mobile.js +403 -0
- package/workspace/public/pm/modes.js +1076 -0
- package/workspace/public/pm/panel-detail.js +648 -0
- package/workspace/public/pm/panel-initiatives.js +426 -0
- package/workspace/public/pm/panel-repos.js +553 -0
- package/workspace/public/pm/panel-tabs.js +487 -0
- package/workspace/public/pm/state.js +168 -0
- package/workspace/public/pm/tab-doc.js +279 -0
- package/workspace/public/pm/tab-task.js +83 -0
- package/workspace/public/pm/tab-term.js +560 -0
- package/workspace/public/pm/tokens.css +73 -0
- package/workspace/public/vendor/ibm-plex-mono-500.woff2 +0 -0
- package/workspace/server.js +174 -11
- package/workspace/focus.test.js +0 -650
- package/workspace/handoff.test.js +0 -443
- package/workspace/profiles.test.js +0 -362
- package/workspace/public/app.js +0 -5560
- package/workspace/public/styles.css +0 -2258
- package/workspace/repo-scan.test.js +0 -327
package/workspace/server.js
CHANGED
|
@@ -83,6 +83,7 @@ const repoScan = require('./lib/repo-scan'); // scanRepo(root) — pur
|
|
|
83
83
|
const profilesLib = require('./lib/profiles'); // readProfiles(homeDir) / profileLine(profile, read)
|
|
84
84
|
const focus = require('./lib/focus'); // buildFocus(input) → FocusView (cross-product)
|
|
85
85
|
const handoff = require('./lib/handoff'); // readLinks(parsedFrontmatterObject) — never a path
|
|
86
|
+
const fourPanel = require('./lib/four-panel'); // GET /api/initiative — the one-initiative join + the declaration stores
|
|
86
87
|
// The "home" repo this server manages. In PMOS-self the app ships INSIDE its repo
|
|
87
88
|
// (pmos/workspace) so the parent dir IS the repo. But a kit/tenant copy is installed standalone
|
|
88
89
|
// — `npm i -g` puts it under node_modules/pmos-workspace, or it runs from an npx bundle — and
|
|
@@ -106,6 +107,7 @@ const EMBEDDED_SELF = !INSTALLED_COPY && SELF_ROOT === PARENT_DIR &&
|
|
|
106
107
|
// (unsupported direct run) has no home — the client must open a repo to do anything.
|
|
107
108
|
const HOME_ENABLED = EMBEDDED_SELF || (!!process.env.PMOS_SELF_ROOT && dirExists(SELF_ROOT));
|
|
108
109
|
const PUB = path.join(__dirname, 'public');
|
|
110
|
+
const PM_DIR = path.join(PUB, 'pm'); // the four-panel front end — served as a directory, guarded below
|
|
109
111
|
const XROOT = path.join(__dirname, 'node_modules', '@xterm');
|
|
110
112
|
const STATE = path.join(__dirname, 'state');
|
|
111
113
|
const SESS_DIR = path.join(STATE, 'sessions');
|
|
@@ -119,7 +121,11 @@ const CLONE_DIR = path.join(STATE, 'clones');
|
|
|
119
121
|
// PMOS's OWN memory of which profile to preselect per agent binary (profiles R8.2). It is a
|
|
120
122
|
// workspace preference, not an agent attribute — the file lives here for exactly that reason.
|
|
121
123
|
const PREFS_FILE = path.join(STATE, 'prefs.json');
|
|
122
|
-
const
|
|
124
|
+
const discovery = require('./lib/server-discovery'); // how a launcher FINDS a running server
|
|
125
|
+
const PORT = process.env.PMOS_WS_PORT ? Number(process.env.PMOS_WS_PORT) : discovery.DEFAULT_PORT;
|
|
126
|
+
// Written on listen, removed on shutdown. This is the server's answer to "where am I?" — see
|
|
127
|
+
// lib/server-discovery.js for why a launcher must ask rather than assume.
|
|
128
|
+
const PID_FILE = path.join(STATE, discovery.pidFileName(PORT));
|
|
123
129
|
const preview = require('./lib/preview-kind'); // the single reader classifier (md|text|html|image|inert)
|
|
124
130
|
const layoutModel = require('./lib/layout'); // the arrangement model — validated on BOTH sides
|
|
125
131
|
// Bind + token policy (D62). DEFAULT = loopback only, no token (unchanged). The opt-in remote flag
|
|
@@ -306,10 +312,15 @@ function repoIdentity(root) {
|
|
|
306
312
|
const now = Date.now();
|
|
307
313
|
if (hit && now - hit.at < REPO_KEY_TTL_MS) return hit;
|
|
308
314
|
let key = root, name = path.basename(root), branch = null, branchError = null, branchCode = null;
|
|
315
|
+
let remote = null;
|
|
309
316
|
try {
|
|
310
317
|
const trees = gitInfo.worktrees(root);
|
|
311
318
|
if (trees.length && trees[0].dir) { key = path.resolve(trees[0].dir); name = path.basename(key); }
|
|
312
319
|
} catch { /* not a repo, or git absent — the tree is its own group, which is the truth */ }
|
|
320
|
+
// Where this tree came FROM. Memoised with the rest of the identity (same 15s TTL) because it is
|
|
321
|
+
// another `git` shell-out on a call that runs for every workspace on every push. A failure is not
|
|
322
|
+
// a fault: a folder registered by path legitimately has no origin, and null is the honest answer.
|
|
323
|
+
try { remote = gitInfo.originUrl(root); } catch { remote = null; }
|
|
313
324
|
try {
|
|
314
325
|
const bi = gitInfo.branchInfo(root);
|
|
315
326
|
if (bi.ok) branch = bi.detached ? null : bi.current;
|
|
@@ -322,7 +333,7 @@ function repoIdentity(root) {
|
|
|
322
333
|
// one drifts.
|
|
323
334
|
else { branchError = bi.error || 'branch could not be read'; branchCode = bi.code || 'git-error'; }
|
|
324
335
|
} catch { branchError = 'branch could not be read'; branchCode = 'git-error'; }
|
|
325
|
-
const val = { at: now, key, name, branch, branchError, branchCode };
|
|
336
|
+
const val = { at: now, key, name, branch, branchError, branchCode, remote };
|
|
326
337
|
repoKeyCache.set(root, val);
|
|
327
338
|
return val;
|
|
328
339
|
}
|
|
@@ -338,6 +349,14 @@ function wsList() {
|
|
|
338
349
|
// the client renders the first quietly and the second as a fault (R1/R2).
|
|
339
350
|
repo_key: rid.key, repo_name: rid.name, branch: rid.branch,
|
|
340
351
|
branch_error: rid.branchError, branch_code: rid.branchCode,
|
|
352
|
+
// Where the tree came from, and the ONE label the repository list can honestly put on a row.
|
|
353
|
+
// `remote` is the raw origin URL (null when there is none) so a caller can derive a richer
|
|
354
|
+
// answer without guessing; `kind` is the surface's own two-value vocabulary — a repository
|
|
355
|
+
// cloned from GitHub, or a folder on this machine. A remote that is not GitHub's reads as
|
|
356
|
+
// `local`, because `github` would be a claim about a host nobody contacted. Deriving `github`
|
|
357
|
+
// from the mere presence of a `.git` directory is what this replaces.
|
|
358
|
+
remote: rid.remote,
|
|
359
|
+
kind: gitInfo.isGithubRemote(rid.remote) ? 'github' : 'local',
|
|
341
360
|
};
|
|
342
361
|
});
|
|
343
362
|
}
|
|
@@ -745,9 +764,34 @@ function surfaceRoadmap(root, w) {
|
|
|
745
764
|
// (KR\d+) — so `## O-INTEGRITY` and `KR-C` were invisible here while the ladder happily accepted
|
|
746
765
|
// anchors naming them. One field with two opinions is the defect this initiative removes; a second
|
|
747
766
|
// parser would have been the same bug wearing a different regex.
|
|
767
|
+
/* WHERE THE RESOLVER ACTUALLY LIVES — the same two-location lookup `lib/console-state.js` already
|
|
768
|
+
* does for the workflow engine, and for the same reason (incident kit-workflow-engine-missing).
|
|
769
|
+
* This function looked ONLY at `../scripts/anchor-resolve.js`, which exists in a PMOS checkout and
|
|
770
|
+
* nowhere else: the published package ships bin/ payload/ workspace/, so in an adopter's install
|
|
771
|
+
* there is no `scripts/` beside the app and every OKR and spine read answered "the anchor resolver
|
|
772
|
+
* is not present in this install". Panel 1 showed no count and panel 2 no groups, in every release
|
|
773
|
+
* that had the surface.
|
|
774
|
+
*
|
|
775
|
+
* It was invisible to every existing check because a dev checkout always has the file one directory
|
|
776
|
+
* up — the identical blind spot that hid the engine. It was found by launching the INSTALLED app
|
|
777
|
+
* with egress blocked (.github/scripts/offline-workspace-smoke.js), which is the shape adopters get
|
|
778
|
+
* and the shape nothing used to run.
|
|
779
|
+
*
|
|
780
|
+
* 1. `../scripts/anchor-resolve.js` — a repo checkout, preferred so a dev panel and CI cannot
|
|
781
|
+
* disagree about what resolves.
|
|
782
|
+
* 2. `lib/engine/anchor-resolve.js` — the copy `scripts/vendor-workspace-engine.js` vendors into
|
|
783
|
+
* the bundle (it is in the engine's own require closure, so it is already shipped). This is
|
|
784
|
+
* what an adopter runs. */
|
|
785
|
+
const ANCHOR_LIB_PATHS = [
|
|
786
|
+
path.join(__dirname, '..', 'scripts', 'anchor-resolve.js'),
|
|
787
|
+
path.join(__dirname, 'lib', 'engine', 'anchor-resolve.js'),
|
|
788
|
+
];
|
|
748
789
|
function anchorLib() {
|
|
749
|
-
|
|
750
|
-
|
|
790
|
+
for (const p of ANCHOR_LIB_PATHS) {
|
|
791
|
+
if (!fs.existsSync(p)) continue;
|
|
792
|
+
try { return require(p); } catch { return null; }
|
|
793
|
+
}
|
|
794
|
+
return null;
|
|
751
795
|
}
|
|
752
796
|
function surfaceOkr(root) {
|
|
753
797
|
const A = anchorLib();
|
|
@@ -1045,7 +1089,7 @@ function unpersist(id) {
|
|
|
1045
1089
|
}
|
|
1046
1090
|
}
|
|
1047
1091
|
// Scrollback written by the PRE-control-mode architecture carries the tmux CLIENT's own screen
|
|
1048
|
-
// setup, and that gets replayed verbatim into a fresh xterm.js on attach (public/
|
|
1092
|
+
// setup, and that gets replayed verbatim into a fresh xterm.js on attach (public/pm/tab-term.js) — putting
|
|
1049
1093
|
// the terminal straight back into the alternate buffer, where there is no scrollback. Strip it.
|
|
1050
1094
|
//
|
|
1051
1095
|
// The two signatures below are the client's, not an application's: the alt-screen switch IMMEDIATELY
|
|
@@ -2657,7 +2701,7 @@ function serveFile(res, file, type, noCache) {
|
|
|
2657
2701
|
fs.readFile(file, (err, buf) => {
|
|
2658
2702
|
if (err) { res.writeHead(404); res.end('not found'); return; }
|
|
2659
2703
|
const headers = { 'Content-Type': type || TYPES[path.extname(file)] || 'application/octet-stream' };
|
|
2660
|
-
// The app SHELL (index.html +
|
|
2704
|
+
// The app SHELL (index.html + the /pm/ module graph) must revalidate every load: without this a tab left open
|
|
2661
2705
|
// across a `git merge` + server restart keeps running the pre-merge JS, so a fix that IS being
|
|
2662
2706
|
// served never reaches the screen. `no-cache` = "always revalidate", not "never store".
|
|
2663
2707
|
if (noCache) headers['Cache-Control'] = 'no-cache';
|
|
@@ -2678,7 +2722,10 @@ async function handleRequest(req, res) {
|
|
|
2678
2722
|
const p = url.pathname;
|
|
2679
2723
|
|
|
2680
2724
|
if (p === '/' || p === '/index.html') return serveFile(res, path.join(PUB, 'index.html'), null, true);
|
|
2681
|
-
|
|
2725
|
+
// NB: there is no `/app.js` route. The single-file front end that owned it (public/app.js +
|
|
2726
|
+
// public/styles.css) was RETIRED with the four-panel design — the shell is index.html plus the
|
|
2727
|
+
// /pm/ module graph below, and one front end is the point. A stray <script src="/app.js"> now
|
|
2728
|
+
// 404s loudly rather than silently resurrecting a second, diverging surface.
|
|
2682
2729
|
if (p === '/lib/preview-kind.js') return serveFile(res, path.join(__dirname, 'lib', 'preview-kind.js'));
|
|
2683
2730
|
if (p === '/lib/layout.js') return serveFile(res, path.join(__dirname, 'lib', 'layout.js'));
|
|
2684
2731
|
if (p === '/lib/mobile-nav.js') return serveFile(res, path.join(__dirname, 'lib', 'mobile-nav.js'));
|
|
@@ -2689,7 +2736,8 @@ async function handleRequest(req, res) {
|
|
|
2689
2736
|
// client feature that depends on it fails in a way no server-side test can see.
|
|
2690
2737
|
if (p === '/lib/apply-change.js') return serveFile(res, path.join(__dirname, 'lib', 'apply-change.js'));
|
|
2691
2738
|
if (p === '/lib/related-docs.js') return serveFile(res, path.join(__dirname, 'lib', 'related-docs.js'));
|
|
2692
|
-
|
|
2739
|
+
// …and no `/styles.css` route either: the token layer and every component rule now ship as
|
|
2740
|
+
// /pm/tokens.css + /pm/mobile.css through the /pm/ directory route below.
|
|
2693
2741
|
// Every browser asks for /favicon.ico unprompted. Without a route it 404s on every load, which
|
|
2694
2742
|
// puts a permanent red line in the console and in this suite's `page errors seen` list — noise
|
|
2695
2743
|
// that trains you to ignore the one place a real client-side failure would show up. An inline SVG
|
|
@@ -2711,7 +2759,28 @@ async function handleRequest(req, res) {
|
|
|
2711
2759
|
// because the terminal renders bold — with only the regular face the browser synthesises it.
|
|
2712
2760
|
if (p === '/vendor/space-grotesk-var.woff2') return serveFile(res, path.join(PUB, 'vendor', 'space-grotesk-var.woff2'));
|
|
2713
2761
|
if (p === '/vendor/ibm-plex-mono-400.woff2') return serveFile(res, path.join(PUB, 'vendor', 'ibm-plex-mono-400.woff2'));
|
|
2762
|
+
// 500 joined 400/700 with the four-panel design, whose section labels and pills are medium-weight
|
|
2763
|
+
// mono. Without the face the browser synthesises it, and a synthesised weight is exactly the kind
|
|
2764
|
+
// of difference the design-parity comparator reports and a human eye does not.
|
|
2765
|
+
if (p === '/vendor/ibm-plex-mono-500.woff2') return serveFile(res, path.join(PUB, 'vendor', 'ibm-plex-mono-500.woff2'));
|
|
2714
2766
|
if (p === '/vendor/ibm-plex-mono-700.woff2') return serveFile(res, path.join(PUB, 'vendor', 'ibm-plex-mono-700.woff2'));
|
|
2767
|
+
// ── the four-panel front end (public/pm/) ──────────────────────────────────────────────────────
|
|
2768
|
+
// A DIRECTORY route, deliberately, where /lib/ above is an allowlist. The allowlist's own comment
|
|
2769
|
+
// records why it is a trap: a file missing from it 404s silently and the feature that needed it
|
|
2770
|
+
// fails somewhere no server-side test looks. That cost is tolerable for six stable modules and is
|
|
2771
|
+
// not tolerable for a front end split into fifteen, where every new module would be one forgotten
|
|
2772
|
+
// line away from a blank panel. The traversal guard is what makes the directory form safe: the
|
|
2773
|
+
// resolved path must still sit inside PM_DIR, so '..' segments and absolute-looking names cannot
|
|
2774
|
+
// reach a file outside it. Extension-restricted as well — this route serves code and styles, not
|
|
2775
|
+
// whatever else happens to land in the folder.
|
|
2776
|
+
if (p.startsWith('/pm/')) {
|
|
2777
|
+
const rel = p.slice('/pm/'.length);
|
|
2778
|
+
const file = path.resolve(PM_DIR, rel);
|
|
2779
|
+
if (!file.startsWith(PM_DIR + path.sep) || !['.js', '.css', '.json'].includes(path.extname(file))) {
|
|
2780
|
+
res.writeHead(404); return res.end('not found');
|
|
2781
|
+
}
|
|
2782
|
+
return serveFile(res, file, null, true);
|
|
2783
|
+
}
|
|
2715
2784
|
|
|
2716
2785
|
if (p === '/api/workspaces' && req.method === 'GET') return json(res, { workspaces: wsList() });
|
|
2717
2786
|
if (p === '/api/workspaces' && req.method === 'POST') {
|
|
@@ -3260,6 +3329,87 @@ async function handleRequest(req, res) {
|
|
|
3260
3329
|
if (out && out.http_status != null) { out = { ...out }; delete out.http_status; }
|
|
3261
3330
|
return json(res, { name, ...out }, code);
|
|
3262
3331
|
}
|
|
3332
|
+
// ── ONE initiative, assembled (the four-panel surface). ────────────────────────────────────
|
|
3333
|
+
// Panel 3 needs the record, its artifacts, its ladder, its external references, its anchor and
|
|
3334
|
+
// its sessions to answer a single question about a single initiative. Every one of those is
|
|
3335
|
+
// already produced somewhere here; what did not exist was the JOIN, so each client had to make
|
|
3336
|
+
// it — four round-trips per selection, and four chances for two surfaces to disagree about one
|
|
3337
|
+
// record. This route calls the SAME producers the existing endpoints call and joins them once.
|
|
3338
|
+
// The existing endpoints are deliberately unchanged: other surfaces still read them.
|
|
3339
|
+
//
|
|
3340
|
+
// GET → the assembled payload.
|
|
3341
|
+
// POST → append one declaration to a file-mode store (see fourPanel's header for why a link is
|
|
3342
|
+
// a declaration and never an outbound call).
|
|
3343
|
+
if (p === '/api/initiative' && req.method === 'GET') {
|
|
3344
|
+
const w = getWs(url.searchParams.get('ws'));
|
|
3345
|
+
const root = w ? w.root : SELF_ROOT;
|
|
3346
|
+
const id = url.searchParams.get('id') || '';
|
|
3347
|
+
// An id is a filename, not a path — the same guard the handoff surface applies, and the reason
|
|
3348
|
+
// this route can join a path without a traversal check at every use site.
|
|
3349
|
+
if (!fourPanel.validId(id)) {
|
|
3350
|
+
return json(res, { ok: false, error: 'an initiative id is required, and it must be a record name' }, 400);
|
|
3351
|
+
}
|
|
3352
|
+
const engine = consoleState.loadEngine();
|
|
3353
|
+
if (!engine.ok) return json(res, { ok: false, id, error: engine.error });
|
|
3354
|
+
const L = engine.engine.layout(root);
|
|
3355
|
+
// The ENGINE's frontmatter parser, not the workspace's — see lib/four-panel.js on why the two
|
|
3356
|
+
// disagree and why this endpoint's `frontmatter` has to agree with its own `ladder`.
|
|
3357
|
+
const rec = fourPanel.readRecord({ root, dir: L.initiatives, id, parse: engine.engine.parseLeadingFrontmatter });
|
|
3358
|
+
// A record that would not READ is the one fact the panel must show, and it is the whole
|
|
3359
|
+
// answer: deriving a ladder over a file that could not be parsed would be deriving over
|
|
3360
|
+
// nothing, and reporting the result beside the failure would invite reading it as data.
|
|
3361
|
+
let ladder = null, linksOut = null;
|
|
3362
|
+
if (rec.ok) {
|
|
3363
|
+
try { ladder = consoleState.ladder({ root, id }); }
|
|
3364
|
+
catch (e) { ladder = { ok: false, error: 'the ladder could not be derived: ' + String((e && e.message) || 'error').slice(0, 140) }; }
|
|
3365
|
+
// An ABSENT record declares nothing, so there is nothing for the handoff reader to read —
|
|
3366
|
+
// distinct from a record that declares no handoff, which readLinks reports as empty.
|
|
3367
|
+
if (!rec.absent) {
|
|
3368
|
+
try { linksOut = handoff.readLinks(rec.frontmatter || {}); }
|
|
3369
|
+
catch (e) { linksOut = null; void e; }
|
|
3370
|
+
}
|
|
3371
|
+
}
|
|
3372
|
+
let index = { initiatives: [] };
|
|
3373
|
+
try { if (wsShape(root) === 'pmos') index = initiativeIndex(root); }
|
|
3374
|
+
catch (e) { void e; }
|
|
3375
|
+
const stateDir = fourPanel.storeDir(engine.engine, root);
|
|
3376
|
+
const out = fourPanel.assemble({
|
|
3377
|
+
id, rec, ladder, links: linksOut, index,
|
|
3378
|
+
sessions: sessionList(), workspaces: wsList(),
|
|
3379
|
+
anchor: rec.ok && !rec.absent
|
|
3380
|
+
? fourPanel.anchorFor({ anchors: anchorLib(), root, raw: (rec.frontmatter || {}).parent_kr })
|
|
3381
|
+
: null,
|
|
3382
|
+
artifactLinks: fourPanel.readDeclarations(stateDir, fourPanel.ARTIFACT_LINKS),
|
|
3383
|
+
initiativeLinks: fourPanel.readDeclarations(stateDir, fourPanel.INITIATIVE_LINKS),
|
|
3384
|
+
});
|
|
3385
|
+
// Discriminated READ results stay 200 — the body carries the reason, the transport code cannot.
|
|
3386
|
+
return json(res, { ws: w ? w.id : null, ...out });
|
|
3387
|
+
}
|
|
3388
|
+
if (p === '/api/initiative/link' && req.method === 'POST') {
|
|
3389
|
+
// Recording that an artifact corresponds to an external page. This writes a DECLARATION and
|
|
3390
|
+
// makes no request: PMOS holds no credential, so it can state what the PM says the mapping is
|
|
3391
|
+
// and it cannot state anything about the destination's contents.
|
|
3392
|
+
const w = getWs(url.searchParams.get('ws'));
|
|
3393
|
+
let body = {};
|
|
3394
|
+
try { body = JSON.parse((await readBody(req)) || '{}'); }
|
|
3395
|
+
catch { return json(res, { ok: false, error: 'invalid JSON body' }, 400); }
|
|
3396
|
+
const w2 = body.ws ? getWs(body.ws) : w;
|
|
3397
|
+
const kind = String(body.store || 'artifact');
|
|
3398
|
+
const built = kind === 'initiative'
|
|
3399
|
+
? fourPanel.initiativeLinkRow(body)
|
|
3400
|
+
: fourPanel.artifactLinkRow(body);
|
|
3401
|
+
if (!built.ok) return json(res, { ok: false, error: built.error }, 400);
|
|
3402
|
+
const engine = consoleState.loadEngine();
|
|
3403
|
+
if (!engine.ok) return json(res, { ok: false, error: engine.error }, 500);
|
|
3404
|
+
const stateDir = fourPanel.storeDir(engine.engine, w2 ? w2.root : SELF_ROOT);
|
|
3405
|
+
const wrote = fourPanel.appendDeclaration(
|
|
3406
|
+
stateDir,
|
|
3407
|
+
kind === 'initiative' ? fourPanel.INITIATIVE_LINKS : fourPanel.ARTIFACT_LINKS,
|
|
3408
|
+
built.row,
|
|
3409
|
+
);
|
|
3410
|
+
// A write that failed returns 500 and says so — never a false green (the /api/gate rule).
|
|
3411
|
+
return json(res, { ...wrote, connectors: fourPanel.connectors() }, wrote.ok ? 200 : 500);
|
|
3412
|
+
}
|
|
3263
3413
|
if (p === '/api/search') {
|
|
3264
3414
|
// Episodic recall over this workspace's own past sessions. Localhost only, like everything
|
|
3265
3415
|
// else here — an agent running in a pane can curl it to check what an earlier run tried.
|
|
@@ -3763,6 +3913,7 @@ if (require.main === module) {
|
|
|
3763
3913
|
if (shuttingDown) return; shuttingDown = true;
|
|
3764
3914
|
for (const s of sessions.values()) if (s.pty) { try { persist(s); } catch {} }
|
|
3765
3915
|
for (const l of listeners) { try { l.close(); } catch {} }
|
|
3916
|
+
try { fs.unlinkSync(PID_FILE); } catch {} // stop being discoverable the moment we stop serving
|
|
3766
3917
|
process.exit(0);
|
|
3767
3918
|
};
|
|
3768
3919
|
process.on('SIGTERM', shutdown);
|
|
@@ -3770,7 +3921,9 @@ if (require.main === module) {
|
|
|
3770
3921
|
const onError = (e) => {
|
|
3771
3922
|
if (e.code === 'EADDRINUSE') {
|
|
3772
3923
|
console.error(`✗ port ${PORT} is already in use (another workspace server running?).`);
|
|
3773
|
-
console.error(`
|
|
3924
|
+
console.error(` \`pmos\` finds a running workspace on any port and reuses it, and picks a free`);
|
|
3925
|
+
console.error(` port when something else holds this one. \`pmos stop\` shuts running ones down.`);
|
|
3926
|
+
console.error(` To pin a port by hand: PMOS_WS_PORT=7500 npm start`);
|
|
3774
3927
|
process.exit(1);
|
|
3775
3928
|
}
|
|
3776
3929
|
if (e.code === 'EADDRNOTAVAIL') {
|
|
@@ -3785,7 +3938,13 @@ if (require.main === module) {
|
|
|
3785
3938
|
l.on('error', onError);
|
|
3786
3939
|
l.listen(PORT, addr, () => {
|
|
3787
3940
|
// stdout only — no request/header logging (D08). Banner (incl. any generated token) printed once.
|
|
3788
|
-
if (++up
|
|
3941
|
+
if (++up !== BIND.binds.length) return;
|
|
3942
|
+
// Record the port WE are serving, whoever started us. The launcher discovers a running
|
|
3943
|
+
// workspace from these files, so a server started by `npm start` (which is exactly what the
|
|
3944
|
+
// old EADDRINUSE advice told the PM to do, on another port) has to leave the same trace as
|
|
3945
|
+
// one started by `pmos` — otherwise the next `pmos` cannot see it and starts a second server.
|
|
3946
|
+
try { fs.mkdirSync(STATE, { recursive: true }); fs.writeFileSync(PID_FILE, String(process.pid)); } catch {}
|
|
3947
|
+
for (const line of remoteAccess.startupLines(BIND, PORT)) console.log(line);
|
|
3789
3948
|
});
|
|
3790
3949
|
});
|
|
3791
3950
|
}
|
|
@@ -3806,4 +3965,8 @@ module.exports = { server, spawnSession, removeSession, renameSession, sessions,
|
|
|
3806
3965
|
trimBuf, SCROLLBACK,
|
|
3807
3966
|
// the liveness heartbeat's guard, exported pure so the "idle machine stays idle" half can be
|
|
3808
3967
|
// asserted without a server that has no clients (the suite always has both).
|
|
3809
|
-
shouldHeartbeat, LIVENESS_MS, TX_REREAD_MS
|
|
3968
|
+
shouldHeartbeat, LIVENESS_MS, TX_REREAD_MS,
|
|
3969
|
+
// where the anchor resolver is looked for — exported so the suite can assert the INSTALL
|
|
3970
|
+
// location is in the list. A dev checkout satisfies entry 1 and would hide the loss of entry 2
|
|
3971
|
+
// exactly as it hid the workflow engine's absence for four releases (PKG4).
|
|
3972
|
+
ANCHOR_LIB_PATHS };
|