create-pmos 0.60.0 → 0.60.2
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/guide/layout.md +5 -3
- package/workspace/lib/kit-update.js +35 -5
- package/workspace/package-lock.json +1 -1
- package/workspace/package.json +1 -1
- package/workspace/public/pm/dest-settings.js +33 -17
- package/workspace/public/pm/frame-copy.js +3 -3
- package/workspace/server.js +8 -5
- package/workspace/test-files.json +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-pmos",
|
|
3
|
-
"version": "0.60.
|
|
3
|
+
"version": "0.60.2",
|
|
4
4
|
"description": "Scaffold the PMOS portable kit — run the PMOS product-management workflow (initiative → lane-fit spec → contract → build → three gates, with work-type lanes for features, research, maintenance, incidents, and content) entirely inside your own repo, with all state as git-versioned files. Includes `workflow-sweep`, which audits the state every board derives from and separates real defects from work you simply have not done. `create-pmos workspace` also installs + launches the local Terminal Workspace GUI. No database, no network, no telemetry.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"bin": {
|
package/payload/KIT-VERSION
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
type: Reference
|
|
3
3
|
title: "Eval calibration log"
|
|
4
4
|
description: "Append-only record of every PM override of an evaluator verdict."
|
|
5
|
-
generated: {by: process:create-pmos, at: "2026-10-06T15:
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-06T15:52:39Z"}
|
|
6
6
|
status: stable
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
type: Reference
|
|
3
3
|
title: "Health budgets"
|
|
4
4
|
description: "Closed registry of health-budget anchors (D59), edited only by the PM."
|
|
5
|
-
generated: {by: process:create-pmos, at: "2026-10-06T15:
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-06T15:52:39Z"}
|
|
6
6
|
status: stable
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
type: Reference
|
|
3
3
|
title: "Discoveries and friction log"
|
|
4
4
|
description: "Append-only record of what runs discovered and where the process rubbed."
|
|
5
|
-
generated: {by: process:create-pmos, at: "2026-10-06T15:
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-06T15:52:39Z"}
|
|
6
6
|
status: stable
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -33,9 +33,11 @@ Settings holds settings only, in seven sections:
|
|
|
33
33
|
- **Validate consent**: the consent of the current repository. Turn it on or off here. **Open Validate** opens the Validate page as a tab.
|
|
34
34
|
- **Online**: puts this workspace at your own address, such as `pmos.pm-os.app`, through a Cloudflare tunnel. Enter the public address, the tunnel name, your Cloudflare team domain, the Access audience (AUD) tag and your email, then press **Turn on**. The workspace runs the tunnel and remembers the switch when it restarts. On your phone, open the address and sign in with Cloudflare Access (Google or an email code). The workspace does not ask for its token there. Only the computer that runs the workspace can change this section. A phone sees whether it is on. When a sign-in at the address is refused, this section names the check that failed, such as the email or the AUD tag. [REMOTE.md](../REMOTE.md) has the one-time Cloudflare steps.
|
|
35
35
|
- **Theme**: Dark or Light.
|
|
36
|
-
- **Updates**: the version of the workspace app and the version of the kit in the current repository. Press **Check for updates** to compare them with the latest published version of PMOS. This is the only time the workspace asks the internet for a version, and it asks only when you press. When a newer version is available,
|
|
37
|
-
- **
|
|
38
|
-
- **
|
|
36
|
+
- **Updates**: the version of the workspace app and the version of the kit in the current repository. Press **Check for updates** to compare them with the latest published version of PMOS. This is the only time the workspace asks the internet for a version, and it asks only when you press. When a newer version is available, one button shows: **Update to** and the version. It updates every part that is older, in this order:
|
|
37
|
+
- **The kit** in the current repository, when it is older. This does what `npx create-pmos@latest --update` does in a terminal. It changes only the kit's own files under `pmos/` (skills, scripts, templates and guides). Your records in `state/`, `planning/` and `okf/product/` do not change. The last lines of the output show under the button. The changed files are under `pmos/`, which Sync treats as records, so Sync (or automatic sync) sends them with your other records. To read them first, look at `git diff pmos/` before you sync.
|
|
38
|
+
- **The workspace app**, when it is older. This does what `pmos upgrade` does in a terminal. The button asks you first. The workspace stops, installs the new version and starts again, and the page reloads when the new version starts. You do not need a terminal. Sessions keep running in tmux. It works on the computer that runs the workspace and at its Online address, because the page comes back there after the restart. It also works on a phone or public address that signs in with a token set in `PMOS_TOKEN`, which the workspace keeps. When the workspace made its own token at start, the token changes on the restart, so the page refuses and says where to press it. When the workspace runs from a PMOS checkout, update it with `git pull` instead.
|
|
39
|
+
|
|
40
|
+
A new screen or a renamed menu comes with the workspace app, not with the kit.
|
|
39
41
|
|
|
40
42
|
Your work is never in Settings. It is in Agenda, Initiatives, Board and Reports.
|
|
41
43
|
|
|
@@ -6,7 +6,9 @@
|
|
|
6
6
|
* latest({ exec }) → Promise<{ ok, version } | { ok:false, error }> npm view create-pmos version
|
|
7
7
|
* compare(st, latestVersion) → { appBehind, kitBehind }
|
|
8
8
|
* updateKit(root, { version, exec }) → Promise<{ ok, output, kit } | { ok:false, error, output }>
|
|
9
|
-
* upgradeApp(root, { version, logFile, spawn }) → { ok, log } | { ok:false, error }
|
|
9
|
+
* upgradeApp(root, { version, logFile, spawn, env }) → { ok, log } | { ok:false, error }
|
|
10
|
+
* updateEnv(env, nodePath) → env with the folder of this node on PATH (added last when absent)
|
|
11
|
+
* whereFrom({ desktop, onlineHost, tokenKept }) → { ok } | { ok:false, error } may this page start the app update?
|
|
10
12
|
*
|
|
11
13
|
* The page does what the terminal does, and nothing more. Each action runs the command the README tells the PM to type:
|
|
12
14
|
* check npm view create-pmos version
|
|
@@ -133,11 +135,36 @@ async function updateKit(root, { version, exec = cp.execFile } = {}) {
|
|
|
133
135
|
return { ok: true, output, kit: readKit(root) };
|
|
134
136
|
}
|
|
135
137
|
|
|
138
|
+
/**
|
|
139
|
+
* May the page that pressed Update the workspace start it? The update stops this server and starts a new one with the
|
|
140
|
+
* same environment (create-pmos and the pmos launcher pass it on), so the page can come back when its way in survives:
|
|
141
|
+
* desktop a direct loopback request on the computer that runs the workspace
|
|
142
|
+
* onlineHost the Online address (D89): the switch is kept in the state folder and the Access sign-in is the edge's
|
|
143
|
+
* tokenKept a phone or public address signed in with a token set in PMOS_TOKEN: the new server keeps that token
|
|
144
|
+
* A token the server generated at start is new after the restart, so a page signed in with it cannot come back.
|
|
145
|
+
*/
|
|
146
|
+
function whereFrom({ desktop = false, onlineHost = false, tokenKept = false } = {}) {
|
|
147
|
+
if (desktop || onlineHost || tokenKept) return { ok: true };
|
|
148
|
+
return { ok: false, error: 'This page signed in with a token that changes when the workspace restarts, so it could not come back. Update the workspace on the computer it runs on, or at its Online address' };
|
|
149
|
+
}
|
|
150
|
+
|
|
136
151
|
/**
|
|
137
152
|
* Start the app reinstall and return at once. The child is detached from this server, because create-pmos stops this
|
|
138
153
|
* server before it reinstalls, and it starts the new one after. Its output goes to `logFile`.
|
|
139
154
|
*/
|
|
140
|
-
|
|
155
|
+
/**
|
|
156
|
+
* The environment of the app update. A server started by a login item or a desktop shortcut can have a PATH without
|
|
157
|
+
* the node that runs it, and then neither npx nor the pmos launcher is found. The folder of this node is added at the
|
|
158
|
+
* end, so a PATH that already finds them is not changed.
|
|
159
|
+
*/
|
|
160
|
+
function updateEnv(env = process.env, nodePath = process.execPath) {
|
|
161
|
+
const key = Object.keys(env).find((k) => k.toUpperCase() === 'PATH') || 'PATH';
|
|
162
|
+
const dir = path.dirname(String(nodePath));
|
|
163
|
+
const parts = String(env[key] || '').split(path.delimiter).filter(Boolean);
|
|
164
|
+
return parts.includes(dir) ? { ...env } : { ...env, [key]: [...parts, dir].join(path.delimiter) };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function upgradeApp(root, { version, logFile, spawn = cp.spawn, env = updateEnv() } = {}) {
|
|
141
168
|
if (!validVersion(version)) return { ok: false, error: 'Check for updates first' };
|
|
142
169
|
let fd;
|
|
143
170
|
try {
|
|
@@ -147,8 +174,11 @@ function upgradeApp(root, { version, logFile, spawn = cp.spawn } = {}) {
|
|
|
147
174
|
} catch (e) { return { ok: false, error: `Could not write the log file: ${e.message}` }; }
|
|
148
175
|
try {
|
|
149
176
|
const child = spawn(NPX, ['-y', `${PKG_NAME}@${version}`, 'workspace', '--reinstall', String(root)],
|
|
150
|
-
{ cwd: String(root), detached: true, stdio: ['ignore', fd, fd], windowsHide: true, shell: IS_WIN });
|
|
151
|
-
|
|
177
|
+
{ cwd: String(root), env, detached: true, stdio: ['ignore', fd, fd], windowsHide: true, shell: IS_WIN });
|
|
178
|
+
// A child that could not start writes nothing itself: say why in the log, which the page names when it gives up.
|
|
179
|
+
if (child && typeof child.on === 'function') {
|
|
180
|
+
child.on('error', (e) => { try { fs.appendFileSync(logFile, `The update could not start: ${e.message}\n`); } catch { /* the page times out */ } });
|
|
181
|
+
}
|
|
152
182
|
if (child && typeof child.unref === 'function') child.unref();
|
|
153
183
|
return { ok: true, log: logFile };
|
|
154
184
|
} catch (e) {
|
|
@@ -158,4 +188,4 @@ function upgradeApp(root, { version, logFile, spawn = cp.spawn } = {}) {
|
|
|
158
188
|
}
|
|
159
189
|
}
|
|
160
190
|
|
|
161
|
-
module.exports = { PKG_NAME, cmpVer, validVersion, readKit, fromCheckout, status, compare, latest, updateKit, upgradeApp };
|
|
191
|
+
module.exports = { PKG_NAME, cmpVer, validVersion, readKit, fromCheckout, status, compare, latest, updateKit, updateEnv, upgradeApp, whereFrom };
|
package/workspace/package.json
CHANGED
|
@@ -9,9 +9,10 @@
|
|
|
9
9
|
* Online the Online switch and its five settings (GET and POST /api/online; workspace-online-mode, D89)
|
|
10
10
|
* Theme Dark or Light
|
|
11
11
|
* Updates the workspace app's version and the current repository's kit version; Check for updates asks the
|
|
12
|
-
* npm registry for the latest create-pmos (POST /api/kit/check, the PM's press only)
|
|
13
|
-
*
|
|
14
|
-
* (workspace-kit-update). The
|
|
12
|
+
* npm registry for the latest create-pmos (POST /api/kit/check, the PM's press only). One Update
|
|
13
|
+
* button runs what `npx create-pmos@latest --update` runs when the kit is behind, then what
|
|
14
|
+
* `pmos upgrade` runs when the app is behind (workspace-kit-update). The app update restarts the
|
|
15
|
+
* server, so the page waits and reloads on the new version.
|
|
15
16
|
* The work itself (what needs you, initiatives, items, reports) is never here.
|
|
16
17
|
*
|
|
17
18
|
* The mount contract (registry.js): mountDestination(host, ctx) is called on every render with the same host. The page
|
|
@@ -259,13 +260,17 @@ async function checkUpdates(ws) {
|
|
|
259
260
|
} else say({ phase: null, error: (j && j.error) || P.updateFailed });
|
|
260
261
|
}
|
|
261
262
|
|
|
263
|
+
/** Update the kit in this repository. Returns true when it finished. */
|
|
262
264
|
async function updateKit(ws) {
|
|
263
265
|
say({ phase: 'kit', message: '', error: '', output: '' });
|
|
264
266
|
const j = await pmPost('/api/kit/update', { ws, version: upd.latest });
|
|
265
267
|
if (j && j.ok !== false) {
|
|
266
268
|
kitByWs.set(ws, { st: j, at: Date.now() });
|
|
267
269
|
say({ phase: null, compare: { ...(upd.compare || {}), kitBehind: false }, message: P.updateKitDone((j.kit && j.kit.version) || upd.latest), output: j.output || '' });
|
|
268
|
-
|
|
270
|
+
return true;
|
|
271
|
+
}
|
|
272
|
+
say({ phase: null, error: (j && j.error) || P.updateFailed, output: (j && j.output) || '' });
|
|
273
|
+
return false;
|
|
269
274
|
}
|
|
270
275
|
|
|
271
276
|
const pause = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
@@ -284,9 +289,21 @@ async function waitForRestart(from, log) {
|
|
|
284
289
|
say({ phase: null, error: P.updateAppLost(log) });
|
|
285
290
|
}
|
|
286
291
|
|
|
292
|
+
/**
|
|
293
|
+
* The one Update button: the kit first when it is behind, then the app when it is behind. The app update stops this
|
|
294
|
+
* server, installs the new version, starts it again, and this page reloads on it. A kit update alone changes no page.
|
|
295
|
+
*/
|
|
296
|
+
async function updateAll(ws, app, cmp) {
|
|
297
|
+
const appToo = !!cmp.appBehind && !app.fromCheckout;
|
|
298
|
+
if (appToo) {
|
|
299
|
+
const yes = await askConfirm({ title: P.updateAppTitle, body: P.updateAppBody(app.version || '?', upd.latest), confirmLabel: P.updateAll });
|
|
300
|
+
if (!yes) return;
|
|
301
|
+
}
|
|
302
|
+
if (cmp.kitBehind && !(await updateKit(ws))) return;
|
|
303
|
+
if (appToo) await updateApp(ws, app.version);
|
|
304
|
+
}
|
|
305
|
+
|
|
287
306
|
async function updateApp(ws, from) {
|
|
288
|
-
const yes = await askConfirm({ title: P.updateAppTitle, body: P.updateAppBody(from || '?', upd.latest), confirmLabel: P.updateApp });
|
|
289
|
-
if (!yes) return;
|
|
290
307
|
say({ phase: 'app', message: P.updateAppBusy, error: '', output: '' });
|
|
291
308
|
const j = await pmPost('/api/kit/upgrade-app', { ws, version: upd.latest });
|
|
292
309
|
if (!j || j.ok === false) { say({ phase: null, message: '', error: (j && j.error) || P.updateFailed }); return; }
|
|
@@ -303,19 +320,18 @@ function updatesSection(scope) {
|
|
|
303
320
|
const busy = !!upd.phase;
|
|
304
321
|
const cmp = upd.compare || {};
|
|
305
322
|
const kids = [];
|
|
306
|
-
const
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
onClick: () => updateApp(ws, app.version),
|
|
310
|
-
}) : null);
|
|
311
|
-
kids.push(appLine);
|
|
323
|
+
const kitPresent = !!(kit && kit.present);
|
|
324
|
+
const canUpdate = (cmp.appBehind && !app.fromCheckout) || (cmp.kitBehind && kitPresent);
|
|
325
|
+
kids.push(el('div', { class: 'fr-set__line' }, el('span', { 'data-fr': 'app-version' }, P.updatesApp(app.version))));
|
|
312
326
|
if (cmp.appBehind && app.fromCheckout) kids.push(el('p', { class: 'fr-set__note', 'data-fr': 'app-checkout' }, P.updatesCheckout));
|
|
313
327
|
const kitText = scope === 'all' || !ws ? P.updatesNoRepo : !kit ? '' : kit.present ? P.updatesKit(kit.version) : P.updatesNoKit;
|
|
314
|
-
kids.push(el('div', { class: 'fr-set__line' }, el('span', { 'data-fr': 'kit-version' }, kitText)
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
328
|
+
kids.push(el('div', { class: 'fr-set__line' }, el('span', { 'data-fr': 'kit-version' }, kitText)));
|
|
329
|
+
if (canUpdate) {
|
|
330
|
+
kids.push(el('div', { class: 'fr-set__line' }, button({
|
|
331
|
+
label: P.updateAllTo(upd.latest), variant: 'primary', size: 'sm', disabled: busy, attrs: { 'data-fr': 'update-all' },
|
|
332
|
+
onClick: () => updateAll(ws, app, { appBehind: !!cmp.appBehind, kitBehind: !!cmp.kitBehind && kitPresent }),
|
|
333
|
+
})));
|
|
334
|
+
}
|
|
319
335
|
const line = upd.phase === 'check' ? P.updatesChecking : upd.phase === 'kit' ? P.updateKitBusy
|
|
320
336
|
: upd.message ? upd.message : upd.latest ? (cmp.appBehind || cmp.kitBehind ? P.updatesLatest(upd.latest) : P.updatesCurrent(upd.latest)) : P.updatesNever;
|
|
321
337
|
kids.push(el('p', { class: 'fr-set__note', role: 'status', 'aria-live': 'polite', 'data-fr': 'update-state' }, line));
|
|
@@ -194,12 +194,12 @@ export const COPY = {
|
|
|
194
194
|
updatesCurrent: (v) => 'Up to date. The latest published version is ' + v + '.',
|
|
195
195
|
updatesCheckout: 'This workspace runs from a PMOS checkout. Update it with git pull.',
|
|
196
196
|
updatesNote: 'Updating the kit changes only its own files (skills, scripts, templates and guides). Your records in state/, planning/ and okf/product/ do not change.',
|
|
197
|
-
updateKit: 'Update the kit',
|
|
198
197
|
updateKitBusy: 'Updating the kit…',
|
|
199
198
|
updateKitDone: (v) => 'The kit is now ' + v + '. The changed files are under pmos/, and Sync sends them with your other records.',
|
|
200
|
-
|
|
199
|
+
updateAll: 'Update',
|
|
200
|
+
updateAllTo: (v) => 'Update to ' + v,
|
|
201
201
|
updateAppTitle: 'Update the workspace?',
|
|
202
|
-
updateAppBody: (from, to) => 'The workspace stops, installs ' + to + ' in place of ' + from + ', and starts again. This takes about a minute. Sessions keep running in tmux.\n\nThis page reloads when the new version starts.',
|
|
202
|
+
updateAppBody: (from, to) => 'The workspace updates the kit in this repository when it is behind, then stops, installs ' + to + ' in place of ' + from + ', and starts again. This takes about a minute. Sessions keep running in tmux.\n\nThis page reloads when the new version starts. You do not need a terminal.',
|
|
203
203
|
updateAppBusy: 'Updating the workspace. This page reloads when the new version starts.',
|
|
204
204
|
updateAppSame: (v, log) => 'The workspace started again on ' + v + ', so the update did not finish. The update log is ' + log + '.',
|
|
205
205
|
updateAppLost: (log) => 'The workspace did not come back on this address. Type pmos in a terminal to open it. The update log is ' + log + '.',
|
package/workspace/server.js
CHANGED
|
@@ -7017,11 +7017,14 @@ async function handleRequest(req, res) {
|
|
|
7017
7017
|
return json(res, { ws: w.id, ...r, ...st() }, r.ok ? 200 : 502);
|
|
7018
7018
|
} finally { kitBusy = null; }
|
|
7019
7019
|
}
|
|
7020
|
-
// The app update stops this server and starts a new one
|
|
7021
|
-
//
|
|
7022
|
-
|
|
7023
|
-
|
|
7024
|
-
|
|
7020
|
+
// The app update stops this server and starts a new one, so the page must be able to come back (kitUpdate.whereFrom).
|
|
7021
|
+
// It is refused from a checkout, which git updates.
|
|
7022
|
+
const where = kitUpdate.whereFrom({
|
|
7023
|
+
desktop: remoteAccess.isLoopbackAddr(req.socket.remoteAddress) && !remoteAccess.isProxied(req.headers, publicHost()),
|
|
7024
|
+
onlineHost: viaOnlineHost(req.headers),
|
|
7025
|
+
tokenKept: !!TOKEN && !BIND.generated,
|
|
7026
|
+
});
|
|
7027
|
+
if (!where.ok) return json(res, { ok: false, error: where.error }, 403);
|
|
7025
7028
|
if (st().app.fromCheckout) return json(res, { ok: false, error: 'This workspace runs from a PMOS checkout. Update it with git pull' }, 409);
|
|
7026
7029
|
const r = kitUpdate.upgradeApp(w.root, { version: body.version, logFile: path.join(STATE, 'app-update.log') });
|
|
7027
7030
|
if (!r.ok) return json(res, { ok: false, error: r.error }, 400);
|
|
@@ -87,6 +87,7 @@
|
|
|
87
87
|
"pm-r26-test.js",
|
|
88
88
|
"pm-doc-category-test.js",
|
|
89
89
|
"pm-r27-test.js",
|
|
90
|
+
"pm-updates-test.js",
|
|
90
91
|
"pm-r28-test.js",
|
|
91
92
|
"pm-r29-today-test.js",
|
|
92
93
|
"pm-r29-portfolio-test.js",
|
|
@@ -180,6 +181,7 @@
|
|
|
180
181
|
"pm-r26-test.js": 9,
|
|
181
182
|
"pm-doc-category-test.js": 4,
|
|
182
183
|
"pm-r27-test.js": 26,
|
|
184
|
+
"pm-updates-test.js": 12,
|
|
183
185
|
"pm-r28-test.js": 9,
|
|
184
186
|
"pm-r29-today-test.js": 4,
|
|
185
187
|
"pm-r29-portfolio-test.js": 3,
|