create-pmos 0.26.0 → 0.28.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 +26 -0
- package/package.json +1 -1
- package/payload/KIT-VERSION +2 -2
- package/workspace/README.md +39 -7
- package/workspace/bin/pmos.js +5 -1
- package/workspace/doctor.js +5 -1
- package/workspace/lib/drive-guard.js +165 -30
- package/workspace/lib/skill-store.js +180 -6
- package/workspace/lib/state-root.js +587 -0
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +2 -2
- package/workspace/public/pm/data.js +80 -0
- package/workspace/public/pm/panel-initiatives.js +169 -4
- package/workspace/public/pm/panel-newsess.js +507 -0
- package/workspace/public/pm/panel-repos.js +31 -0
- package/workspace/public/pm/panel-tabs.js +34 -2
- package/workspace/public/pm/state.js +40 -1
- package/workspace/public/pm/tab-term.js +277 -16
- package/workspace/server.js +370 -26
- package/workspace/test-run.js +84 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/* test-run.js — THE ISOLATION A SUITE GETS BY CALLING ONE FUNCTION.
|
|
3
|
+
*
|
|
4
|
+
* Every suite in this workspace boots real servers, opens real PTYs and writes real state. The
|
|
5
|
+
* PM's live runtime state used to live at `workspace/state/`, INSIDE the directory the suites run
|
|
6
|
+
* from, so it was reachable BY CONSTRUCTION — by any verb, by any subprocess, by any stray shell
|
|
7
|
+
* command in the tree. `test.js#resetState()` removed sessions/, scratchpad/, workspaces.json,
|
|
8
|
+
* schedules.json, deliverables/ and skills/ from it five times a run, and the suite was safe in CI
|
|
9
|
+
* only because a fresh checkout has nothing there.
|
|
10
|
+
*
|
|
11
|
+
* The state has moved out (lib/state-root.js). What remains is to make sure a suite never resolves
|
|
12
|
+
* to the NEW per-user location either — an installed copy's state IS the PM's own, so "the app's
|
|
13
|
+
* own directory" was never the isolation boundary anybody thought it was. `configure()` points
|
|
14
|
+
* PMOS_STATE_DIR at a fresh scratch directory before `server.js` is ever required and before any
|
|
15
|
+
* child is spawned, so a run writes only into a directory it made.
|
|
16
|
+
*
|
|
17
|
+
* It also hands out a PRIVATE TMUX SOCKET (never the PM's own, `pmos`) and an EPHEMERAL PORT
|
|
18
|
+
* claimed from the kernel rather than a bet on 7599/7601-7606. The port is about collisions, not
|
|
19
|
+
* about state, and it is worth being exact about what it buys: it makes a collision RARE. It does
|
|
20
|
+
* not DETECT one — a suite that does collide still drives whatever answers, and proving whose
|
|
21
|
+
* server is on a port is a different defect with its own repair. Nothing here claims otherwise.
|
|
22
|
+
*
|
|
23
|
+
* It lives at the workspace root rather than under lib/ on purpose: lib/ is the SHIPPED server
|
|
24
|
+
* process, and no-network.test.js holds every file there to a standard this scaffolding does not
|
|
25
|
+
* need to meet. It belongs beside the suites.
|
|
26
|
+
*/
|
|
27
|
+
const fs = require('fs');
|
|
28
|
+
const os = require('os');
|
|
29
|
+
const path = require('path');
|
|
30
|
+
const { execFileSync } = require('child_process');
|
|
31
|
+
|
|
32
|
+
const HOST = '127.0.0.1';
|
|
33
|
+
|
|
34
|
+
let state = null; // set by configure(); this module is a no-op until then
|
|
35
|
+
|
|
36
|
+
/** Bind :0, read the port back, release it. Synchronous because callers need PORT as a const. */
|
|
37
|
+
function claimEphemeralPort() {
|
|
38
|
+
const src = "const s=require('net').createServer();s.listen(0,'127.0.0.1',()=>{process.stdout.write(String(s.address().port));s.close()});";
|
|
39
|
+
const out = execFileSync(process.execPath, ['-e', src], { encoding: 'utf8' });
|
|
40
|
+
const n = Number(String(out).trim());
|
|
41
|
+
if (!Number.isInteger(n) || n <= 0) throw new Error('could not claim an ephemeral port');
|
|
42
|
+
return n;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Arm a suite. MUST be called before `require('./server')` and before any child is spawned —
|
|
47
|
+
* server.js resolves its state root at require time, and a child inherits the environment as it
|
|
48
|
+
* stood when it was spawned.
|
|
49
|
+
*
|
|
50
|
+
* Returns, and also exposes on the module: PORT, HOST, BASE, STATE_DIR.
|
|
51
|
+
*/
|
|
52
|
+
function configure({ name = 'run', port = null } = {}) {
|
|
53
|
+
if (state) return state;
|
|
54
|
+
const stateDir = process.env.PMOS_STATE_DIR
|
|
55
|
+
? path.resolve(process.env.PMOS_STATE_DIR)
|
|
56
|
+
: fs.mkdtempSync(path.join(os.tmpdir(), `pmos-state-${name}-`));
|
|
57
|
+
fs.mkdirSync(stateDir, { recursive: true });
|
|
58
|
+
// In the environment BEFORE server.js is required here and before any child inherits it. Every
|
|
59
|
+
// boot site in every suite goes through process.env, so this is a chokepoint by construction
|
|
60
|
+
// rather than a convention each call site has to remember — and a convention that must be
|
|
61
|
+
// remembered at hundreds of call sites is a convention that will be forgotten at one of them,
|
|
62
|
+
// which is exactly how this class of defect got in.
|
|
63
|
+
process.env.PMOS_STATE_DIR = stateDir;
|
|
64
|
+
// A private tmux socket, never the PM's own ('pmos'), so a suite cannot reach their sessions.
|
|
65
|
+
process.env.PMOS_TMUX_SOCK = process.env.PMOS_TMUX_SOCK || `pmos-${name}-${process.pid}`;
|
|
66
|
+
|
|
67
|
+
const chosen = Number(port) > 0 ? Number(port) : claimEphemeralPort();
|
|
68
|
+
|
|
69
|
+
state = {
|
|
70
|
+
PORT: chosen, HOST, BASE: `http://${HOST}:${chosen}`, STATE_DIR: stateDir,
|
|
71
|
+
/** Environment for a spawned server/launcher: this run's state dir and tmux socket. */
|
|
72
|
+
env(extra = {}) {
|
|
73
|
+
return { ...process.env, PMOS_STATE_DIR: stateDir, ...extra };
|
|
74
|
+
},
|
|
75
|
+
/** A port nobody else holds, for a suite that needs a second server. */
|
|
76
|
+
port: claimEphemeralPort,
|
|
77
|
+
/** Remove the scratch state directory. Safe by construction: this run made it. */
|
|
78
|
+
cleanup() { try { fs.rmSync(stateDir, { recursive: true, force: true }); } catch {} },
|
|
79
|
+
};
|
|
80
|
+
Object.assign(module.exports, state);
|
|
81
|
+
return state;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
module.exports = { configure, claimEphemeralPort, HOST };
|