phewsh 0.15.78 → 0.15.80
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/bin/phewsh.js +3 -0
- package/commands/project.js +121 -0
- package/commands/serve.js +55 -2
- package/lib/projects-index.js +72 -2
- package/package.json +1 -1
package/bin/phewsh.js
CHANGED
|
@@ -103,6 +103,8 @@ const COMMANDS = {
|
|
|
103
103
|
setup: () => require('../commands/setup')(),
|
|
104
104
|
update: () => require('../commands/update')(),
|
|
105
105
|
serve: () => require('../commands/serve')(),
|
|
106
|
+
project: () => require('../commands/project')(),
|
|
107
|
+
projects: () => require('../commands/project')(),
|
|
106
108
|
sequence: () => require('../commands/sequence')(),
|
|
107
109
|
seq: () => require('../commands/sequence')(),
|
|
108
110
|
ambient: () => require('../commands/ambient')(),
|
|
@@ -161,6 +163,7 @@ function showHelp() {
|
|
|
161
163
|
console.log(` ${cyan('watch')} ${g('Auto-sync .intent/ → native harness files + cloud')}`);
|
|
162
164
|
console.log(` ${cyan('push/pull')} ${g('Manual sync to/from phewsh.com/intent')}`);
|
|
163
165
|
console.log(` ${cyan('serve')} ${g('Execution bridge — run from phewsh.com/ion or /intent')}`);
|
|
166
|
+
console.log(` ${cyan('project')} ${g('Choose which projects this machine\'s worker shows on /ion')}`);
|
|
164
167
|
console.log(` ${cyan('ion')} ${g('Shared visual room for humans + local agents')}`);
|
|
165
168
|
console.log(` ${cyan('mcp')} ${g('Connect AI agents via MCP protocol')}`);
|
|
166
169
|
console.log(` ${cyan('ambient')} ${g('Continuity without launching phewsh — enhance your other tools')}`);
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// phewsh project — the serve registry: which projects may this machine's
|
|
2
|
+
// worker expose to phewsh.com/ion and /cockpit?
|
|
3
|
+
//
|
|
4
|
+
// Jul 8 2026 Option C ruling: one worker per machine, explicit registry,
|
|
5
|
+
// identity = normalized git remote (never the folder name). Adding a project
|
|
6
|
+
// here does NOT execute anything and does NOT let anyone run work remotely —
|
|
7
|
+
// it only makes the project visible as "online" when `phewsh serve` runs.
|
|
8
|
+
// Manual claim remains the execution boundary.
|
|
9
|
+
//
|
|
10
|
+
// Usage:
|
|
11
|
+
// phewsh project Show the registry + what to do next
|
|
12
|
+
// phewsh project add [path] Register a project (default: this directory)
|
|
13
|
+
// phewsh project list Same as bare `phewsh project`
|
|
14
|
+
// phewsh project remove <name|path> Stop exposing a project
|
|
15
|
+
|
|
16
|
+
const path = require('path');
|
|
17
|
+
const { addServeProject, removeServeProject, serveProjects } = require('../lib/projects-index');
|
|
18
|
+
|
|
19
|
+
const b = (s) => `\x1b[1m${s}\x1b[0m`;
|
|
20
|
+
const g = (s) => `\x1b[38;5;247m${s}\x1b[0m`;
|
|
21
|
+
const w = (s) => `\x1b[97m${s}\x1b[0m`;
|
|
22
|
+
const green = (s) => `\x1b[32m${s}\x1b[0m`;
|
|
23
|
+
const yellow = (s) => `\x1b[33m${s}\x1b[0m`;
|
|
24
|
+
const cyan = (s) => `\x1b[36m${s}\x1b[0m`;
|
|
25
|
+
|
|
26
|
+
function showList() {
|
|
27
|
+
const projects = serveProjects();
|
|
28
|
+
console.log('');
|
|
29
|
+
console.log(` ${b(w('Served projects'))} ${g('— what this machine\'s worker shows on phewsh.com/ion')}`);
|
|
30
|
+
console.log('');
|
|
31
|
+
if (projects.length === 0) {
|
|
32
|
+
console.log(` ${g('None yet. Registering a project is one command, run inside its repo:')}`);
|
|
33
|
+
console.log('');
|
|
34
|
+
console.log(` ${cyan('cd <your-project> && phewsh project add')}`);
|
|
35
|
+
console.log('');
|
|
36
|
+
console.log(` ${g('Then start the worker once for the whole machine:')} ${cyan('phewsh serve')}`);
|
|
37
|
+
} else {
|
|
38
|
+
for (const p of projects) {
|
|
39
|
+
console.log(` ${green('●')} ${w(p.name)}`);
|
|
40
|
+
console.log(` ${g('repo:')} ${g(p.remote || 'no remote')}`);
|
|
41
|
+
console.log(` ${g('path:')} ${g(p.path)}`);
|
|
42
|
+
}
|
|
43
|
+
console.log('');
|
|
44
|
+
console.log(` ${g('These appear as this machine\'s projects when')} ${cyan('phewsh serve')} ${g('is running.')}`);
|
|
45
|
+
console.log(` ${g('Add another:')} ${cyan('phewsh project add [path]')} ${g('Stop exposing one:')} ${cyan('phewsh project remove <name>')}`);
|
|
46
|
+
}
|
|
47
|
+
console.log('');
|
|
48
|
+
console.log(` ${g('Registering never executes anything — work still starts only when a')}`);
|
|
49
|
+
console.log(` ${g('human claims it. See phewsh.com/ion for the team room.')}`);
|
|
50
|
+
console.log('');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function main() {
|
|
54
|
+
const args = process.argv.slice(3);
|
|
55
|
+
const sub = args[0];
|
|
56
|
+
|
|
57
|
+
if (sub === 'add') {
|
|
58
|
+
const dir = args[1] ? path.resolve(args[1]) : process.cwd();
|
|
59
|
+
try {
|
|
60
|
+
const p = addServeProject(dir);
|
|
61
|
+
console.log('');
|
|
62
|
+
console.log(` ${green('✓')} ${w(p.name)} ${g('registered for this machine\'s worker.')}`);
|
|
63
|
+
console.log(` ${g('identity:')} ${g(p.remote)}`);
|
|
64
|
+
console.log('');
|
|
65
|
+
console.log(` ${g('Next:')} ${cyan('phewsh serve')} ${g('— one worker serves every registered project;')}`);
|
|
66
|
+
console.log(` ${g('phewsh.com/ion will show it as online on this machine.')}`);
|
|
67
|
+
console.log('');
|
|
68
|
+
} catch (err) {
|
|
69
|
+
console.log('');
|
|
70
|
+
console.log(` ${yellow('●')} ${err.message.split('\n').join(`\n ${g('')}`)}`);
|
|
71
|
+
console.log('');
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (sub === 'remove' || sub === 'rm') {
|
|
78
|
+
if (!args[1]) {
|
|
79
|
+
console.log('');
|
|
80
|
+
console.log(` ${g('Which one? ')} ${cyan('phewsh project remove <name|path>')}`);
|
|
81
|
+
console.log('');
|
|
82
|
+
showList();
|
|
83
|
+
process.exit(1);
|
|
84
|
+
}
|
|
85
|
+
const hit = removeServeProject(args[1]);
|
|
86
|
+
console.log('');
|
|
87
|
+
if (hit) {
|
|
88
|
+
console.log(` ${green('✓')} ${w(hit.name)} ${g('is no longer exposed by this machine\'s worker.')}`);
|
|
89
|
+
console.log(` ${g('(The project itself is untouched — this only changes what the worker shows.)')}`);
|
|
90
|
+
} else {
|
|
91
|
+
console.log(` ${yellow('●')} ${g('No served project matches')} ${w(args[1])}${g('. Here\'s what\'s registered:')}`);
|
|
92
|
+
showList();
|
|
93
|
+
process.exit(1);
|
|
94
|
+
}
|
|
95
|
+
console.log('');
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (sub && sub !== 'list' && sub !== 'ls' && sub !== 'help' && sub !== '--help' && sub !== '-h') {
|
|
100
|
+
console.log('');
|
|
101
|
+
console.log(` ${yellow('●')} ${g('Unknown subcommand:')} ${w(sub)}`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (sub === 'help' || sub === '--help' || sub === '-h') {
|
|
105
|
+
console.log('');
|
|
106
|
+
console.log(` ${b(w('phewsh project'))} ${g('— choose which projects this machine\'s worker exposes')}`);
|
|
107
|
+
console.log('');
|
|
108
|
+
console.log(` ${cyan('phewsh project')} ${g('show the registry + guidance')}`);
|
|
109
|
+
console.log(` ${cyan('phewsh project add [path]')} ${g('register a project (default: here)')}`);
|
|
110
|
+
console.log(` ${cyan('phewsh project remove <name|path>')} ${g('stop exposing a project')}`);
|
|
111
|
+
console.log('');
|
|
112
|
+
console.log(` ${g('Safety: registering only affects visibility. Execution always requires')}`);
|
|
113
|
+
console.log(` ${g('a human claim — nothing runs remotely because it was registered.')}`);
|
|
114
|
+
console.log('');
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
showList();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
module.exports = main;
|
package/commands/serve.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
// phewsh serve --port 8080 Start on custom port
|
|
13
13
|
|
|
14
14
|
const http = require('http');
|
|
15
|
-
const {
|
|
15
|
+
const { execFileSync, spawn } = require('child_process');
|
|
16
16
|
const crypto = require('crypto');
|
|
17
17
|
const os = require('os');
|
|
18
18
|
const path = require('path');
|
|
@@ -35,6 +35,18 @@ function getPort() {
|
|
|
35
35
|
return 7483;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
// The project this worker serves = the directory it was started in. The
|
|
39
|
+
// normalized origin remote is the identity the claim path already verifies
|
|
40
|
+
// against (task.js repo-match); name alone is display, remote is truth.
|
|
41
|
+
function currentProject() {
|
|
42
|
+
let remote = null;
|
|
43
|
+
try {
|
|
44
|
+
remote = execFileSync('git', ['remote', 'get-url', 'origin'], { stdio: ['ignore', 'pipe', 'ignore'] })
|
|
45
|
+
.toString().trim() || null;
|
|
46
|
+
} catch { /* not a git repo, or no origin — worker still serves the directory */ }
|
|
47
|
+
return { name: path.basename(process.cwd()), remote };
|
|
48
|
+
}
|
|
49
|
+
|
|
38
50
|
// ─── Runtime Detection ─────────────────────────────────────────────────────
|
|
39
51
|
|
|
40
52
|
// Harness runners — shared table in lib/harnesses.js. PHEWSH is not a
|
|
@@ -59,6 +71,7 @@ function detectRuntimes() {
|
|
|
59
71
|
// ─── Job Queue ─────────────────────────────────────────────────────────────
|
|
60
72
|
|
|
61
73
|
const { gatherReceipts, recordSessionEvent, recordResultFile } = require('../lib/receipts-data');
|
|
74
|
+
const { serveProjects } = require('../lib/projects-index');
|
|
62
75
|
|
|
63
76
|
const jobs = new Map();
|
|
64
77
|
|
|
@@ -245,10 +258,15 @@ function main() {
|
|
|
245
258
|
return;
|
|
246
259
|
}
|
|
247
260
|
|
|
248
|
-
// Health check
|
|
261
|
+
// Health check — includes which project this worker is serving, so the
|
|
262
|
+
// web can say "worker online — <project>" instead of an anonymous dot.
|
|
263
|
+
// `projects` = the explicit serve registry (`phewsh project add`); only
|
|
264
|
+
// deliberately registered projects are exposed, never the session index.
|
|
249
265
|
if (url.pathname === '/health' && req.method === 'GET') {
|
|
250
266
|
return json(req, res, {
|
|
251
267
|
status: 'ok',
|
|
268
|
+
project: currentProject(),
|
|
269
|
+
projects: serveProjects().map(p => ({ name: p.name, remote: p.remote })),
|
|
252
270
|
runtimes: detectRuntimes(),
|
|
253
271
|
version: require('../package.json').version,
|
|
254
272
|
uptime: process.uptime(),
|
|
@@ -314,6 +332,7 @@ function main() {
|
|
|
314
332
|
pending: pendingDecisions().length,
|
|
315
333
|
bypasses: bypassStats(),
|
|
316
334
|
recentProjects: listProjects().slice(0, 5).map(p => ({ name: p.name, path: p.path, lastOpened: p.lastOpened })),
|
|
335
|
+
servedProjects: serveProjects().map(p => ({ name: p.name, remote: p.remote })),
|
|
317
336
|
version: require('../package.json').version,
|
|
318
337
|
});
|
|
319
338
|
} catch (err) {
|
|
@@ -351,6 +370,26 @@ function main() {
|
|
|
351
370
|
|
|
352
371
|
const server = http.createServer(handleRequest);
|
|
353
372
|
|
|
373
|
+
// One worker per machine (per port) for now. A second `phewsh serve` used to
|
|
374
|
+
// die with a raw EADDRINUSE stack — say what's true and how to proceed instead.
|
|
375
|
+
server.on('error', (err) => {
|
|
376
|
+
if (err.code === 'EADDRINUSE') {
|
|
377
|
+
console.log('');
|
|
378
|
+
console.log(` ${yellow('●')} A phewsh worker is already running on port ${port}.`);
|
|
379
|
+
console.log('');
|
|
380
|
+
console.log(` ${g('One worker per machine for now — the running worker serves the project')}`);
|
|
381
|
+
console.log(` ${g('directory it was started in. To serve a different project:')}`);
|
|
382
|
+
console.log(` ${g('· stop the other worker (Ctrl+C) and start this one, or')}`);
|
|
383
|
+
console.log(` ${g('· run on another port:')} ${w(`phewsh serve --port ${port + 1}`)}`);
|
|
384
|
+
console.log(` ${g('(note: phewsh.com currently discovers port 7483 only)')}`);
|
|
385
|
+
console.log('');
|
|
386
|
+
console.log(` ${g('A one-worker-many-projects registry is the planned next step.')}`);
|
|
387
|
+
console.log('');
|
|
388
|
+
process.exit(1);
|
|
389
|
+
}
|
|
390
|
+
throw err;
|
|
391
|
+
});
|
|
392
|
+
|
|
354
393
|
server.listen(port, '127.0.0.1', () => {
|
|
355
394
|
const mirror = http.createServer(handleRequest);
|
|
356
395
|
mirror.on('error', () => { /* IPv6 unavailable or already bound */ });
|
|
@@ -374,6 +413,20 @@ function main() {
|
|
|
374
413
|
console.log(` ${g('https://docs.anthropic.com/en/docs/claude-code')}`);
|
|
375
414
|
}
|
|
376
415
|
console.log('');
|
|
416
|
+
const registered = serveProjects();
|
|
417
|
+
const here = currentProject();
|
|
418
|
+
console.log(` ${b('Projects this worker shows on phewsh.com/ion:')}`);
|
|
419
|
+
if (registered.length === 0) {
|
|
420
|
+
console.log(` ${g('only the current directory:')} ${w(here.name)}${here.remote ? g(` (${here.remote})`) : ''}`);
|
|
421
|
+
console.log(` ${g('Register projects by name so the web knows them:')} ${cyan('phewsh project add')} ${g('(run inside each repo)')}`);
|
|
422
|
+
} else {
|
|
423
|
+
for (const p of registered) {
|
|
424
|
+
const isHere = p.path === process.cwd();
|
|
425
|
+
console.log(` ${green('●')} ${w(p.name)}${g(` (${p.remote})`)}${isHere ? g(' ← current directory') : ''}`);
|
|
426
|
+
}
|
|
427
|
+
console.log(` ${g('Manage the list:')} ${cyan('phewsh project')}`);
|
|
428
|
+
}
|
|
429
|
+
console.log('');
|
|
377
430
|
console.log(` ${g('Open phewsh.com/ion to see the worker online, or phewsh.com/intent → Work')}`);
|
|
378
431
|
console.log(` ${g('Press Ctrl+C to stop')}`);
|
|
379
432
|
console.log('');
|
package/lib/projects-index.js
CHANGED
|
@@ -13,7 +13,8 @@ const fs = require('fs');
|
|
|
13
13
|
const path = require('path');
|
|
14
14
|
const os = require('os');
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
// PHEWSH_PROJECT_INDEX override exists for tests only — never point real use away from home.
|
|
17
|
+
const INDEX_FILE = process.env.PHEWSH_PROJECT_INDEX || path.join(os.homedir(), '.phewsh', 'project-index.json');
|
|
17
18
|
|
|
18
19
|
// Shallow-scanned roots when the user asks to find projects. One level deep,
|
|
19
20
|
// opt-in only — deep-scanning someone's machine uninvited is invasive.
|
|
@@ -114,6 +115,75 @@ function scanForCandidates(roots = SCAN_ROOTS) {
|
|
|
114
115
|
return found;
|
|
115
116
|
}
|
|
116
117
|
|
|
118
|
+
// ─── Serve registry (Jul 8 2026 Option C ruling) ────────────────────────────
|
|
119
|
+
// Auto-recorded session entries are NOT auto-exposed to the worker. Only
|
|
120
|
+
// projects the human deliberately registers with `phewsh project add` carry
|
|
121
|
+
// serve:true, and only those appear on the bridge/web. Identity is the
|
|
122
|
+
// normalized git remote — the same convention the task-claim path enforces —
|
|
123
|
+
// never the folder name.
|
|
124
|
+
|
|
125
|
+
const { execFileSync } = require('child_process');
|
|
126
|
+
const { normalizeRemote } = require('./team-tasks');
|
|
127
|
+
|
|
128
|
+
function originRemote(dir) {
|
|
129
|
+
try {
|
|
130
|
+
return execFileSync('git', ['remote', 'get-url', 'origin'], {
|
|
131
|
+
cwd: dir, stdio: ['ignore', 'pipe', 'ignore'],
|
|
132
|
+
}).toString().trim() || null;
|
|
133
|
+
} catch { return null; }
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Register a project so the local worker may expose it. Validates before
|
|
138
|
+
* writing; throws with a plain-language message when something is missing.
|
|
139
|
+
*/
|
|
140
|
+
function addServeProject(dir) {
|
|
141
|
+
if (!fs.existsSync(path.resolve(dir))) throw new Error(`That folder doesn't exist: ${path.resolve(dir)}`);
|
|
142
|
+
const key = fs.realpathSync(path.resolve(dir)); // one entry per real directory, symlinks collapse
|
|
143
|
+
|
|
144
|
+
if (!fs.existsSync(path.join(key, '.git'))) {
|
|
145
|
+
throw new Error(`Not a git repository: ${key}\nA served project needs a repo so work stays traceable. Run this inside a git repo (or git init first).`);
|
|
146
|
+
}
|
|
147
|
+
const remote = originRemote(key);
|
|
148
|
+
if (!remote) {
|
|
149
|
+
throw new Error(`This repo has no 'origin' remote yet.\nThe remote is the project's identity — it's how phewsh guarantees work lands in the right repo.\nAdd one first: git remote add origin <url>`);
|
|
150
|
+
}
|
|
151
|
+
const index = load();
|
|
152
|
+
index.projects[key] = {
|
|
153
|
+
...(index.projects[key] || {}),
|
|
154
|
+
name: path.basename(key),
|
|
155
|
+
path: key,
|
|
156
|
+
remote: normalizeRemote(remote),
|
|
157
|
+
serve: true,
|
|
158
|
+
serveAddedAt: new Date().toISOString(),
|
|
159
|
+
lastOpened: index.projects[key]?.lastOpened || new Date().toISOString(),
|
|
160
|
+
};
|
|
161
|
+
save(index);
|
|
162
|
+
return index.projects[key];
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Stop exposing a project (by name or path). The session-index entry stays. */
|
|
166
|
+
function removeServeProject(nameOrPath) {
|
|
167
|
+
const index = load();
|
|
168
|
+
let wantPath = path.resolve(nameOrPath);
|
|
169
|
+
try { wantPath = fs.realpathSync(wantPath); } catch { /* not a live path — name match still works */ }
|
|
170
|
+
const hit = Object.values(index.projects).find(p =>
|
|
171
|
+
p.serve && (p.path === wantPath || p.name === nameOrPath));
|
|
172
|
+
if (!hit) return null;
|
|
173
|
+
delete index.projects[hit.path].serve;
|
|
174
|
+
delete index.projects[hit.path].serveAddedAt;
|
|
175
|
+
save(index);
|
|
176
|
+
return hit;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Projects the worker is allowed to expose, pruned to paths that still exist. */
|
|
180
|
+
function serveProjects() {
|
|
181
|
+
const index = load();
|
|
182
|
+
return Object.values(index.projects)
|
|
183
|
+
.filter(p => p.serve === true && fs.existsSync(p.path))
|
|
184
|
+
.sort((a, b) => String(a.name).localeCompare(String(b.name)));
|
|
185
|
+
}
|
|
186
|
+
|
|
117
187
|
function fmtAgo(ts) {
|
|
118
188
|
if (!ts) return '';
|
|
119
189
|
const mins = Math.floor((Date.now() - new Date(ts).getTime()) / 60000);
|
|
@@ -123,4 +193,4 @@ function fmtAgo(ts) {
|
|
|
123
193
|
return `${Math.floor(hrs / 24)}d ago`;
|
|
124
194
|
}
|
|
125
195
|
|
|
126
|
-
module.exports = { INDEX_FILE, SCAN_ROOTS, recordProject, listProjects, scanForProjects, scanForCandidates, fmtAgo };
|
|
196
|
+
module.exports = { INDEX_FILE, SCAN_ROOTS, recordProject, listProjects, scanForProjects, scanForCandidates, fmtAgo, addServeProject, removeServeProject, serveProjects };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "phewsh",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.80",
|
|
4
4
|
"description": "One mission. Many AI tools. No lost context. PHEWSH keeps your project's intent, decisions, and working context aligned across Claude Code, Codex, Cursor, Gemini and your team — so the next AI knows what the last one learned.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"phewsh": "bin/phewsh.js"
|