create-pmos 0.15.0 → 0.16.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/README.md +38 -0
- package/package.json +1 -1
- package/payload/KIT-VERSION +1 -1
- package/workspace/README.md +44 -12
- package/workspace/design-parity.js +358 -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/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 +877 -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 +460 -0
- package/workspace/public/pm/panel-tabs.js +487 -0
- package/workspace/public/pm/state.js +164 -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 +158 -8
- 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');
|
|
@@ -306,10 +308,15 @@ function repoIdentity(root) {
|
|
|
306
308
|
const now = Date.now();
|
|
307
309
|
if (hit && now - hit.at < REPO_KEY_TTL_MS) return hit;
|
|
308
310
|
let key = root, name = path.basename(root), branch = null, branchError = null, branchCode = null;
|
|
311
|
+
let remote = null;
|
|
309
312
|
try {
|
|
310
313
|
const trees = gitInfo.worktrees(root);
|
|
311
314
|
if (trees.length && trees[0].dir) { key = path.resolve(trees[0].dir); name = path.basename(key); }
|
|
312
315
|
} catch { /* not a repo, or git absent — the tree is its own group, which is the truth */ }
|
|
316
|
+
// Where this tree came FROM. Memoised with the rest of the identity (same 15s TTL) because it is
|
|
317
|
+
// another `git` shell-out on a call that runs for every workspace on every push. A failure is not
|
|
318
|
+
// a fault: a folder registered by path legitimately has no origin, and null is the honest answer.
|
|
319
|
+
try { remote = gitInfo.originUrl(root); } catch { remote = null; }
|
|
313
320
|
try {
|
|
314
321
|
const bi = gitInfo.branchInfo(root);
|
|
315
322
|
if (bi.ok) branch = bi.detached ? null : bi.current;
|
|
@@ -322,7 +329,7 @@ function repoIdentity(root) {
|
|
|
322
329
|
// one drifts.
|
|
323
330
|
else { branchError = bi.error || 'branch could not be read'; branchCode = bi.code || 'git-error'; }
|
|
324
331
|
} catch { branchError = 'branch could not be read'; branchCode = 'git-error'; }
|
|
325
|
-
const val = { at: now, key, name, branch, branchError, branchCode };
|
|
332
|
+
const val = { at: now, key, name, branch, branchError, branchCode, remote };
|
|
326
333
|
repoKeyCache.set(root, val);
|
|
327
334
|
return val;
|
|
328
335
|
}
|
|
@@ -338,6 +345,14 @@ function wsList() {
|
|
|
338
345
|
// the client renders the first quietly and the second as a fault (R1/R2).
|
|
339
346
|
repo_key: rid.key, repo_name: rid.name, branch: rid.branch,
|
|
340
347
|
branch_error: rid.branchError, branch_code: rid.branchCode,
|
|
348
|
+
// Where the tree came from, and the ONE label the repository list can honestly put on a row.
|
|
349
|
+
// `remote` is the raw origin URL (null when there is none) so a caller can derive a richer
|
|
350
|
+
// answer without guessing; `kind` is the surface's own two-value vocabulary — a repository
|
|
351
|
+
// cloned from GitHub, or a folder on this machine. A remote that is not GitHub's reads as
|
|
352
|
+
// `local`, because `github` would be a claim about a host nobody contacted. Deriving `github`
|
|
353
|
+
// from the mere presence of a `.git` directory is what this replaces.
|
|
354
|
+
remote: rid.remote,
|
|
355
|
+
kind: gitInfo.isGithubRemote(rid.remote) ? 'github' : 'local',
|
|
341
356
|
};
|
|
342
357
|
});
|
|
343
358
|
}
|
|
@@ -745,9 +760,34 @@ function surfaceRoadmap(root, w) {
|
|
|
745
760
|
// (KR\d+) — so `## O-INTEGRITY` and `KR-C` were invisible here while the ladder happily accepted
|
|
746
761
|
// anchors naming them. One field with two opinions is the defect this initiative removes; a second
|
|
747
762
|
// parser would have been the same bug wearing a different regex.
|
|
763
|
+
/* WHERE THE RESOLVER ACTUALLY LIVES — the same two-location lookup `lib/console-state.js` already
|
|
764
|
+
* does for the workflow engine, and for the same reason (incident kit-workflow-engine-missing).
|
|
765
|
+
* This function looked ONLY at `../scripts/anchor-resolve.js`, which exists in a PMOS checkout and
|
|
766
|
+
* nowhere else: the published package ships bin/ payload/ workspace/, so in an adopter's install
|
|
767
|
+
* there is no `scripts/` beside the app and every OKR and spine read answered "the anchor resolver
|
|
768
|
+
* is not present in this install". Panel 1 showed no count and panel 2 no groups, in every release
|
|
769
|
+
* that had the surface.
|
|
770
|
+
*
|
|
771
|
+
* It was invisible to every existing check because a dev checkout always has the file one directory
|
|
772
|
+
* up — the identical blind spot that hid the engine. It was found by launching the INSTALLED app
|
|
773
|
+
* with egress blocked (.github/scripts/offline-workspace-smoke.js), which is the shape adopters get
|
|
774
|
+
* and the shape nothing used to run.
|
|
775
|
+
*
|
|
776
|
+
* 1. `../scripts/anchor-resolve.js` — a repo checkout, preferred so a dev panel and CI cannot
|
|
777
|
+
* disagree about what resolves.
|
|
778
|
+
* 2. `lib/engine/anchor-resolve.js` — the copy `scripts/vendor-workspace-engine.js` vendors into
|
|
779
|
+
* the bundle (it is in the engine's own require closure, so it is already shipped). This is
|
|
780
|
+
* what an adopter runs. */
|
|
781
|
+
const ANCHOR_LIB_PATHS = [
|
|
782
|
+
path.join(__dirname, '..', 'scripts', 'anchor-resolve.js'),
|
|
783
|
+
path.join(__dirname, 'lib', 'engine', 'anchor-resolve.js'),
|
|
784
|
+
];
|
|
748
785
|
function anchorLib() {
|
|
749
|
-
|
|
750
|
-
|
|
786
|
+
for (const p of ANCHOR_LIB_PATHS) {
|
|
787
|
+
if (!fs.existsSync(p)) continue;
|
|
788
|
+
try { return require(p); } catch { return null; }
|
|
789
|
+
}
|
|
790
|
+
return null;
|
|
751
791
|
}
|
|
752
792
|
function surfaceOkr(root) {
|
|
753
793
|
const A = anchorLib();
|
|
@@ -1045,7 +1085,7 @@ function unpersist(id) {
|
|
|
1045
1085
|
}
|
|
1046
1086
|
}
|
|
1047
1087
|
// 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/
|
|
1088
|
+
// setup, and that gets replayed verbatim into a fresh xterm.js on attach (public/pm/tab-term.js) — putting
|
|
1049
1089
|
// the terminal straight back into the alternate buffer, where there is no scrollback. Strip it.
|
|
1050
1090
|
//
|
|
1051
1091
|
// The two signatures below are the client's, not an application's: the alt-screen switch IMMEDIATELY
|
|
@@ -2657,7 +2697,7 @@ function serveFile(res, file, type, noCache) {
|
|
|
2657
2697
|
fs.readFile(file, (err, buf) => {
|
|
2658
2698
|
if (err) { res.writeHead(404); res.end('not found'); return; }
|
|
2659
2699
|
const headers = { 'Content-Type': type || TYPES[path.extname(file)] || 'application/octet-stream' };
|
|
2660
|
-
// The app SHELL (index.html +
|
|
2700
|
+
// The app SHELL (index.html + the /pm/ module graph) must revalidate every load: without this a tab left open
|
|
2661
2701
|
// across a `git merge` + server restart keeps running the pre-merge JS, so a fix that IS being
|
|
2662
2702
|
// served never reaches the screen. `no-cache` = "always revalidate", not "never store".
|
|
2663
2703
|
if (noCache) headers['Cache-Control'] = 'no-cache';
|
|
@@ -2678,7 +2718,10 @@ async function handleRequest(req, res) {
|
|
|
2678
2718
|
const p = url.pathname;
|
|
2679
2719
|
|
|
2680
2720
|
if (p === '/' || p === '/index.html') return serveFile(res, path.join(PUB, 'index.html'), null, true);
|
|
2681
|
-
|
|
2721
|
+
// NB: there is no `/app.js` route. The single-file front end that owned it (public/app.js +
|
|
2722
|
+
// public/styles.css) was RETIRED with the four-panel design — the shell is index.html plus the
|
|
2723
|
+
// /pm/ module graph below, and one front end is the point. A stray <script src="/app.js"> now
|
|
2724
|
+
// 404s loudly rather than silently resurrecting a second, diverging surface.
|
|
2682
2725
|
if (p === '/lib/preview-kind.js') return serveFile(res, path.join(__dirname, 'lib', 'preview-kind.js'));
|
|
2683
2726
|
if (p === '/lib/layout.js') return serveFile(res, path.join(__dirname, 'lib', 'layout.js'));
|
|
2684
2727
|
if (p === '/lib/mobile-nav.js') return serveFile(res, path.join(__dirname, 'lib', 'mobile-nav.js'));
|
|
@@ -2689,7 +2732,8 @@ async function handleRequest(req, res) {
|
|
|
2689
2732
|
// client feature that depends on it fails in a way no server-side test can see.
|
|
2690
2733
|
if (p === '/lib/apply-change.js') return serveFile(res, path.join(__dirname, 'lib', 'apply-change.js'));
|
|
2691
2734
|
if (p === '/lib/related-docs.js') return serveFile(res, path.join(__dirname, 'lib', 'related-docs.js'));
|
|
2692
|
-
|
|
2735
|
+
// …and no `/styles.css` route either: the token layer and every component rule now ship as
|
|
2736
|
+
// /pm/tokens.css + /pm/mobile.css through the /pm/ directory route below.
|
|
2693
2737
|
// Every browser asks for /favicon.ico unprompted. Without a route it 404s on every load, which
|
|
2694
2738
|
// puts a permanent red line in the console and in this suite's `page errors seen` list — noise
|
|
2695
2739
|
// that trains you to ignore the one place a real client-side failure would show up. An inline SVG
|
|
@@ -2711,7 +2755,28 @@ async function handleRequest(req, res) {
|
|
|
2711
2755
|
// because the terminal renders bold — with only the regular face the browser synthesises it.
|
|
2712
2756
|
if (p === '/vendor/space-grotesk-var.woff2') return serveFile(res, path.join(PUB, 'vendor', 'space-grotesk-var.woff2'));
|
|
2713
2757
|
if (p === '/vendor/ibm-plex-mono-400.woff2') return serveFile(res, path.join(PUB, 'vendor', 'ibm-plex-mono-400.woff2'));
|
|
2758
|
+
// 500 joined 400/700 with the four-panel design, whose section labels and pills are medium-weight
|
|
2759
|
+
// mono. Without the face the browser synthesises it, and a synthesised weight is exactly the kind
|
|
2760
|
+
// of difference the design-parity comparator reports and a human eye does not.
|
|
2761
|
+
if (p === '/vendor/ibm-plex-mono-500.woff2') return serveFile(res, path.join(PUB, 'vendor', 'ibm-plex-mono-500.woff2'));
|
|
2714
2762
|
if (p === '/vendor/ibm-plex-mono-700.woff2') return serveFile(res, path.join(PUB, 'vendor', 'ibm-plex-mono-700.woff2'));
|
|
2763
|
+
// ── the four-panel front end (public/pm/) ──────────────────────────────────────────────────────
|
|
2764
|
+
// A DIRECTORY route, deliberately, where /lib/ above is an allowlist. The allowlist's own comment
|
|
2765
|
+
// records why it is a trap: a file missing from it 404s silently and the feature that needed it
|
|
2766
|
+
// fails somewhere no server-side test looks. That cost is tolerable for six stable modules and is
|
|
2767
|
+
// not tolerable for a front end split into fifteen, where every new module would be one forgotten
|
|
2768
|
+
// line away from a blank panel. The traversal guard is what makes the directory form safe: the
|
|
2769
|
+
// resolved path must still sit inside PM_DIR, so '..' segments and absolute-looking names cannot
|
|
2770
|
+
// reach a file outside it. Extension-restricted as well — this route serves code and styles, not
|
|
2771
|
+
// whatever else happens to land in the folder.
|
|
2772
|
+
if (p.startsWith('/pm/')) {
|
|
2773
|
+
const rel = p.slice('/pm/'.length);
|
|
2774
|
+
const file = path.resolve(PM_DIR, rel);
|
|
2775
|
+
if (!file.startsWith(PM_DIR + path.sep) || !['.js', '.css', '.json'].includes(path.extname(file))) {
|
|
2776
|
+
res.writeHead(404); return res.end('not found');
|
|
2777
|
+
}
|
|
2778
|
+
return serveFile(res, file, null, true);
|
|
2779
|
+
}
|
|
2715
2780
|
|
|
2716
2781
|
if (p === '/api/workspaces' && req.method === 'GET') return json(res, { workspaces: wsList() });
|
|
2717
2782
|
if (p === '/api/workspaces' && req.method === 'POST') {
|
|
@@ -3260,6 +3325,87 @@ async function handleRequest(req, res) {
|
|
|
3260
3325
|
if (out && out.http_status != null) { out = { ...out }; delete out.http_status; }
|
|
3261
3326
|
return json(res, { name, ...out }, code);
|
|
3262
3327
|
}
|
|
3328
|
+
// ── ONE initiative, assembled (the four-panel surface). ────────────────────────────────────
|
|
3329
|
+
// Panel 3 needs the record, its artifacts, its ladder, its external references, its anchor and
|
|
3330
|
+
// its sessions to answer a single question about a single initiative. Every one of those is
|
|
3331
|
+
// already produced somewhere here; what did not exist was the JOIN, so each client had to make
|
|
3332
|
+
// it — four round-trips per selection, and four chances for two surfaces to disagree about one
|
|
3333
|
+
// record. This route calls the SAME producers the existing endpoints call and joins them once.
|
|
3334
|
+
// The existing endpoints are deliberately unchanged: other surfaces still read them.
|
|
3335
|
+
//
|
|
3336
|
+
// GET → the assembled payload.
|
|
3337
|
+
// POST → append one declaration to a file-mode store (see fourPanel's header for why a link is
|
|
3338
|
+
// a declaration and never an outbound call).
|
|
3339
|
+
if (p === '/api/initiative' && req.method === 'GET') {
|
|
3340
|
+
const w = getWs(url.searchParams.get('ws'));
|
|
3341
|
+
const root = w ? w.root : SELF_ROOT;
|
|
3342
|
+
const id = url.searchParams.get('id') || '';
|
|
3343
|
+
// An id is a filename, not a path — the same guard the handoff surface applies, and the reason
|
|
3344
|
+
// this route can join a path without a traversal check at every use site.
|
|
3345
|
+
if (!fourPanel.validId(id)) {
|
|
3346
|
+
return json(res, { ok: false, error: 'an initiative id is required, and it must be a record name' }, 400);
|
|
3347
|
+
}
|
|
3348
|
+
const engine = consoleState.loadEngine();
|
|
3349
|
+
if (!engine.ok) return json(res, { ok: false, id, error: engine.error });
|
|
3350
|
+
const L = engine.engine.layout(root);
|
|
3351
|
+
// The ENGINE's frontmatter parser, not the workspace's — see lib/four-panel.js on why the two
|
|
3352
|
+
// disagree and why this endpoint's `frontmatter` has to agree with its own `ladder`.
|
|
3353
|
+
const rec = fourPanel.readRecord({ root, dir: L.initiatives, id, parse: engine.engine.parseLeadingFrontmatter });
|
|
3354
|
+
// A record that would not READ is the one fact the panel must show, and it is the whole
|
|
3355
|
+
// answer: deriving a ladder over a file that could not be parsed would be deriving over
|
|
3356
|
+
// nothing, and reporting the result beside the failure would invite reading it as data.
|
|
3357
|
+
let ladder = null, linksOut = null;
|
|
3358
|
+
if (rec.ok) {
|
|
3359
|
+
try { ladder = consoleState.ladder({ root, id }); }
|
|
3360
|
+
catch (e) { ladder = { ok: false, error: 'the ladder could not be derived: ' + String((e && e.message) || 'error').slice(0, 140) }; }
|
|
3361
|
+
// An ABSENT record declares nothing, so there is nothing for the handoff reader to read —
|
|
3362
|
+
// distinct from a record that declares no handoff, which readLinks reports as empty.
|
|
3363
|
+
if (!rec.absent) {
|
|
3364
|
+
try { linksOut = handoff.readLinks(rec.frontmatter || {}); }
|
|
3365
|
+
catch (e) { linksOut = null; void e; }
|
|
3366
|
+
}
|
|
3367
|
+
}
|
|
3368
|
+
let index = { initiatives: [] };
|
|
3369
|
+
try { if (wsShape(root) === 'pmos') index = initiativeIndex(root); }
|
|
3370
|
+
catch (e) { void e; }
|
|
3371
|
+
const stateDir = fourPanel.storeDir(engine.engine, root);
|
|
3372
|
+
const out = fourPanel.assemble({
|
|
3373
|
+
id, rec, ladder, links: linksOut, index,
|
|
3374
|
+
sessions: sessionList(), workspaces: wsList(),
|
|
3375
|
+
anchor: rec.ok && !rec.absent
|
|
3376
|
+
? fourPanel.anchorFor({ anchors: anchorLib(), root, raw: (rec.frontmatter || {}).parent_kr })
|
|
3377
|
+
: null,
|
|
3378
|
+
artifactLinks: fourPanel.readDeclarations(stateDir, fourPanel.ARTIFACT_LINKS),
|
|
3379
|
+
initiativeLinks: fourPanel.readDeclarations(stateDir, fourPanel.INITIATIVE_LINKS),
|
|
3380
|
+
});
|
|
3381
|
+
// Discriminated READ results stay 200 — the body carries the reason, the transport code cannot.
|
|
3382
|
+
return json(res, { ws: w ? w.id : null, ...out });
|
|
3383
|
+
}
|
|
3384
|
+
if (p === '/api/initiative/link' && req.method === 'POST') {
|
|
3385
|
+
// Recording that an artifact corresponds to an external page. This writes a DECLARATION and
|
|
3386
|
+
// makes no request: PMOS holds no credential, so it can state what the PM says the mapping is
|
|
3387
|
+
// and it cannot state anything about the destination's contents.
|
|
3388
|
+
const w = getWs(url.searchParams.get('ws'));
|
|
3389
|
+
let body = {};
|
|
3390
|
+
try { body = JSON.parse((await readBody(req)) || '{}'); }
|
|
3391
|
+
catch { return json(res, { ok: false, error: 'invalid JSON body' }, 400); }
|
|
3392
|
+
const w2 = body.ws ? getWs(body.ws) : w;
|
|
3393
|
+
const kind = String(body.store || 'artifact');
|
|
3394
|
+
const built = kind === 'initiative'
|
|
3395
|
+
? fourPanel.initiativeLinkRow(body)
|
|
3396
|
+
: fourPanel.artifactLinkRow(body);
|
|
3397
|
+
if (!built.ok) return json(res, { ok: false, error: built.error }, 400);
|
|
3398
|
+
const engine = consoleState.loadEngine();
|
|
3399
|
+
if (!engine.ok) return json(res, { ok: false, error: engine.error }, 500);
|
|
3400
|
+
const stateDir = fourPanel.storeDir(engine.engine, w2 ? w2.root : SELF_ROOT);
|
|
3401
|
+
const wrote = fourPanel.appendDeclaration(
|
|
3402
|
+
stateDir,
|
|
3403
|
+
kind === 'initiative' ? fourPanel.INITIATIVE_LINKS : fourPanel.ARTIFACT_LINKS,
|
|
3404
|
+
built.row,
|
|
3405
|
+
);
|
|
3406
|
+
// A write that failed returns 500 and says so — never a false green (the /api/gate rule).
|
|
3407
|
+
return json(res, { ...wrote, connectors: fourPanel.connectors() }, wrote.ok ? 200 : 500);
|
|
3408
|
+
}
|
|
3263
3409
|
if (p === '/api/search') {
|
|
3264
3410
|
// Episodic recall over this workspace's own past sessions. Localhost only, like everything
|
|
3265
3411
|
// else here — an agent running in a pane can curl it to check what an earlier run tried.
|
|
@@ -3806,4 +3952,8 @@ module.exports = { server, spawnSession, removeSession, renameSession, sessions,
|
|
|
3806
3952
|
trimBuf, SCROLLBACK,
|
|
3807
3953
|
// the liveness heartbeat's guard, exported pure so the "idle machine stays idle" half can be
|
|
3808
3954
|
// asserted without a server that has no clients (the suite always has both).
|
|
3809
|
-
shouldHeartbeat, LIVENESS_MS, TX_REREAD_MS
|
|
3955
|
+
shouldHeartbeat, LIVENESS_MS, TX_REREAD_MS,
|
|
3956
|
+
// where the anchor resolver is looked for — exported so the suite can assert the INSTALL
|
|
3957
|
+
// location is in the list. A dev checkout satisfies entry 1 and would hide the loss of entry 2
|
|
3958
|
+
// exactly as it hid the workflow engine's absence for four releases (PKG4).
|
|
3959
|
+
ANCHOR_LIB_PATHS };
|