create-pmos 0.60.1 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-pmos",
3
- "version": "0.60.1",
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": {
@@ -1,3 +1,3 @@
1
- kernel-source: pmos@63010ef
1
+ kernel-source: pmos@d57effd
2
2
  assembled: 2026-10-06
3
3
  mode: vendor
@@ -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:28:53Z"}
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:28:53Z"}
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:28:53Z"}
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, a button shows for each part that is older:
37
- - **Update the kit** 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
- - **Update the workspace** does what `pmos upgrade` does in a terminal. It asks you first. The workspace stops, installs the new version and starts again, and the page reloads when the new version starts. 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.
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,8 @@
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)
10
11
  * whereFrom({ desktop, onlineHost, tokenKept }) → { ok } | { ok:false, error } may this page start the app update?
11
12
  *
12
13
  * The page does what the terminal does, and nothing more. Each action runs the command the README tells the PM to type:
@@ -151,7 +152,19 @@ function whereFrom({ desktop = false, onlineHost = false, tokenKept = false } =
151
152
  * Start the app reinstall and return at once. The child is detached from this server, because create-pmos stops this
152
153
  * server before it reinstalls, and it starts the new one after. Its output goes to `logFile`.
153
154
  */
154
- function upgradeApp(root, { version, logFile, spawn = cp.spawn } = {}) {
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() } = {}) {
155
168
  if (!validVersion(version)) return { ok: false, error: 'Check for updates first' };
156
169
  let fd;
157
170
  try {
@@ -161,8 +174,11 @@ function upgradeApp(root, { version, logFile, spawn = cp.spawn } = {}) {
161
174
  } catch (e) { return { ok: false, error: `Could not write the log file: ${e.message}` }; }
162
175
  try {
163
176
  const child = spawn(NPX, ['-y', `${PKG_NAME}@${version}`, 'workspace', '--reinstall', String(root)],
164
- { cwd: String(root), detached: true, stdio: ['ignore', fd, fd], windowsHide: true, shell: IS_WIN });
165
- if (child && typeof child.on === 'function') child.on('error', () => { /* reported in the log by npx, or the page times out */ });
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
+ }
166
182
  if (child && typeof child.unref === 'function') child.unref();
167
183
  return { ok: true, log: logFile };
168
184
  } catch (e) {
@@ -172,4 +188,4 @@ function upgradeApp(root, { version, logFile, spawn = cp.spawn } = {}) {
172
188
  }
173
189
  }
174
190
 
175
- module.exports = { PKG_NAME, cmpVer, validVersion, readKit, fromCheckout, status, compare, latest, updateKit, upgradeApp, whereFrom };
191
+ module.exports = { PKG_NAME, cmpVer, validVersion, readKit, fromCheckout, status, compare, latest, updateKit, updateEnv, upgradeApp, whereFrom };
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pmos-workspace",
3
- "version": "0.60.1",
3
+ "version": "0.60.2",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pmos-workspace",
3
- "version": "0.60.1",
3
+ "version": "0.60.2",
4
4
  "private": true,
5
5
  "description": "PMOS Terminal Workspace — local companion hosting real PTYs beside the library and gates (D62)",
6
6
  "license": "MIT",
@@ -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), and Update the
13
- * kit / Update the workspace run what `npx create-pmos@latest --update` and `pmos upgrade` run
14
- * (workspace-kit-update). The workspace update restarts the server, so the page waits and reloads.
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
- } else say({ phase: null, error: (j && j.error) || P.updateFailed, output: (j && j.output) || '' });
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 appLine = el('div', { class: 'fr-set__line' }, el('span', { 'data-fr': 'app-version' }, P.updatesApp(app.version)),
307
- cmp.appBehind && !app.fromCheckout ? button({
308
- label: P.updateApp, variant: 'primary', size: 'sm', disabled: busy, attrs: { 'data-fr': 'update-app' },
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
- cmp.kitBehind && kit && kit.present ? button({
316
- label: P.updateKit, variant: 'primary', size: 'sm', disabled: busy, attrs: { 'data-fr': 'update-kit' },
317
- onClick: () => updateKit(ws),
318
- }) : null));
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
- updateApp: 'Update the workspace',
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 + '.',
@@ -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,