create-pmos 0.100.0 → 0.102.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/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 +1 -1
- package/workspace/lib/kit-update.js +15 -1
- package/workspace/lib/profile-discovery.js +194 -0
- package/workspace/lib/profiles.js +18 -1
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/public/pm/dest-settings.js +11 -6
- package/workspace/public/pm/frame-copy.js +1 -1
- package/workspace/public/pm/panel-cockpit.js +6 -2
- package/workspace/public/pm/panel-newsess.js +7 -1
- package/workspace/public/pm/view-notes-menu.js +2 -5
- package/workspace/server.js +6 -4
- package/workspace/test-lists/workspace-profile-detection.json +9 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-pmos",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.102.0",
|
|
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-08T00:
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-08T00:36:21Z"}
|
|
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-08T00:
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-08T00:36:21Z"}
|
|
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-08T00:
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-08T00:36:21Z"}
|
|
6
6
|
status: stable
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -30,7 +30,7 @@ Each place is drawn when its run is built. Until then the place says which run b
|
|
|
30
30
|
Settings holds settings only, in eight sections. On a wide screen they are cards in two columns, like Agenda: Repositories, New sessions, Profiles and Theme on the left, and Validate consent, People, Online and Updates on the right. On a narrow screen the cards stack in one column. Each switch (Validate consent, People, Online) turns on when you press it. **Check for updates** is at the top right of the page.
|
|
31
31
|
|
|
32
32
|
- **Repositories**: every registered repository. Press a repository to make it the current one. Press its ⋯ for **Rename** or **Remove from PMOS**. Each asks in a sheet, and a remove asks you to confirm. **Add repository** shows the scan of a folder before you add it.
|
|
33
|
-
- **Profiles**: the Claude profiles a session can start with. Press **New profile** to make one. This writes `~/.claude/profiles/<id>.json`. Account secrets are never shown in the browser.
|
|
33
|
+
- **Profiles**: the Claude profiles a session can start with. These are the descriptors in `~/.claude/profiles`, and also the profiles you already run: a shell command that sets `CLAUDE_CONFIG_DIR` (for example `alias claude-work='CLAUDE_CONFIG_DIR=~/.claude-work claude'`), a launcher script named `claude-*`, or a config folder such as `~/.claude-work`. A profile found this way says where it was found, and a session under it sets only `CLAUDE_CONFIG_DIR`. Press **New profile** to make one. This writes `~/.claude/profiles/<id>.json`. Account secrets are never shown in the browser.
|
|
34
34
|
- **New sessions**: the default CLI, the default model, the default Claude profile and the default launch flags for a new session. The flags come from `claude --help`, for example `--chrome`. A flag that needs a value lists the values it accepts.
|
|
35
35
|
- **Validate consent**: the consent of the current repository. Use its switch to turn it on or off. **Open Validate** opens the Validate page as a tab.
|
|
36
36
|
- **People**: whether the morning brief keeps attendee names from the calendar for the current repository. It is off until you turn it on. Turning it off also hides the names recorded before.
|
|
@@ -166,6 +166,20 @@ async function latest({ exec = cp.execFile, source: src = source() } = {}) {
|
|
|
166
166
|
return { ok: true, version: v };
|
|
167
167
|
}
|
|
168
168
|
|
|
169
|
+
/** Check the local repository's package version (for dev/testing without npm). Reads npm-pkg/package.json for kit. */
|
|
170
|
+
function latestLocal({ repoRoot = null } = {}) {
|
|
171
|
+
try {
|
|
172
|
+
const pkgPath = path.join(String(repoRoot || ''), 'npm-pkg', 'package.json');
|
|
173
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
174
|
+
if (!pkg.version || !validVersion(pkg.version)) {
|
|
175
|
+
return { ok: false, error: 'Local package.json has no valid version' };
|
|
176
|
+
}
|
|
177
|
+
return { ok: true, version: pkg.version, source: 'local-git' };
|
|
178
|
+
} catch (e) {
|
|
179
|
+
return { ok: false, error: `Could not read local version: ${e.message}` };
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
169
183
|
/**
|
|
170
184
|
* npm lists a new version in the check a moment before its download is served everywhere. A download asked for in that
|
|
171
185
|
* gap answers 404, and npm's CDN keeps the 404 for up to five minutes (Cache-Control max-age=300), so the same press
|
|
@@ -250,4 +264,4 @@ function upgradeApp(root, { version, logFile, spawn = cp.spawn, env = updateEnv(
|
|
|
250
264
|
}
|
|
251
265
|
}
|
|
252
266
|
|
|
253
|
-
module.exports = { PKG_NAME, cmpVer, notServedYet, validVersion, source, npxArgs, readKit, fromCheckout, status, compare, latest, updateKit, updateEnv, upgradeApp, whereFrom };
|
|
267
|
+
module.exports = { PKG_NAME, cmpVer, notServedYet, validVersion, source, npxArgs, readKit, fromCheckout, status, compare, latest, latestLocal, updateKit, updateEnv, upgradeApp, whereFrom };
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/*
|
|
3
|
+
* Detected Claude profiles — a PURE reader (initiative `workspace-profile-detection`).
|
|
4
|
+
*
|
|
5
|
+
* Most people who run Claude Code under more than one account do not keep a PMOS descriptor. They keep a second
|
|
6
|
+
* CONFIG DIRECTORY and a command that points Claude at it:
|
|
7
|
+
*
|
|
8
|
+
* alias claude-work='CLAUDE_CONFIG_DIR=~/.claude-work claude'
|
|
9
|
+
*
|
|
10
|
+
* `CLAUDE_CONFIG_DIR` is how Claude Code picks where its settings, credentials and history live, so one directory is
|
|
11
|
+
* one profile. PMOS can start a session under such a profile with the same environment overlay a descriptor gives:
|
|
12
|
+
* the one variable `CLAUDE_CONFIG_DIR`. This module finds them. It reads three places under the home directory and
|
|
13
|
+
* writes nothing, runs nothing and never opens a credential file:
|
|
14
|
+
*
|
|
15
|
+
* 1. shell start-up files and fish functions — an `alias`, a function or a one-line command that sets
|
|
16
|
+
* `CLAUDE_CONFIG_DIR` and runs `claude`. The alias name is the profile id.
|
|
17
|
+
* 2. launcher scripts named `claude-*` in the usual bin directories that set `CLAUDE_CONFIG_DIR`.
|
|
18
|
+
* 3. config directories that look like Claude's own — `~/.claude-<name>`, `~/.claude_<name>`,
|
|
19
|
+
* `~/.config/claude-<name>` — recognised by a file or folder only Claude writes there. The id is
|
|
20
|
+
* `claude-<name>`.
|
|
21
|
+
*
|
|
22
|
+
* Every result names WHERE it was found (`source`), because a detected profile is a guess from files PMOS did not
|
|
23
|
+
* write, and the surface must say so. The overlay is exactly `{ CLAUDE_CONFIG_DIR: <absolute path> }`: any other
|
|
24
|
+
* variable an alias sets (a key, a proxy) is left alone, and the surface says only one variable is applied.
|
|
25
|
+
*
|
|
26
|
+
* A descriptor under `~/.claude/profiles` always wins over a detected profile of the same id (see profiles.js).
|
|
27
|
+
*/
|
|
28
|
+
const fs = require('fs');
|
|
29
|
+
const path = require('path');
|
|
30
|
+
|
|
31
|
+
const ID_RE = /^[a-z0-9][a-z0-9._-]{0,63}$/;
|
|
32
|
+
/** Something only Claude Code writes into its config directory. A folder with none of these is not offered. */
|
|
33
|
+
const CONFIG_MARKERS = ['settings.json', '.claude.json', '.credentials.json', 'projects', 'CLAUDE.md', 'commands', 'agents', 'todos', 'statsig', 'shell-snapshots'];
|
|
34
|
+
const RC_FILES = ['.zshrc', '.zprofile', '.zshenv', '.zlogin', '.bashrc', '.bash_profile', '.bash_login', '.profile', '.aliases', '.bash_aliases', '.config/fish/config.fish'];
|
|
35
|
+
const FISH_FUNCTIONS = '.config/fish/functions';
|
|
36
|
+
const BIN_DIRS = ['.local/bin', 'bin', '.bin', '.config/claude/bin'];
|
|
37
|
+
const MAX_TEXT = 256 * 1024; // a start-up file larger than this is not read
|
|
38
|
+
const MAX_SCRIPT = 16 * 1024; // a launcher script larger than this is a binary or something else
|
|
39
|
+
|
|
40
|
+
const safeRead = (file, max) => {
|
|
41
|
+
try {
|
|
42
|
+
const st = fs.statSync(file);
|
|
43
|
+
if (!st.isFile() || st.size > max) return null;
|
|
44
|
+
return fs.readFileSync(file, 'utf8');
|
|
45
|
+
} catch { return null; }
|
|
46
|
+
};
|
|
47
|
+
const isDir = (p) => { try { return fs.statSync(p).isDirectory(); } catch { return false; } };
|
|
48
|
+
|
|
49
|
+
/** `~`, `$HOME` and `${HOME}` at the start of a path, resolved against `home`; quotes removed. null when the value is
|
|
50
|
+
* not a plain path (it holds another variable, a command substitution or a glob). */
|
|
51
|
+
function resolveDir(raw, home) {
|
|
52
|
+
let v = String(raw || '').trim().replace(/^["']|["']$/g, '');
|
|
53
|
+
if (!v) return null;
|
|
54
|
+
v = v.replace(/^\$\{?HOME\}?(?=\/|$)/, home).replace(/^~(?=\/|$)/, home);
|
|
55
|
+
if (/[$`*?]|\$\(/.test(v)) return null;
|
|
56
|
+
if (!path.isAbsolute(v)) return null;
|
|
57
|
+
return path.normalize(v).replace(/\/+$/, '') || null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** The value after `CLAUDE_CONFIG_DIR=` in one line of shell: quoted or bare. */
|
|
61
|
+
function configDirOf(line, home) {
|
|
62
|
+
const m = /CLAUDE_CONFIG_DIR=(?:"([^"]*)"|'([^']*)'|([^\s;'"`)}]+))/.exec(line);
|
|
63
|
+
if (!m) return null;
|
|
64
|
+
return resolveDir(m[1] != null ? m[1] : m[2] != null ? m[2] : m[3], home);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** `claude` run as a command: after the assignment, as a word of its own (not part of the alias name `claude-work`). */
|
|
68
|
+
const runsClaude = (s) => /(^|[\s;&|(])(?:command\s+|exec\s+|env\s+)?(?:\S*\/)?claude(?=[\s;&|)'"}]|$)/.test(s);
|
|
69
|
+
|
|
70
|
+
/** Names defined on one line of a shell start-up file: `alias n=…`, `n() { … }`, `function n …`, fish `alias n '…'`. */
|
|
71
|
+
function definedNames(line) {
|
|
72
|
+
let m = /^\s*alias\s+(?:-\w+\s+)?([A-Za-z0-9._-]+)(?:=|\s)/.exec(line);
|
|
73
|
+
if (m) return m[1];
|
|
74
|
+
m = /^\s*(?:function\s+)?([A-Za-z0-9._-]+)\s*\(\s*\)\s*\{/.exec(line);
|
|
75
|
+
if (m) return m[1];
|
|
76
|
+
m = /^\s*function\s+([A-Za-z0-9._-]+)/.exec(line);
|
|
77
|
+
return m ? m[1] : null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Profiles named in the text of one start-up file. `file` and the line number are kept as provenance. */
|
|
81
|
+
function fromShellText(text, file, home) {
|
|
82
|
+
const out = [];
|
|
83
|
+
const lines = text.split(/\r?\n/);
|
|
84
|
+
for (let i = 0; i < lines.length; i++) {
|
|
85
|
+
const line = lines[i];
|
|
86
|
+
if (!line.includes('CLAUDE_CONFIG_DIR') || /^\s*#/.test(line)) continue;
|
|
87
|
+
if (/^\s*(?:export|set\s+-\w*x|declare|typeset)\b/.test(line)) continue; // a plain export names no command
|
|
88
|
+
let name = definedNames(line);
|
|
89
|
+
let tail = [line];
|
|
90
|
+
// a function written over several lines: the name is on an earlier line, the command on this one or the next two
|
|
91
|
+
if (!name) {
|
|
92
|
+
for (let j = i - 1; j >= 0 && j >= i - 4 && !name; j--) {
|
|
93
|
+
if (/^\s*(?:\}|end\b)/.test(lines[j])) break;
|
|
94
|
+
if ((/\{\s*$/.test(lines[j]) || /^\s*function\s/.test(lines[j])) && definedNames(lines[j])) name = definedNames(lines[j]);
|
|
95
|
+
}
|
|
96
|
+
tail = lines.slice(i, i + 3).filter((l) => !/^\s*#/.test(l));
|
|
97
|
+
}
|
|
98
|
+
const dir = configDirOf(line, home);
|
|
99
|
+
const assign = line.indexOf('CLAUDE_CONFIG_DIR=');
|
|
100
|
+
const first = (assign < 0 ? line : line.slice(assign + 'CLAUDE_CONFIG_DIR='.length)).replace(/^(?:"[^"]*"|'[^']*'|[^\s;'"`)}]+)/, '');
|
|
101
|
+
const after = [first, ...tail.slice(1)].join('\n');
|
|
102
|
+
if (!name || !dir || !runsClaude(after)) continue;
|
|
103
|
+
out.push({ id: name, dir, source: 'shell', from: file, line: i + 1 });
|
|
104
|
+
}
|
|
105
|
+
return out;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Launcher scripts: `claude-work` on PATH whose text sets CLAUDE_CONFIG_DIR. */
|
|
109
|
+
function fromLauncher(file, home) {
|
|
110
|
+
const text = safeRead(file, MAX_SCRIPT);
|
|
111
|
+
if (!text || text.includes('\0') || !text.includes('CLAUDE_CONFIG_DIR')) return null;
|
|
112
|
+
for (const line of text.split(/\r?\n/)) {
|
|
113
|
+
if (/^\s*#/.test(line)) continue;
|
|
114
|
+
const dir = configDirOf(line, home);
|
|
115
|
+
if (dir) return { id: path.basename(file), dir, source: 'launcher', from: file, line: null };
|
|
116
|
+
}
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function looksLikeConfigDir(dir) {
|
|
121
|
+
return CONFIG_MARKERS.some((m) => { try { fs.accessSync(path.join(dir, m)); return true; } catch { return false; } });
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** `~/.claude-work`, `~/.claude_work`, `~/.config/claude-work` → `claude-work`. */
|
|
125
|
+
function fromConfigDirs(home) {
|
|
126
|
+
const out = [];
|
|
127
|
+
const scan = (parent, re) => {
|
|
128
|
+
let names = [];
|
|
129
|
+
try { names = fs.readdirSync(parent); } catch { return; }
|
|
130
|
+
for (const n of names.slice().sort()) {
|
|
131
|
+
const m = re.exec(n);
|
|
132
|
+
if (!m) continue;
|
|
133
|
+
const dir = path.join(parent, n);
|
|
134
|
+
if (!isDir(dir) || !looksLikeConfigDir(dir)) continue;
|
|
135
|
+
out.push({ id: ('claude-' + m[1]).toLowerCase(), dir, source: 'config-dir', from: dir, line: null });
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
scan(home, /^\.claude[-_.]([A-Za-z0-9][A-Za-z0-9._-]*)$/);
|
|
139
|
+
scan(path.join(home, '.config'), /^claude[-_]([A-Za-z0-9][A-Za-z0-9._-]*)$/);
|
|
140
|
+
return out;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* detectProfiles(homeDir) → [{ id, label, account:null, mcp:[], env:{CLAUDE_CONFIG_DIR}, path, detected:{source, from, line} }]
|
|
145
|
+
* Sorted by id. When one id turns up in several places the shell definition wins over a launcher, a launcher over a bare
|
|
146
|
+
* directory: the command is what the PM types, so its directory is the one that is meant. A profile whose directory does
|
|
147
|
+
* not exist is dropped (the command would start Claude on an empty home, which is a new login, not a profile).
|
|
148
|
+
*/
|
|
149
|
+
function detectProfiles(homeDir) {
|
|
150
|
+
if (typeof homeDir !== 'string' || !homeDir) return [];
|
|
151
|
+
const found = [];
|
|
152
|
+
|
|
153
|
+
for (const rel of RC_FILES) {
|
|
154
|
+
const file = path.join(homeDir, rel);
|
|
155
|
+
const text = safeRead(file, MAX_TEXT);
|
|
156
|
+
if (text) found.push(...fromShellText(text, file, homeDir));
|
|
157
|
+
}
|
|
158
|
+
const fishDir = path.join(homeDir, FISH_FUNCTIONS);
|
|
159
|
+
try {
|
|
160
|
+
for (const n of fs.readdirSync(fishDir).sort()) {
|
|
161
|
+
if (!n.endsWith('.fish')) continue;
|
|
162
|
+
const file = path.join(fishDir, n);
|
|
163
|
+
const text = safeRead(file, MAX_SCRIPT);
|
|
164
|
+
if (!text || !text.includes('CLAUDE_CONFIG_DIR')) continue;
|
|
165
|
+
const dir = configDirOf(text, homeDir);
|
|
166
|
+
if (dir) found.push({ id: n.slice(0, -5), dir, source: 'shell', from: file, line: null });
|
|
167
|
+
}
|
|
168
|
+
} catch { /* no fish functions */ }
|
|
169
|
+
for (const rel of BIN_DIRS) {
|
|
170
|
+
const bin = path.join(homeDir, rel);
|
|
171
|
+
let names = [];
|
|
172
|
+
try { names = fs.readdirSync(bin); } catch { continue; }
|
|
173
|
+
for (const n of names.slice().sort()) {
|
|
174
|
+
if (!/^claude[-_.]/.test(n)) continue;
|
|
175
|
+
const hit = fromLauncher(path.join(bin, n), homeDir);
|
|
176
|
+
if (hit) found.push(hit);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
found.push(...fromConfigDirs(homeDir));
|
|
180
|
+
|
|
181
|
+
const rank = { shell: 0, launcher: 1, 'config-dir': 2 };
|
|
182
|
+
const byId = new Map();
|
|
183
|
+
for (const f of found) {
|
|
184
|
+
if (!ID_RE.test(f.id) || f.id === 'claude' || !isDir(f.dir)) continue;
|
|
185
|
+
const cur = byId.get(f.id);
|
|
186
|
+
if (!cur || rank[f.source] < rank[cur.source]) byId.set(f.id, f);
|
|
187
|
+
}
|
|
188
|
+
return [...byId.values()].sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)).map((f) => ({
|
|
189
|
+
id: f.id, label: f.id, account: null, mcp: [], env: { CLAUDE_CONFIG_DIR: f.dir },
|
|
190
|
+
path: f.from, detected: { source: f.source, from: f.from, line: f.line },
|
|
191
|
+
}));
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
module.exports = { detectProfiles, resolveDir, configDirOf, fromShellText, ID_RE };
|
|
@@ -151,7 +151,7 @@ function validate(raw, stem) {
|
|
|
151
151
|
* with no home directory gets a stated failure; guessing one would be the same class of invention
|
|
152
152
|
* this whole module refuses.
|
|
153
153
|
*/
|
|
154
|
-
function
|
|
154
|
+
function readDescriptors(homeDir) {
|
|
155
155
|
if (typeof homeDir !== 'string' || !homeDir) {
|
|
156
156
|
return {
|
|
157
157
|
ok: false, absent: false, dir: null, profiles: [], broken: [],
|
|
@@ -212,6 +212,23 @@ function readProfiles(homeDir) {
|
|
|
212
212
|
return { ok: true, absent: false, dir, profiles, broken, error: null };
|
|
213
213
|
}
|
|
214
214
|
|
|
215
|
+
/*
|
|
216
|
+
* readProfiles(homeDir, { detect }) — the descriptors above, and with `detect: true` also the profiles PMOS can SEE but
|
|
217
|
+
* the PM never wrote down: a config directory plus the command that points Claude at it (lib/profile-discovery.js).
|
|
218
|
+
* A detected profile carries `detected: { source, from, line }` so every surface can say where it came from. A descriptor
|
|
219
|
+
* of the same id always wins, and the four-way discrimination above is about the descriptor directory only: `absent`
|
|
220
|
+
* stays true when that directory is missing even though detected profiles are listed.
|
|
221
|
+
*/
|
|
222
|
+
function readProfiles(homeDir, opts) {
|
|
223
|
+
const read = readDescriptors(homeDir);
|
|
224
|
+
if (!opts || !opts.detect || typeof homeDir !== 'string' || !homeDir) return read;
|
|
225
|
+
let found = [];
|
|
226
|
+
try { found = require('./profile-discovery').detectProfiles(homeDir); } catch { found = []; }
|
|
227
|
+
const taken = new Set([...read.profiles.map((p) => p.id), ...read.broken.map((b) => b.file.replace(/\.json$/, ''))]);
|
|
228
|
+
const extra = found.filter((p) => !taken.has(p.id));
|
|
229
|
+
return extra.length ? { ...read, profiles: [...read.profiles, ...extra] } : read;
|
|
230
|
+
}
|
|
231
|
+
|
|
215
232
|
/*
|
|
216
233
|
* The one line naming what a pane is running under — PRD R6, verbatim sentences.
|
|
217
234
|
*
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pmos-workspace",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.102.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "pmos-workspace",
|
|
9
|
-
"version": "0.
|
|
9
|
+
"version": "0.102.0",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@xterm/addon-fit": "^0.11.0",
|
package/workspace/package.json
CHANGED
|
@@ -189,7 +189,7 @@ function profilesSection() {
|
|
|
189
189
|
if (sess.msg) kids.push(el('p', { class: 'fr-set__note', role: 'status', 'data-fr': 'profiles-msg' }, sess.msg));
|
|
190
190
|
kids.push(el('p', { class: 'fr-set__note', 'data-fr': 'profiles-found' }, !sess.profiles ? P.profilesReading
|
|
191
191
|
: sess.profiles.ok === false ? P.profilesUnread(sess.profiles.error)
|
|
192
|
-
: found.length ? P.profilesFound(found.map((x) => x.id)) : P.profilesNone(sess.profiles.dir)));
|
|
192
|
+
: found.length ? P.profilesFound(found.map((x) => x.id), found.filter((x) => x.detected).map((x) => x.id)) : P.profilesNone(sess.profiles.dir)));
|
|
193
193
|
const add = button({ label: P.profileNew, variant: 'outline', size: 'sm', icon: 'plus', attrs: { 'data-fr': 'profile-new', 'aria-expanded': sess.formOpen ? 'true' : 'false' },
|
|
194
194
|
onClick: () => { sess.formOpen = !sess.formOpen; sess.msg = ''; setState({}); } });
|
|
195
195
|
return section('profiles', P.profiles, kids, add, 'bot');
|
|
@@ -395,12 +395,12 @@ function readKitStatus(ws, force) {
|
|
|
395
395
|
|
|
396
396
|
const say = (fields) => { Object.assign(upd, fields); setState({}); };
|
|
397
397
|
|
|
398
|
-
async function checkUpdates(ws) {
|
|
398
|
+
async function checkUpdates(ws, local) {
|
|
399
399
|
say({ phase: 'check', message: '', error: '', output: '' });
|
|
400
|
-
const j = await pmPost('/api/kit/check', { ws });
|
|
400
|
+
const j = await pmPost('/api/kit/check', { ws, local });
|
|
401
401
|
if (j && j.ok !== false) {
|
|
402
402
|
kitByWs.set(ws, { st: j, at: Date.now() });
|
|
403
|
-
say({ phase: null, latest: j.latest, compare: { appBehind: !!j.appBehind, kitBehind: !!j.kitBehind } });
|
|
403
|
+
say({ phase: null, latest: j.latest, compare: { appBehind: !!j.appBehind, kitBehind: !!j.kitBehind }, source: j.source });
|
|
404
404
|
} else say({ phase: null, error: (j && j.error) || P.updateFailed });
|
|
405
405
|
}
|
|
406
406
|
|
|
@@ -480,9 +480,14 @@ function updatesSection(scope) {
|
|
|
480
480
|
onClick: () => updateAll(ws, app, { appBehind: !!cmp.appBehind, kitBehind: !!cmp.kitBehind && kitPresent }),
|
|
481
481
|
})));
|
|
482
482
|
}
|
|
483
|
+
kids.push(el('div', { class: 'fr-set__line' }, el('label', { class: 'fr-set__checkbox' },
|
|
484
|
+
el('input', { type: 'checkbox', checked: !!upd.checkLocal, onChange: (e) => { upd.checkLocal = e.target.checked; setState({}); } }),
|
|
485
|
+
el('span', null, 'Check local git version')
|
|
486
|
+
)));
|
|
483
487
|
const line = upd.phase === 'check' ? P.updatesChecking : upd.phase === 'kit' ? P.updateKitBusy
|
|
484
488
|
: upd.message ? upd.message : upd.latest ? (cmp.appBehind || cmp.kitBehind ? P.updatesLatest(upd.latest) : P.updatesCurrent(upd.latest)) : P.updatesNever;
|
|
485
|
-
|
|
489
|
+
const sourceNote = upd.source === 'local-git' ? ' (from local git)' : upd.source ? ` (from ${upd.source})` : '';
|
|
490
|
+
kids.push(el('p', { class: 'fr-set__note', role: 'status', 'aria-live': 'polite', 'data-fr': 'update-state' }, line + sourceNote));
|
|
486
491
|
if (upd.error) kids.push(el('p', { class: 'fr-set__note fr-set__error', role: 'alert', 'data-fr': 'update-error' }, upd.error));
|
|
487
492
|
if (upd.output) kids.push(el('pre', { class: 'fr-set__output', 'data-fr': 'update-output' }, upd.output));
|
|
488
493
|
kids.push(el('p', { class: 'fr-set__note' }, P.updatesNote));
|
|
@@ -495,7 +500,7 @@ function checkButton(scope) {
|
|
|
495
500
|
const ws = wsOfRepo(repo) || wsOfRepo(activeProduct());
|
|
496
501
|
return button({
|
|
497
502
|
label: P.updatesCheck, variant: 'outline', size: 'sm', icon: 'refresh-cw', disabled: !!upd.phase || !ws,
|
|
498
|
-
attrs: { 'data-fr': 'check-updates' }, onClick: () => checkUpdates(ws),
|
|
503
|
+
attrs: { 'data-fr': 'check-updates' }, onClick: () => checkUpdates(ws, upd.checkLocal),
|
|
499
504
|
});
|
|
500
505
|
}
|
|
501
506
|
|
|
@@ -176,7 +176,7 @@ export const COPY = {
|
|
|
176
176
|
profileNew: 'New profile',
|
|
177
177
|
profilesReading: 'Reading ~/.claude/profiles…',
|
|
178
178
|
profilesUnread: (e) => 'The profiles could not be read — ' + (e || 'no reason given'),
|
|
179
|
-
profilesFound: (ids) => 'Found ' + ids.length + ': ' + ids.join(', ') + '.',
|
|
179
|
+
profilesFound: (ids, seen) => 'Found ' + ids.length + ': ' + ids.join(', ') + '.' + (seen && seen.length ? ' ' + seen.join(', ') + (seen.length === 1 ? ' was' : ' were') + ' detected from your shell commands or config folders; a session under ' + (seen.length === 1 ? 'it' : 'one') + ' sets only CLAUDE_CONFIG_DIR.' : ''),
|
|
180
180
|
profilesNone: (dir) => 'No profile in ' + (dir || '~/.claude/profiles') + ' yet.',
|
|
181
181
|
pfId: 'Id (the file name)', pfLabel: 'Label', pfAccount: 'Account, shown as written and not checked', pfMcp: 'MCP server names, declared only',
|
|
182
182
|
pfEnvHead: 'Environment variables applied to the session',
|
|
@@ -124,8 +124,12 @@ export function mountView(host, tab) {
|
|
|
124
124
|
if (!v) inner.appendChild(el('div', { style: 'color:var(--muted-foreground);font-size:12px' }, 'this view is not known to this version'));
|
|
125
125
|
else if (board) {
|
|
126
126
|
inner.style.maxWidth = 'none';
|
|
127
|
-
|
|
128
|
-
|
|
127
|
+
const ctx = { tab, view: tab.view, renderMode, ready: !v.mode || !!m, el, setChildren, state, setState, FIXTURE, openView, repo: currentProduct() };
|
|
128
|
+
board.renderBoard(inner, ctx)
|
|
129
|
+
.then((boardEl) => {
|
|
130
|
+
if (boardEl) setChildren(inner, [boardEl]);
|
|
131
|
+
})
|
|
132
|
+
.catch((e) => { console.error('[pm] board ' + tab.view + ' failed:', e); renderMode(inner); });
|
|
129
133
|
} else if (boardMods[tab.view] === null) {
|
|
130
134
|
// the board module is still loading: draw nothing rather than flash the old list
|
|
131
135
|
} else if (m && typeof m.renderMode === 'function') {
|
|
@@ -503,12 +503,18 @@ function profileSection() {
|
|
|
503
503
|
: 'no default is set — new ' + agent + ' sessions start with no profile'))));
|
|
504
504
|
}
|
|
505
505
|
|
|
506
|
-
if (p.absent) {
|
|
506
|
+
if (p.absent && !good.length) {
|
|
507
507
|
out.push(el('div', { style: SUB }, iv('no profile directory at ' + (p.dir || '?')
|
|
508
508
|
+ ' — nothing was read, so nothing is offered but “no profile”')));
|
|
509
509
|
} else if (!good.length && !broken.length) {
|
|
510
510
|
out.push(el('div', { style: SUB }, iv((p.dir || 'the profile directory') + ' was read and holds no descriptors')));
|
|
511
511
|
}
|
|
512
|
+
const seen = good.filter((x) => x && x.detected);
|
|
513
|
+
if (seen.length) {
|
|
514
|
+
out.push(el('div', { class: 'fr-fp-sub', 'data-profile': 'detected' }, iv(seen.map((x) => x.id).join(', ')
|
|
515
|
+
+ (seen.length === 1 ? ' was' : ' were') + ' found from your shell commands or config folders — a session under '
|
|
516
|
+
+ (seen.length === 1 ? 'it' : 'one') + ' sets only CLAUDE_CONFIG_DIR')));
|
|
517
|
+
}
|
|
512
518
|
if (broken.length) {
|
|
513
519
|
out.push(el('div', { style: WARN },
|
|
514
520
|
iv(broken.length + ' descriptor' + (broken.length === 1 ? '' : 's') + ' could not be parsed and '
|
|
@@ -54,12 +54,9 @@ function groupByInitiative(notes) {
|
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
function renderNoteRow(note, isSelected, onClick) {
|
|
57
|
-
const
|
|
58
|
-
const summaryText =
|
|
59
|
-
.split('\n')
|
|
57
|
+
const summaryLines = (note.summary_lines || []);
|
|
58
|
+
const summaryText = summaryLines
|
|
60
59
|
.slice(0, 2)
|
|
61
|
-
.map((line) => line.trim())
|
|
62
|
-
.filter(Boolean)
|
|
63
60
|
.join(' ')
|
|
64
61
|
.substring(0, 60);
|
|
65
62
|
|
package/workspace/server.js
CHANGED
|
@@ -424,7 +424,7 @@ function sessionEnv() {
|
|
|
424
424
|
//
|
|
425
425
|
// The read is not cached. A descriptor added while the workspace is open must appear on the next
|
|
426
426
|
// look, and a stale copy of a file whose whole purpose is provenance is worse than a re-read.
|
|
427
|
-
function readProfilesNow() { return profilesLib.readProfiles(os.homedir()); }
|
|
427
|
+
function readProfilesNow() { return profilesLib.readProfiles(os.homedir(), { detect: true }); }
|
|
428
428
|
/** One descriptor by id, or null. Returns the READ too, because profileLine(null, read) needs it
|
|
429
429
|
* to tell "no profiles on this machine" from "the read failed and we cannot say". */
|
|
430
430
|
function profileById(id) {
|
|
@@ -442,6 +442,8 @@ function profileWire(p, read) {
|
|
|
442
442
|
return {
|
|
443
443
|
id: p.id, label: p.label == null ? null : p.label, account: p.account == null ? null : p.account,
|
|
444
444
|
mcp: Array.isArray(p.mcp) ? p.mcp.slice() : [],
|
|
445
|
+
// where a profile PMOS found (rather than was given) came from: a shell file, a launcher script or a config folder
|
|
446
|
+
detected: p.detected ? { source: p.detected.source, from: p.detected.from, line: p.detected.line } : null,
|
|
445
447
|
env_count: keys.length, env_keys: keys,
|
|
446
448
|
// Computed HERE, once, so no surface can re-word R6's five sentences.
|
|
447
449
|
line: profilesLib.profileLine(p, read),
|
|
@@ -3752,7 +3754,7 @@ function prepareNextStep(body) {
|
|
|
3752
3754
|
const preview = startPreview({ initiative: id, ws: w.id });
|
|
3753
3755
|
if (!preview.ok) return { ...head, refused: { code: 'no-plan', why: preview.error || 'no branch and tree could be chosen' }, plan: null, prompt: null, prompt_sha: null, sources: [], judgment: { on: judgmentOn } };
|
|
3754
3756
|
let mcp = [];
|
|
3755
|
-
if (body.profile) { try { const rp =
|
|
3757
|
+
if (body.profile) { try { const rp = readProfilesNow(); const pr = rp.ok && (rp.profiles || []).find((x) => x.id === body.profile); if (pr && Array.isArray(pr.mcp)) mcp = pr.mcp; } catch { mcp = []; } }
|
|
3756
3758
|
const sources = nextStepActions.sourcesFor(item, mcp);
|
|
3757
3759
|
const prompt = nextStepActions.buildPrompt({ initiative: id, title: got.title, item, branch: preview.branch || null, sources, judgment: judgmentLib.fragment(judgmentOn) });
|
|
3758
3760
|
const sha = nextStepActions.planHash({ prompt, branch: preview.branch, wsId: preview.wsId, plan: preview.plan, judgment: judgmentOn });
|
|
@@ -7327,12 +7329,12 @@ async function handleRequest(req, res) {
|
|
|
7327
7329
|
// the page did not show.
|
|
7328
7330
|
const w = body.ws ? workspaces.get(String(body.ws)) : null;
|
|
7329
7331
|
if (!w) return json(res, { ok: false, error: 'no such workspace' }, 400);
|
|
7330
|
-
const st = () => kitUpdate.status({ root: w.root, appVersion: APP_VERSION, appDir: __dirname });
|
|
7332
|
+
const st = () => kitUpdate.status({ root: w.root, appVersion: APP_VERSION, appDir: __dirname, env: process.env });
|
|
7331
7333
|
if (p === '/api/kit/check') {
|
|
7332
7334
|
const r = await kitUpdate.latest({ source: kitUpdate.source() });
|
|
7333
7335
|
const now = st();
|
|
7334
7336
|
if (!r.ok) return json(res, { ok: false, error: r.error, ws: w.id, ...now }, 502);
|
|
7335
|
-
return json(res, { ok: true, ws: w.id, ...now, latest: r.version, ...kitUpdate.compare(now, r.version) });
|
|
7337
|
+
return json(res, { ok: true, ws: w.id, ...now, latest: r.version, source: r.source || 'npm', ...kitUpdate.compare(now, r.version) });
|
|
7336
7338
|
}
|
|
7337
7339
|
if (kitBusy) return json(res, { ok: false, error: kitBusy === 'app' ? 'The workspace is already updating' : 'A kit update is already running' }, 409);
|
|
7338
7340
|
if (p === '/api/kit/update') {
|