@phren/cli 0.1.13 → 0.1.14

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.
Files changed (34) hide show
  1. package/dist/cli/hooks-session.d.ts +18 -36
  2. package/dist/cli/hooks-session.js +21 -1482
  3. package/dist/cli/namespaces-findings.d.ts +1 -0
  4. package/dist/cli/namespaces-findings.js +208 -0
  5. package/dist/cli/namespaces-profile.d.ts +1 -0
  6. package/dist/cli/namespaces-profile.js +76 -0
  7. package/dist/cli/namespaces-projects.d.ts +1 -0
  8. package/dist/cli/namespaces-projects.js +370 -0
  9. package/dist/cli/namespaces-review.d.ts +1 -0
  10. package/dist/cli/namespaces-review.js +45 -0
  11. package/dist/cli/namespaces-skills.d.ts +4 -0
  12. package/dist/cli/namespaces-skills.js +550 -0
  13. package/dist/cli/namespaces-store.d.ts +2 -0
  14. package/dist/cli/namespaces-store.js +367 -0
  15. package/dist/cli/namespaces-tasks.d.ts +1 -0
  16. package/dist/cli/namespaces-tasks.js +369 -0
  17. package/dist/cli/namespaces-utils.d.ts +4 -0
  18. package/dist/cli/namespaces-utils.js +47 -0
  19. package/dist/cli/namespaces.d.ts +7 -11
  20. package/dist/cli/namespaces.js +8 -2011
  21. package/dist/cli/session-background.d.ts +3 -0
  22. package/dist/cli/session-background.js +176 -0
  23. package/dist/cli/session-git.d.ts +17 -0
  24. package/dist/cli/session-git.js +181 -0
  25. package/dist/cli/session-metrics.d.ts +2 -0
  26. package/dist/cli/session-metrics.js +67 -0
  27. package/dist/cli/session-start.d.ts +3 -0
  28. package/dist/cli/session-start.js +289 -0
  29. package/dist/cli/session-stop.d.ts +8 -0
  30. package/dist/cli/session-stop.js +468 -0
  31. package/dist/cli/session-tool-hook.d.ts +18 -0
  32. package/dist/cli/session-tool-hook.js +376 -0
  33. package/dist/tools/search.js +1 -1
  34. package/package.json +1 -1
@@ -1,2011 +1,8 @@
1
- import * as fs from "fs";
2
- import * as path from "path";
3
- import { execFileSync } from "child_process";
4
- import { expandHomePath, findArchivedProjectNameCaseInsensitive, findProjectNameCaseInsensitive, getPhrenPath, getProjectDirs, homePath, hookConfigPath, normalizeProjectNameForCreate, readRootManifest, } from "../shared.js";
5
- import { isValidProjectName, errorMessage } from "../utils.js";
6
- import { logger } from "../logger.js";
7
- function resolveProjectStorePath(phrenPath, project) {
8
- try {
9
- const { getNonPrimaryStores } = require("../store-registry.js");
10
- if (fs.existsSync(path.join(phrenPath, project)))
11
- return phrenPath;
12
- for (const store of getNonPrimaryStores(phrenPath)) {
13
- if (fs.existsSync(path.join(store.path, project)))
14
- return store.path;
15
- }
16
- }
17
- catch { /* fall through */ }
18
- return phrenPath;
19
- }
20
- import { readInstallPreferences, writeInstallPreferences } from "../init/preferences.js";
21
- import { buildSkillManifest, findLocalSkill, findSkill, getAllSkills } from "../skill/registry.js";
22
- import { detectSkillCollisions } from "../link/skills.js";
23
- import { setSkillEnabledAndSync, syncSkillLinksForScope } from "../skill/files.js";
24
- import { findProjectDir } from "../project-locator.js";
25
- import { TASK_FILE_ALIASES, addTask, completeTask, updateTask, reorderTask, pinTask, removeTask, workNextTask, tidyDoneTasks, linkTaskIssue, promoteTask, resolveTaskItem } from "../data/tasks.js";
26
- import { buildTaskIssueBody, createGithubIssueForTask, parseGithubIssueUrl, resolveProjectGithubRepo } from "../task/github.js";
27
- import { PROJECT_HOOK_EVENTS, PROJECT_OWNERSHIP_MODES, isProjectHookEnabled, parseProjectOwnershipMode, readProjectConfig, writeProjectConfig, writeProjectHookConfig, } from "../project-config.js";
28
- import { addFinding, removeFinding } from "../core/finding.js";
29
- import { supersedeFinding, retractFinding, resolveFindingContradiction } from "../finding/lifecycle.js";
30
- import { readCustomHooks, getHookTarget, HOOK_EVENT_VALUES, validateCustomHookCommand } from "../hooks.js";
31
- import { runtimeFile } from "../shared.js";
32
- import { resolveAllStores, addStoreToRegistry, removeStoreFromRegistry, generateStoreId, readTeamBootstrap, } from "../store-registry.js";
33
- const HOOK_TOOLS = ["claude", "copilot", "cursor", "codex"];
34
- function printSkillsUsage() {
35
- console.log("Usage:");
36
- console.log(" phren skills list [--project <name>]");
37
- console.log(" phren skills show <name> [--project <name>]");
38
- console.log(" phren skills edit <name> [--project <name>]");
39
- console.log(" phren skills add <project> <path>");
40
- console.log(" phren skills resolve <project|global> [--json]");
41
- console.log(" phren skills doctor <project|global>");
42
- console.log(" phren skills sync <project|global>");
43
- console.log(" phren skills enable <project|global> <name>");
44
- console.log(" phren skills disable <project|global> <name>");
45
- console.log(" phren skills remove <project> <name>");
46
- }
47
- function printHooksUsage() {
48
- console.log("Usage:");
49
- console.log(" phren hooks list [--project <name>]");
50
- console.log(" phren hooks show <tool>");
51
- console.log(" phren hooks edit <tool>");
52
- console.log(" phren hooks enable <tool>");
53
- console.log(" phren hooks disable <tool>");
54
- console.log(" phren hooks add-custom <event> <command>");
55
- console.log(" phren hooks remove-custom <event> [<command>]");
56
- console.log(" phren hooks errors [--limit <n>]");
57
- console.log(" tools: claude|copilot|cursor|codex");
58
- console.log(" events: " + HOOK_EVENT_VALUES.join(", "));
59
- }
60
- function normalizeHookTool(raw) {
61
- if (!raw)
62
- return null;
63
- const tool = raw.toLowerCase();
64
- return HOOK_TOOLS.includes(tool) ? tool : null;
65
- }
66
- function getOptionValue(args, name) {
67
- const exactIdx = args.indexOf(name);
68
- if (exactIdx !== -1)
69
- return args[exactIdx + 1];
70
- const prefixed = args.find((arg) => arg.startsWith(`${name}=`));
71
- return prefixed ? prefixed.slice(name.length + 1) : undefined;
72
- }
73
- function parseMcpToggle(raw) {
74
- if (!raw)
75
- return undefined;
76
- const normalized = raw.trim().toLowerCase();
77
- if (normalized === "on" || normalized === "true" || normalized === "enabled")
78
- return true;
79
- if (normalized === "off" || normalized === "false" || normalized === "disabled")
80
- return false;
81
- return undefined;
82
- }
83
- function findSkillPath(name, profile, project) {
84
- const found = findSkill(getPhrenPath(), profile, project, name);
85
- if (!found || "error" in found)
86
- return null;
87
- return found.path;
88
- }
89
- function openInEditor(filePath) {
90
- const editor = process.env.EDITOR || process.env.VISUAL || "nano";
91
- try {
92
- execFileSync(editor, [filePath], { stdio: "inherit" });
93
- }
94
- catch (err) {
95
- if ((process.env.PHREN_DEBUG))
96
- logger.debug("cli-namespaces", `openInEditor: ${errorMessage(err)}`);
97
- console.error(`Editor "${editor}" failed. Set $EDITOR to your preferred editor.`);
98
- process.exit(1);
99
- }
100
- }
101
- export function handleSkillsNamespace(args, profile) {
102
- const subcommand = args[0];
103
- if (!subcommand || subcommand === "--help" || subcommand === "-h") {
104
- printSkillsUsage();
105
- return;
106
- }
107
- if (subcommand === "list") {
108
- const projectIdx = args.indexOf("--project");
109
- const project = projectIdx !== -1 ? args[projectIdx + 1] : undefined;
110
- handleSkillList(profile, project);
111
- return;
112
- }
113
- if (subcommand === "show" || subcommand === "edit") {
114
- const name = args[1];
115
- if (!name) {
116
- printSkillsUsage();
117
- process.exit(1);
118
- }
119
- const projectIdx = args.indexOf("--project");
120
- const project = projectIdx !== -1 ? args[projectIdx + 1] : undefined;
121
- const skillPath = findSkillPath(name, profile, project);
122
- if (!skillPath) {
123
- console.error(`Skill not found: "${name}"${project ? ` in project "${project}"` : ""}`);
124
- process.exit(1);
125
- }
126
- if (subcommand === "show") {
127
- console.log(fs.readFileSync(skillPath, "utf8"));
128
- }
129
- else {
130
- openInEditor(skillPath);
131
- }
132
- return;
133
- }
134
- if (subcommand === "add") {
135
- const project = args[1];
136
- const skillPath = args[2];
137
- if (!project || !skillPath) {
138
- printSkillsUsage();
139
- process.exit(1);
140
- }
141
- if (!isValidProjectName(project)) {
142
- console.error(`Invalid project name: "${project}"`);
143
- process.exit(1);
144
- }
145
- const source = path.resolve(expandHomePath(skillPath));
146
- if (!fs.existsSync(source) || !fs.statSync(source).isFile()) {
147
- console.error(`Skill file not found: ${source}`);
148
- process.exit(1);
149
- }
150
- const baseName = path.basename(source);
151
- const fileName = baseName.toLowerCase().endsWith(".md") ? baseName : `${baseName}.md`;
152
- const destDir = path.join(getPhrenPath(), project, "skills");
153
- const dest = path.join(destDir, fileName);
154
- fs.mkdirSync(destDir, { recursive: true });
155
- if (fs.existsSync(dest)) {
156
- console.error(`Skill already exists: ${dest}`);
157
- process.exit(1);
158
- }
159
- try {
160
- fs.symlinkSync(source, dest);
161
- console.log(`Linked skill ${fileName} into ${project}.`);
162
- }
163
- catch (err) {
164
- if ((process.env.PHREN_DEBUG))
165
- logger.debug("cli-namespaces", `skill add symlinkFailed: ${errorMessage(err)}`);
166
- fs.copyFileSync(source, dest);
167
- console.log(`Copied skill ${fileName} into ${project}.`);
168
- }
169
- return;
170
- }
171
- if (subcommand === "resolve" || subcommand === "doctor" || subcommand === "sync") {
172
- const scope = args[1];
173
- if (!scope) {
174
- printSkillsUsage();
175
- process.exit(1);
176
- }
177
- if (scope.toLowerCase() !== "global" && !isValidProjectName(scope)) {
178
- console.error(`Invalid project name: "${scope}"`);
179
- process.exit(1);
180
- }
181
- if (subcommand === "sync") {
182
- const syncedManifest = syncSkillLinksForScope(getPhrenPath(), scope);
183
- if (!syncedManifest) {
184
- console.error(`Project directory not found for "${scope}".`);
185
- process.exit(1);
186
- }
187
- const mirrorDir = resolveSkillMirrorDir(scope) || homePath(".claude", "skills");
188
- console.log(`Synced ${syncedManifest.skills.filter((skill) => skill.visibleToAgents).length} skill(s) for ${scope}.`);
189
- console.log(` ${path.join(path.dirname(mirrorDir), "skill-manifest.json")}`);
190
- console.log(` ${path.join(path.dirname(mirrorDir), "skill-commands.json")}`);
191
- return;
192
- }
193
- const destDir = resolveSkillMirrorDir(scope);
194
- const manifest = buildSkillManifest(getPhrenPath(), profile, scope, destDir || undefined);
195
- if (subcommand === "resolve") {
196
- if (args.includes("--json")) {
197
- console.log(JSON.stringify(manifest, null, 2));
198
- return;
199
- }
200
- printResolvedManifest(scope, manifest, destDir);
201
- return;
202
- }
203
- printSkillDoctor(scope, manifest, destDir);
204
- return;
205
- }
206
- if (subcommand === "enable" || subcommand === "disable") {
207
- const scope = args[1];
208
- const name = args[2];
209
- if (!scope || !name) {
210
- printSkillsUsage();
211
- process.exit(1);
212
- }
213
- if (scope.toLowerCase() !== "global" && !isValidProjectName(scope)) {
214
- console.error(`Invalid project name: "${scope}"`);
215
- process.exit(1);
216
- }
217
- const resolved = findSkill(getPhrenPath(), profile, scope, name);
218
- if (!resolved || "error" in resolved) {
219
- console.error(`Skill not found: "${name}" in "${scope}"`);
220
- process.exit(1);
221
- }
222
- setSkillEnabledAndSync(getPhrenPath(), scope, resolved.name, subcommand === "enable");
223
- console.log(`${subcommand === "enable" ? "Enabled" : "Disabled"} skill ${resolved.name} in ${scope}.`);
224
- return;
225
- }
226
- if (subcommand === "remove") {
227
- const project = args[1];
228
- const name = args[2];
229
- if (!project || !name) {
230
- printSkillsUsage();
231
- process.exit(1);
232
- }
233
- if (!isValidProjectName(project)) {
234
- console.error(`Invalid project name: "${project}"`);
235
- process.exit(1);
236
- }
237
- const resolved = findLocalSkill(getPhrenPath(), project, name)?.path || null;
238
- if (!resolved) {
239
- console.error(`Skill not found: "${name}" in project "${project}"`);
240
- process.exit(1);
241
- }
242
- const removePath = path.basename(resolved) === "SKILL.md" ? path.dirname(resolved) : resolved;
243
- if (fs.statSync(removePath).isDirectory()) {
244
- fs.rmSync(removePath, { recursive: true, force: true });
245
- }
246
- else {
247
- fs.unlinkSync(removePath);
248
- }
249
- console.log(`Removed skill ${name.replace(/\.md$/i, "")} from ${project}.`);
250
- return;
251
- }
252
- console.error(`Unknown skills subcommand: ${subcommand}`);
253
- printSkillsUsage();
254
- process.exit(1);
255
- }
256
- export function handleHooksNamespace(args) {
257
- const subcommand = args[0];
258
- if (!subcommand || subcommand === "--help" || subcommand === "-h") {
259
- printHooksUsage();
260
- return;
261
- }
262
- if (subcommand === "list") {
263
- const phrenPath = getPhrenPath();
264
- const prefs = readInstallPreferences(phrenPath);
265
- const hooksEnabled = prefs.hooksEnabled !== false;
266
- const toolPrefs = prefs.hookTools && typeof prefs.hookTools === "object" ? prefs.hookTools : {};
267
- const project = getOptionValue(args.slice(1), "--project");
268
- if (project && !isValidProjectName(project)) {
269
- console.error(`Project "${project}" not found.`);
270
- process.exit(1);
271
- }
272
- if (project) {
273
- const storePath = resolveProjectStorePath(phrenPath, project);
274
- if (!fs.existsSync(path.join(storePath, project))) {
275
- console.error(`Project "${project}" not found.`);
276
- process.exit(1);
277
- }
278
- }
279
- const rows = HOOK_TOOLS.map((tool) => ({
280
- tool,
281
- hookType: "lifecycle",
282
- status: hooksEnabled && toolPrefs[tool] !== false ? "enabled" : "disabled",
283
- }));
284
- console.log("Tool Hook Type Status");
285
- console.log("-------- --------- --------");
286
- for (const row of rows) {
287
- console.log(`${row.tool.padEnd(8)} ${row.hookType.padEnd(9)} ${row.status}`);
288
- }
289
- if (project) {
290
- const projectConfig = readProjectConfig(phrenPath, project);
291
- const base = projectConfig.hooks?.enabled;
292
- console.log("");
293
- console.log(`Project ${project}`);
294
- console.log(` base: ${typeof base === "boolean" ? (base ? "enabled" : "disabled") : "inherit"}`);
295
- for (const event of PROJECT_HOOK_EVENTS) {
296
- const configured = projectConfig.hooks?.[event];
297
- const effective = isProjectHookEnabled(phrenPath, project, event, projectConfig);
298
- console.log(` ${event}: ${effective ? "enabled" : "disabled"}${typeof configured === "boolean" ? ` (explicit ${configured ? "on" : "off"})` : " (inherit)"}`);
299
- }
300
- }
301
- const customHooks = readCustomHooks(phrenPath);
302
- if (customHooks.length > 0) {
303
- console.log("");
304
- console.log(`${customHooks.length} custom hook(s):`);
305
- for (const h of customHooks) {
306
- const hookKind = "webhook" in h ? "[webhook] " : "";
307
- console.log(` ${h.event}: ${hookKind}${getHookTarget(h)}${h.timeout ? ` (${h.timeout}ms)` : ""}`);
308
- }
309
- }
310
- return;
311
- }
312
- if (subcommand === "show" || subcommand === "edit") {
313
- const tool = normalizeHookTool(args[1]);
314
- if (!tool) {
315
- printHooksUsage();
316
- process.exit(1);
317
- }
318
- const configPath = hookConfigPath(tool, getPhrenPath());
319
- if (!configPath || !fs.existsSync(configPath)) {
320
- console.error(`Hook config not found for "${tool}": ${configPath ?? "(unknown path)"}`);
321
- process.exit(1);
322
- }
323
- if (subcommand === "show") {
324
- console.log(fs.readFileSync(configPath, "utf8"));
325
- }
326
- else {
327
- openInEditor(configPath);
328
- }
329
- return;
330
- }
331
- if (subcommand === "enable" || subcommand === "disable") {
332
- const tool = normalizeHookTool(args[1]);
333
- if (!tool) {
334
- printHooksUsage();
335
- process.exit(1);
336
- }
337
- const prefs = readInstallPreferences(getPhrenPath());
338
- writeInstallPreferences(getPhrenPath(), {
339
- hookTools: {
340
- ...(prefs.hookTools && typeof prefs.hookTools === "object" ? prefs.hookTools : {}),
341
- [tool]: subcommand === "enable",
342
- },
343
- });
344
- console.log(`${subcommand === "enable" ? "Enabled" : "Disabled"} hooks for ${tool}.`);
345
- return;
346
- }
347
- if (subcommand === "add-custom") {
348
- const event = args[1];
349
- const command = args.slice(2).join(" ");
350
- if (!event || !command) {
351
- console.error('Usage: phren hooks add-custom <event> "<command>"');
352
- console.error("Events: " + HOOK_EVENT_VALUES.join(", "));
353
- process.exit(1);
354
- }
355
- if (!HOOK_EVENT_VALUES.includes(event)) {
356
- console.error(`Invalid event "${event}". Valid events: ${HOOK_EVENT_VALUES.join(", ")}`);
357
- process.exit(1);
358
- }
359
- const commandErr = validateCustomHookCommand(command);
360
- if (commandErr) {
361
- console.error(commandErr);
362
- process.exit(1);
363
- }
364
- const phrenPath = getPhrenPath();
365
- const prefs = readInstallPreferences(phrenPath);
366
- const existing = Array.isArray(prefs.customHooks) ? prefs.customHooks : [];
367
- const newHook = { event: event, command };
368
- writeInstallPreferences(phrenPath, { ...prefs, customHooks: [...existing, newHook] });
369
- console.log(`Added custom hook for "${event}": ${command}`);
370
- return;
371
- }
372
- if (subcommand === "remove-custom") {
373
- const event = args[1];
374
- if (!event) {
375
- console.error('Usage: phren hooks remove-custom <event> [<command>]');
376
- process.exit(1);
377
- }
378
- if (!HOOK_EVENT_VALUES.includes(event)) {
379
- console.error(`Invalid event "${event}". Valid events: ${HOOK_EVENT_VALUES.join(", ")}`);
380
- process.exit(1);
381
- }
382
- const command = args.slice(2).join(" ") || undefined;
383
- const phrenPath = getPhrenPath();
384
- const prefs = readInstallPreferences(phrenPath);
385
- const existing = Array.isArray(prefs.customHooks) ? prefs.customHooks : [];
386
- const remaining = existing.filter(h => h.event !== event || (command && !getHookTarget(h).includes(command)));
387
- const removed = existing.length - remaining.length;
388
- if (removed === 0) {
389
- console.error(`No custom hooks matched event="${event}"${command ? ` command containing "${command}"` : ""}.`);
390
- process.exit(1);
391
- }
392
- writeInstallPreferences(phrenPath, { ...prefs, customHooks: remaining });
393
- console.log(`Removed ${removed} custom hook(s) for "${event}".`);
394
- return;
395
- }
396
- if (subcommand === "errors") {
397
- const phrenPath = getPhrenPath();
398
- const logPath = runtimeFile(phrenPath, "hook-errors.log");
399
- if (!fs.existsSync(logPath)) {
400
- console.log("No hook errors recorded.");
401
- return;
402
- }
403
- const content = fs.readFileSync(logPath, "utf8").trim();
404
- if (!content) {
405
- console.log("No hook errors recorded.");
406
- return;
407
- }
408
- const lines = content.split("\n");
409
- const limitArg = getOptionValue(args.slice(1), "--limit");
410
- const limit = limitArg ? Math.max(1, parseInt(limitArg, 10) || 20) : 20;
411
- const display = lines.slice(-limit);
412
- console.log(`Hook errors (last ${display.length} of ${lines.length}):\n`);
413
- for (const line of display) {
414
- console.log(line);
415
- }
416
- return;
417
- }
418
- console.error(`Unknown hooks subcommand: ${subcommand}`);
419
- printHooksUsage();
420
- process.exit(1);
421
- }
422
- export function handleSkillList(profile, project) {
423
- if (project) {
424
- const manifest = buildSkillManifest(getPhrenPath(), profile, project, resolveSkillMirrorDir(project) || undefined);
425
- printResolvedManifest(project, manifest, resolveSkillMirrorDir(project));
426
- return;
427
- }
428
- const sources = getAllSkills(getPhrenPath(), profile);
429
- if (!sources.length) {
430
- console.log("No skills found.");
431
- return;
432
- }
433
- const nameWidth = Math.max(4, ...sources.map((source) => source.name.length));
434
- const sourceWidth = Math.max(6, ...sources.map((source) => source.source.length));
435
- const formatWidth = Math.max(6, ...sources.map((source) => source.format.length));
436
- const commandWidth = Math.max(7, ...sources.map((source) => source.command.length));
437
- const statusWidth = 8;
438
- console.log(`${"Name".padEnd(nameWidth)} ${"Source".padEnd(sourceWidth)} ${"Format".padEnd(formatWidth)} ${"Command".padEnd(commandWidth)} ${"Status".padEnd(statusWidth)} Path`);
439
- console.log(`${"─".repeat(nameWidth)} ${"─".repeat(sourceWidth)} ${"─".repeat(formatWidth)} ${"─".repeat(commandWidth)} ${"─".repeat(statusWidth)} ${"─".repeat(30)}`);
440
- for (const skill of sources) {
441
- console.log(`${skill.name.padEnd(nameWidth)} ${skill.source.padEnd(sourceWidth)} ${skill.format.padEnd(formatWidth)} ${skill.command.padEnd(commandWidth)} ${(skill.enabled ? "enabled" : "disabled").padEnd(statusWidth)} ${skill.path}`);
442
- }
443
- console.log(`\n${sources.length} skill(s) found.`);
444
- }
445
- export function handleDetectSkills(args, profile) {
446
- const importFlag = args.includes("--import");
447
- const nativeSkillsDir = homePath(".claude", "skills");
448
- if (!fs.existsSync(nativeSkillsDir)) {
449
- console.log("No native skills directory found at ~/.claude/skills/");
450
- return;
451
- }
452
- const trackedSkills = new Set();
453
- const phrenPath = getPhrenPath();
454
- const globalSkillsDir = path.join(phrenPath, "global", "skills");
455
- if (fs.existsSync(globalSkillsDir)) {
456
- for (const entry of fs.readdirSync(globalSkillsDir)) {
457
- trackedSkills.add(entry.replace(/\.md$/, ""));
458
- if (fs.statSync(path.join(globalSkillsDir, entry)).isDirectory()) {
459
- trackedSkills.add(entry);
460
- }
461
- }
462
- }
463
- for (const dir of getProjectDirs(phrenPath, profile)) {
464
- for (const projectSkillsDir of [path.join(dir, "skills"), path.join(dir, ".claude", "skills")]) {
465
- if (!fs.existsSync(projectSkillsDir))
466
- continue;
467
- for (const entry of fs.readdirSync(projectSkillsDir)) {
468
- trackedSkills.add(entry.replace(/\.md$/, ""));
469
- }
470
- }
471
- }
472
- const untracked = [];
473
- for (const entry of fs.readdirSync(nativeSkillsDir)) {
474
- const entryPath = path.join(nativeSkillsDir, entry);
475
- const stat = fs.statSync(entryPath);
476
- try {
477
- if (fs.lstatSync(entryPath).isSymbolicLink())
478
- continue;
479
- }
480
- catch (err) {
481
- if ((process.env.PHREN_DEBUG))
482
- logger.debug("cli-namespaces", `skillList lstat: ${errorMessage(err)}`);
483
- }
484
- const name = entry.replace(/\.md$/, "");
485
- if (trackedSkills.has(name))
486
- continue;
487
- if (stat.isFile() && entry.endsWith(".md")) {
488
- untracked.push({ name, path: entryPath, isDir: false });
489
- }
490
- else if (stat.isDirectory()) {
491
- const skillFile = path.join(entryPath, "SKILL.md");
492
- if (fs.existsSync(skillFile)) {
493
- untracked.push({ name, path: entryPath, isDir: true });
494
- }
495
- }
496
- }
497
- if (!untracked.length) {
498
- console.log("All skills in ~/.claude/skills/ are already tracked by phren.");
499
- return;
500
- }
501
- console.log(`Found ${untracked.length} untracked skill(s) in ~/.claude/skills/:\n`);
502
- for (const skill of untracked) {
503
- console.log(` ${skill.name} (${skill.path})`);
504
- }
505
- if (!importFlag) {
506
- console.log("\nRun with --import to copy these into phren global skills.");
507
- return;
508
- }
509
- fs.mkdirSync(globalSkillsDir, { recursive: true });
510
- let imported = 0;
511
- for (const skill of untracked) {
512
- const dest = skill.isDir
513
- ? path.join(globalSkillsDir, skill.name)
514
- : path.join(globalSkillsDir, `${skill.name}.md`);
515
- if (fs.existsSync(dest)) {
516
- console.log(` skip ${skill.name} (already exists in global/skills/)`);
517
- continue;
518
- }
519
- if (skill.isDir) {
520
- fs.cpSync(skill.path, dest, { recursive: true });
521
- }
522
- else {
523
- fs.copyFileSync(skill.path, dest);
524
- }
525
- const destDisplay = skill.isDir ? `global/skills/${skill.name}/` : `global/skills/${skill.name}.md`;
526
- console.log(` imported ${skill.name} -> ${destDisplay}`);
527
- imported++;
528
- }
529
- console.log(`\nImported ${imported} skill(s). They are now tracked in phren global skills.`);
530
- }
531
- function resolveSkillMirrorDir(scope) {
532
- if (scope.toLowerCase() === "global")
533
- return homePath(".claude", "skills");
534
- const projectDir = findProjectDir(scope);
535
- return projectDir ? path.join(projectDir, ".claude", "skills") : null;
536
- }
537
- function printResolvedManifest(scope, manifest, destDir) {
538
- console.log(`Scope: ${scope}`);
539
- console.log(`Mirror: ${destDir || "(unavailable on disk)"}`);
540
- console.log("");
541
- for (const skill of manifest.skills) {
542
- const status = skill.visibleToAgents ? "visible" : "disabled";
543
- const overrideText = skill.overrides.length ? ` override:${skill.overrides.length}` : "";
544
- console.log(`${skill.command} ${skill.name} ${skill.source} ${status}${overrideText}`);
545
- console.log(` ${skill.path}`);
546
- }
547
- if (manifest.problems.length) {
548
- console.log("\nProblems:");
549
- for (const problem of manifest.problems) {
550
- console.log(`- ${problem.message}`);
551
- }
552
- }
553
- }
554
- function printSkillDoctor(scope, manifest, destDir) {
555
- printResolvedManifest(scope, manifest, destDir);
556
- const problems = [];
557
- if (!destDir) {
558
- problems.push(`Mirror target for ${scope} is not discoverable on disk.`);
559
- }
560
- else {
561
- const parentDir = path.dirname(destDir);
562
- if (!fs.existsSync(path.join(parentDir, "skill-manifest.json"))) {
563
- problems.push(`Missing generated manifest: ${path.join(parentDir, "skill-manifest.json")}`);
564
- }
565
- if (!fs.existsSync(path.join(parentDir, "skill-commands.json"))) {
566
- problems.push(`Missing generated command registry: ${path.join(parentDir, "skill-commands.json")}`);
567
- }
568
- for (const skill of manifest.skills.filter((entry) => entry.visibleToAgents)) {
569
- const dest = path.join(destDir, skill.format === "folder" ? skill.name : path.basename(skill.path));
570
- try {
571
- if (!fs.existsSync(dest) || fs.realpathSync(dest) !== fs.realpathSync(skill.root)) {
572
- problems.push(`Mirror drift for ${skill.name}: expected ${dest} -> ${skill.root}`);
573
- }
574
- }
575
- catch {
576
- problems.push(`Mirror drift for ${skill.name}: expected ${dest} -> ${skill.root}`);
577
- }
578
- }
579
- // Check for user-owned files blocking phren skill links
580
- const phrenPath = getPhrenPath();
581
- const srcDir = scope.toLowerCase() === "global"
582
- ? path.join(phrenPath, "global", "skills")
583
- : path.join(phrenPath, scope, "skills");
584
- const collisions = detectSkillCollisions(srcDir, destDir, phrenPath);
585
- for (const collision of collisions) {
586
- problems.push(`Skill collision: ${collision.message}`);
587
- }
588
- }
589
- if (!manifest.problems.length && !problems.length) {
590
- console.log("\nDoctor: no skill pipeline issues detected.");
591
- return;
592
- }
593
- console.log("\nDoctor findings:");
594
- for (const problem of [...manifest.problems.map((entry) => entry.message), ...problems]) {
595
- console.log(`- ${problem}`);
596
- }
597
- }
598
- export async function handleProjectsNamespace(args, profile) {
599
- const subcommand = args[0];
600
- if (!subcommand || subcommand === "list" || subcommand === "--help" || subcommand === "-h") {
601
- if (subcommand === "--help" || subcommand === "-h") {
602
- console.log("Usage:");
603
- console.log(" phren projects list List all projects");
604
- console.log(" phren projects configure <name> Update per-project enrollment settings");
605
- console.log(" flags: --ownership=<mode> --hooks=on|off");
606
- console.log(" phren projects remove <name> Remove a project (asks for confirmation)");
607
- console.log(" phren projects export <name> Export project data as JSON to stdout");
608
- console.log(" phren projects import <file> Import project from a JSON file");
609
- console.log(" phren projects archive <name> Archive a project (removes from active index)");
610
- console.log(" phren projects unarchive <name> Restore an archived project");
611
- return;
612
- }
613
- return handleProjectsList(profile);
614
- }
615
- if (subcommand === "add") {
616
- console.error("`phren projects add` has been removed from the supported workflow.");
617
- console.error("Use `cd ~/your-project && phren add` so enrollment stays path-based.");
618
- process.exit(1);
619
- }
620
- if (subcommand === "remove") {
621
- const manifest = readRootManifest(getPhrenPath());
622
- if (manifest?.installMode === "project-local") {
623
- console.error("projects remove is unsupported in project-local mode. Use `phren uninstall`.");
624
- process.exit(1);
625
- }
626
- const name = args[1];
627
- if (!name) {
628
- console.error("Usage: phren projects remove <name>");
629
- process.exit(1);
630
- }
631
- return handleProjectsRemove(name, profile);
632
- }
633
- if (subcommand === "configure") {
634
- const name = args[1];
635
- if (!name) {
636
- console.error(`Usage: phren projects configure <name> [--ownership=${PROJECT_OWNERSHIP_MODES.join("|")}] [--hooks=on|off]`);
637
- process.exit(1);
638
- }
639
- if (!isValidProjectName(name)) {
640
- console.error(`Invalid project name: "${name}".`);
641
- process.exit(1);
642
- }
643
- if (!fs.existsSync(path.join(getPhrenPath(), name))) {
644
- console.error(`Project "${name}" not found.`);
645
- process.exit(1);
646
- }
647
- const ownershipArg = args.find((arg) => arg.startsWith("--ownership="))?.slice("--ownership=".length);
648
- const hooksArg = args.find((arg) => arg.startsWith("--hooks="))?.slice("--hooks=".length);
649
- const ownership = ownershipArg ? parseProjectOwnershipMode(ownershipArg) : undefined;
650
- const hooksEnabled = parseMcpToggle(hooksArg);
651
- if (!ownershipArg && hooksArg === undefined) {
652
- console.error(`Usage: phren projects configure <name> [--ownership=${PROJECT_OWNERSHIP_MODES.join("|")}] [--hooks=on|off]`);
653
- process.exit(1);
654
- }
655
- if (ownershipArg && !ownership) {
656
- console.error(`Usage: phren projects configure <name> [--ownership=${PROJECT_OWNERSHIP_MODES.join("|")}] [--hooks=on|off]`);
657
- process.exit(1);
658
- }
659
- if (hooksArg !== undefined && hooksEnabled === undefined) {
660
- console.error(`Invalid --hooks value "${hooksArg}". Use on or off.`);
661
- process.exit(1);
662
- }
663
- const updates = [];
664
- if (ownership) {
665
- writeProjectConfig(getPhrenPath(), name, { ownership });
666
- updates.push(`ownership=${ownership}`);
667
- }
668
- if (hooksEnabled !== undefined) {
669
- writeProjectHookConfig(getPhrenPath(), name, { enabled: hooksEnabled });
670
- updates.push(`hooks=${hooksEnabled ? "on" : "off"}`);
671
- }
672
- console.log(`Updated ${name}: ${updates.join(", ")}`);
673
- return;
674
- }
675
- if (subcommand === "export") {
676
- const name = args[1];
677
- if (!name) {
678
- console.error("Usage: phren projects export <name>");
679
- process.exit(1);
680
- }
681
- if (!isValidProjectName(name)) {
682
- console.error(`Invalid project name: "${name}".`);
683
- process.exit(1);
684
- }
685
- const phrenPath = getPhrenPath();
686
- const storePath = resolveProjectStorePath(phrenPath, name);
687
- const projectDir = path.join(storePath, name);
688
- if (!fs.existsSync(projectDir)) {
689
- console.error(`Project "${name}" not found.`);
690
- process.exit(1);
691
- }
692
- const { readFindings, readTasks, resolveTaskFilePath } = await import("../data/access.js");
693
- const exported = { project: name, exportedAt: new Date().toISOString(), version: 1 };
694
- const summaryPath = path.join(projectDir, "summary.md");
695
- if (fs.existsSync(summaryPath))
696
- exported.summary = fs.readFileSync(summaryPath, "utf8");
697
- const learningsResult = readFindings(storePath, name);
698
- if (learningsResult.ok)
699
- exported.learnings = learningsResult.data;
700
- const findingsPath = path.join(projectDir, "FINDINGS.md");
701
- if (fs.existsSync(findingsPath))
702
- exported.findingsRaw = fs.readFileSync(findingsPath, "utf8");
703
- const taskResult = readTasks(storePath, name);
704
- if (taskResult.ok) {
705
- exported.task = taskResult.data.items;
706
- const taskRawPath = resolveTaskFilePath(storePath, name);
707
- if (taskRawPath && fs.existsSync(taskRawPath))
708
- exported.taskRaw = fs.readFileSync(taskRawPath, "utf8");
709
- }
710
- const claudePath = path.join(projectDir, "CLAUDE.md");
711
- if (fs.existsSync(claudePath))
712
- exported.claudeMd = fs.readFileSync(claudePath, "utf8");
713
- process.stdout.write(JSON.stringify(exported, null, 2) + "\n");
714
- return;
715
- }
716
- if (subcommand === "import") {
717
- const filePath = args[1];
718
- if (!filePath) {
719
- console.error("Usage: phren projects import <file>");
720
- process.exit(1);
721
- }
722
- const resolvedPath = path.resolve(expandHomePath(filePath));
723
- if (!fs.existsSync(resolvedPath)) {
724
- console.error(`File not found: ${resolvedPath}`);
725
- process.exit(1);
726
- }
727
- let rawData;
728
- try {
729
- rawData = fs.readFileSync(resolvedPath, "utf8");
730
- }
731
- catch (err) {
732
- console.error(`Failed to read file: ${errorMessage(err)}`);
733
- process.exit(1);
734
- }
735
- let decoded;
736
- try {
737
- decoded = JSON.parse(rawData);
738
- }
739
- catch {
740
- console.error("Invalid JSON in file.");
741
- process.exit(1);
742
- }
743
- if (!decoded || typeof decoded !== "object" || !decoded.project) {
744
- console.error("Invalid import payload: missing project field.");
745
- process.exit(1);
746
- }
747
- const { TASKS_FILENAME } = await import("../data/access.js");
748
- const phrenPath = getPhrenPath();
749
- const projectName = normalizeProjectNameForCreate(String(decoded.project));
750
- if (!isValidProjectName(projectName)) {
751
- console.error(`Invalid project name: "${decoded.project}".`);
752
- process.exit(1);
753
- }
754
- const existingProject = findProjectNameCaseInsensitive(phrenPath, projectName);
755
- if (existingProject && existingProject !== projectName) {
756
- console.error(`Project "${existingProject}" already exists with different casing. Refusing to import "${projectName}".`);
757
- process.exit(1);
758
- }
759
- const projectDir = path.join(phrenPath, projectName);
760
- if (fs.existsSync(projectDir)) {
761
- console.error(`Project "${projectName}" already exists. Remove it first or use the MCP tool with overwrite:true.`);
762
- process.exit(1);
763
- }
764
- const imported = [];
765
- const stagingRoot = fs.mkdtempSync(path.join(phrenPath, `.phren-import-${projectName}-`));
766
- const stagedProjectDir = path.join(stagingRoot, projectName);
767
- try {
768
- fs.mkdirSync(stagedProjectDir, { recursive: true });
769
- if (typeof decoded.summary === "string") {
770
- fs.writeFileSync(path.join(stagedProjectDir, "summary.md"), decoded.summary);
771
- imported.push("summary.md");
772
- }
773
- if (typeof decoded.claudeMd === "string") {
774
- fs.writeFileSync(path.join(stagedProjectDir, "CLAUDE.md"), decoded.claudeMd);
775
- imported.push("CLAUDE.md");
776
- }
777
- if (typeof decoded.findingsRaw === "string") {
778
- fs.writeFileSync(path.join(stagedProjectDir, "FINDINGS.md"), decoded.findingsRaw);
779
- imported.push("FINDINGS.md");
780
- }
781
- else if (Array.isArray(decoded.learnings) && decoded.learnings.length > 0) {
782
- const date = new Date().toISOString().slice(0, 10);
783
- const lines = [`# ${projectName} Findings`, "", `## ${date}`, ""];
784
- for (const item of decoded.learnings) {
785
- if (item.text)
786
- lines.push(`- ${item.text}`);
787
- }
788
- lines.push("");
789
- fs.writeFileSync(path.join(stagedProjectDir, "FINDINGS.md"), lines.join("\n"));
790
- imported.push("FINDINGS.md");
791
- }
792
- if (typeof decoded.taskRaw === "string") {
793
- fs.writeFileSync(path.join(stagedProjectDir, TASKS_FILENAME), decoded.taskRaw);
794
- imported.push(TASKS_FILENAME);
795
- }
796
- fs.renameSync(stagedProjectDir, projectDir);
797
- fs.rmSync(stagingRoot, { recursive: true, force: true });
798
- console.log(`Imported project "${projectName}": ${imported.join(", ") || "(no files)"}`);
799
- }
800
- catch (err) {
801
- try {
802
- fs.rmSync(stagingRoot, { recursive: true, force: true });
803
- }
804
- catch { /* best-effort */ }
805
- console.error(`Import failed: ${errorMessage(err)}`);
806
- process.exit(1);
807
- }
808
- return;
809
- }
810
- if (subcommand === "archive" || subcommand === "unarchive") {
811
- const name = args[1];
812
- if (!name) {
813
- console.error(`Usage: phren projects ${subcommand} <name>`);
814
- process.exit(1);
815
- }
816
- if (!isValidProjectName(name)) {
817
- console.error(`Invalid project name: "${name}".`);
818
- process.exit(1);
819
- }
820
- const phrenPath = getPhrenPath();
821
- if (subcommand === "archive") {
822
- const activeProject = findProjectNameCaseInsensitive(phrenPath, name);
823
- const storePath = resolveProjectStorePath(phrenPath, activeProject ?? name);
824
- const projectDir = activeProject ? path.join(storePath, activeProject) : path.join(storePath, name);
825
- const archiveDir = path.join(storePath, `${activeProject ?? name}.archived`);
826
- if (!fs.existsSync(projectDir)) {
827
- console.error(`Project "${name}" not found.`);
828
- process.exit(1);
829
- }
830
- if (fs.existsSync(archiveDir)) {
831
- console.error(`Archive "${name}.archived" already exists. Unarchive or remove it first.`);
832
- process.exit(1);
833
- }
834
- try {
835
- fs.renameSync(projectDir, archiveDir);
836
- console.log(`Archived project "${name}". Data preserved at ${archiveDir}.`);
837
- console.log("Note: the search index will be updated on next search.");
838
- }
839
- catch (err) {
840
- console.error(`Archive failed: ${errorMessage(err)}`);
841
- process.exit(1);
842
- }
843
- }
844
- else {
845
- const activeProject = findProjectNameCaseInsensitive(phrenPath, name);
846
- if (activeProject) {
847
- console.error(`Project "${activeProject}" already exists as an active project.`);
848
- process.exit(1);
849
- }
850
- const archivedProject = findArchivedProjectNameCaseInsensitive(phrenPath, name);
851
- const storePath = resolveProjectStorePath(phrenPath, archivedProject ?? name);
852
- const projectDir = path.join(storePath, archivedProject ?? name);
853
- const archiveDir = path.join(storePath, `${archivedProject ?? name}.archived`);
854
- if (!fs.existsSync(archiveDir)) {
855
- const available = fs.readdirSync(phrenPath)
856
- .filter((e) => e.endsWith(".archived"))
857
- .map((e) => e.replace(/\.archived$/, ""));
858
- if (available.length > 0) {
859
- console.error(`No archive found for "${name}". Available archives: ${available.join(", ")}`);
860
- }
861
- else {
862
- console.error(`No archive found for "${name}".`);
863
- }
864
- process.exit(1);
865
- }
866
- try {
867
- fs.renameSync(archiveDir, projectDir);
868
- console.log(`Unarchived project "${archivedProject ?? name}". It is now active again.`);
869
- console.log("Note: the search index will be updated on next search.");
870
- }
871
- catch (err) {
872
- console.error(`Unarchive failed: ${errorMessage(err)}`);
873
- process.exit(1);
874
- }
875
- }
876
- return;
877
- }
878
- console.error(`Unknown subcommand: ${subcommand}`);
879
- console.error("Usage: phren projects [list|configure|remove|export|import|archive|unarchive]");
880
- process.exit(1);
881
- }
882
- function handleProjectsList(profile) {
883
- const phrenPath = getPhrenPath();
884
- const projectDirs = getProjectDirs(phrenPath, profile);
885
- const projects = projectDirs
886
- .map((dir) => path.basename(dir))
887
- .filter((name) => name !== "global")
888
- .sort();
889
- if (!projects.length) {
890
- console.log("No projects found. Run: cd ~/your-project && phren add");
891
- return;
892
- }
893
- console.log(`\nProjects in ${phrenPath}:\n`);
894
- for (const name of projects) {
895
- const projectDir = path.join(phrenPath, name);
896
- let dirFiles;
897
- try {
898
- dirFiles = new Set(fs.readdirSync(projectDir));
899
- }
900
- catch (err) {
901
- if ((process.env.PHREN_DEBUG))
902
- logger.debug("cli-namespaces", `projects list readdir: ${errorMessage(err)}`);
903
- dirFiles = new Set();
904
- }
905
- const tags = [];
906
- if (dirFiles.has("FINDINGS.md"))
907
- tags.push("findings");
908
- if (TASK_FILE_ALIASES.some((filename) => dirFiles.has(filename)))
909
- tags.push("tasks");
910
- const tagStr = tags.length ? ` [${tags.join(", ")}]` : "";
911
- console.log(` ${name}${tagStr}`);
912
- }
913
- console.log(`\n${projects.length} project(s) total.`);
914
- console.log("Add another project: cd ~/your-project && phren add");
915
- }
916
- async function handleProjectsRemove(name, profile) {
917
- if (!isValidProjectName(name)) {
918
- console.error(`Invalid project name: "${name}".`);
919
- process.exit(1);
920
- }
921
- if (name === "global") {
922
- console.error('Cannot remove the "global" project.');
923
- process.exit(1);
924
- }
925
- const phrenPath = getPhrenPath();
926
- const projectDir = path.join(phrenPath, name);
927
- if (!fs.existsSync(projectDir)) {
928
- console.error(`Project "${name}" not found at ${projectDir}`);
929
- process.exit(1);
930
- }
931
- let fileCount = 0;
932
- const countFiles = (dir) => {
933
- for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
934
- if (entry.isDirectory())
935
- countFiles(path.join(dir, entry.name));
936
- else
937
- fileCount++;
938
- }
939
- };
940
- try {
941
- countFiles(projectDir);
942
- }
943
- catch (err) {
944
- if ((process.env.PHREN_DEBUG))
945
- logger.debug("cli-namespaces", `projects remove countFiles: ${errorMessage(err)}`);
946
- }
947
- const readline = await import("readline");
948
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
949
- const answer = await new Promise((resolve) => {
950
- rl.question(`Remove project "${name}" (${fileCount} file${fileCount === 1 ? "" : "s"})? This cannot be undone. Type the project name to confirm: `, (input) => { rl.close(); resolve(input.trim()); });
951
- });
952
- if (answer !== name) {
953
- console.log("Aborted.");
954
- return;
955
- }
956
- fs.rmSync(projectDir, { recursive: true, force: true });
957
- console.log(`Removed project "${name}".`);
958
- console.log(`If this project was in a profile, remove it from profiles/${profile || "personal"}.yaml manually.`);
959
- }
960
- // ── Task namespace ────────────────────────────────────────────────────────────
961
- function printTaskUsage() {
962
- console.log("Usage:");
963
- console.log(' phren task add <project> "<text>"');
964
- console.log(' phren task complete <project> "<text>"');
965
- console.log(' phren task remove <project> "<text>"');
966
- console.log(' phren task next [project]');
967
- console.log(' phren task promote <project> "<text>" [--active]');
968
- console.log(' phren task tidy [project] [--keep=<n>] [--dry-run]');
969
- console.log(' phren task link <project> "<text>" --issue <number> [--url <url>]');
970
- console.log(' phren task link <project> "<text>" --unlink');
971
- console.log(' phren task create-issue <project> "<text>" [--repo <owner/name>] [--title "<title>"] [--done]');
972
- console.log(' phren task update <project> "<text>" [--priority=high|medium|low] [--section=Active|Queue|Done] [--context="..."]');
973
- console.log(' phren task pin <project> "<text>"');
974
- console.log(' phren task reorder <project> "<text>" --rank=<n>');
975
- }
976
- export async function handleTaskNamespace(args) {
977
- const subcommand = args[0];
978
- if (!subcommand || subcommand === "--help" || subcommand === "-h") {
979
- printTaskUsage();
980
- return;
981
- }
982
- if (subcommand === "list") {
983
- // Delegate to the cross-project task view (same as `phren tasks`)
984
- const { handleTaskView } = await import("./ops.js");
985
- return handleTaskView(args[1] || "default");
986
- }
987
- if (subcommand === "add") {
988
- const project = args[1];
989
- const text = args.slice(2).join(" ");
990
- if (!project || !text) {
991
- console.error('Usage: phren task add <project> "<text>"');
992
- process.exit(1);
993
- }
994
- const result = addTask(getPhrenPath(), project, text);
995
- if (!result.ok) {
996
- console.error(result.error);
997
- process.exit(1);
998
- }
999
- console.log(`Task added: ${result.data.line}`);
1000
- return;
1001
- }
1002
- if (subcommand === "complete") {
1003
- const project = args[1];
1004
- const match = args.slice(2).join(" ");
1005
- if (!project || !match) {
1006
- console.error('Usage: phren task complete <project> "<text>"');
1007
- process.exit(1);
1008
- }
1009
- const result = completeTask(getPhrenPath(), project, match);
1010
- if (!result.ok) {
1011
- console.error(result.error);
1012
- process.exit(1);
1013
- }
1014
- console.log(result.data);
1015
- return;
1016
- }
1017
- if (subcommand === "update") {
1018
- const project = args[1];
1019
- if (!project) {
1020
- printTaskUsage();
1021
- process.exit(1);
1022
- }
1023
- // Collect non-flag args as the match text, flags as updates
1024
- const positional = [];
1025
- const updates = {};
1026
- for (const arg of args.slice(2)) {
1027
- if (arg.startsWith("--priority=")) {
1028
- updates.priority = arg.slice("--priority=".length);
1029
- }
1030
- else if (arg.startsWith("--section=")) {
1031
- updates.section = arg.slice("--section=".length);
1032
- }
1033
- else if (arg.startsWith("--context=")) {
1034
- updates.context = arg.slice("--context=".length);
1035
- }
1036
- else if (!arg.startsWith("--")) {
1037
- positional.push(arg);
1038
- }
1039
- }
1040
- const match = positional.join(" ");
1041
- if (!match) {
1042
- printTaskUsage();
1043
- process.exit(1);
1044
- }
1045
- const result = updateTask(getPhrenPath(), project, match, updates);
1046
- if (!result.ok) {
1047
- console.error(result.error);
1048
- process.exit(1);
1049
- }
1050
- console.log(result.data);
1051
- return;
1052
- }
1053
- if (subcommand === "pin") {
1054
- const project = args[1];
1055
- const match = args.slice(2).join(" ");
1056
- if (!project || !match) {
1057
- console.error('Usage: phren task pin <project> "<text>"');
1058
- process.exit(1);
1059
- }
1060
- const result = pinTask(getPhrenPath(), project, match);
1061
- if (!result.ok) {
1062
- console.error(result.error);
1063
- process.exit(1);
1064
- }
1065
- console.log(result.data);
1066
- return;
1067
- }
1068
- if (subcommand === "reorder") {
1069
- const project = args[1];
1070
- if (!project) {
1071
- printTaskUsage();
1072
- process.exit(1);
1073
- }
1074
- const positional = [];
1075
- let rankArg;
1076
- for (const arg of args.slice(2)) {
1077
- if (arg.startsWith("--rank=")) {
1078
- rankArg = arg.slice("--rank=".length);
1079
- }
1080
- else if (!arg.startsWith("--")) {
1081
- positional.push(arg);
1082
- }
1083
- }
1084
- const match = positional.join(" ");
1085
- const rank = rankArg ? Number.parseInt(rankArg, 10) : Number.NaN;
1086
- if (!match || !rankArg || !Number.isFinite(rank) || rank < 1) {
1087
- console.error('Usage: phren task reorder <project> "<text>" --rank=<n>');
1088
- process.exit(1);
1089
- }
1090
- const result = reorderTask(getPhrenPath(), project, match, rank);
1091
- if (!result.ok) {
1092
- console.error(result.error);
1093
- process.exit(1);
1094
- }
1095
- console.log(result.data);
1096
- return;
1097
- }
1098
- if (subcommand === "remove") {
1099
- const project = args[1];
1100
- const match = args.slice(2).join(" ");
1101
- if (!project || !match) {
1102
- console.error('Usage: phren task remove <project> "<text>"');
1103
- process.exit(1);
1104
- }
1105
- const result = removeTask(getPhrenPath(), project, match);
1106
- if (!result.ok) {
1107
- console.error(result.error);
1108
- process.exit(1);
1109
- }
1110
- console.log(result.data);
1111
- return;
1112
- }
1113
- if (subcommand === "next") {
1114
- const project = args[1];
1115
- if (!project) {
1116
- console.error("Usage: phren task next <project>");
1117
- process.exit(1);
1118
- }
1119
- const result = workNextTask(getPhrenPath(), project);
1120
- if (!result.ok) {
1121
- console.error(result.error);
1122
- process.exit(1);
1123
- }
1124
- console.log(result.data);
1125
- return;
1126
- }
1127
- if (subcommand === "promote") {
1128
- const project = args[1];
1129
- if (!project) {
1130
- printTaskUsage();
1131
- process.exit(1);
1132
- }
1133
- const positional = [];
1134
- let moveToActive = false;
1135
- for (const arg of args.slice(2)) {
1136
- if (arg === "--active") {
1137
- moveToActive = true;
1138
- }
1139
- else if (!arg.startsWith("--")) {
1140
- positional.push(arg);
1141
- }
1142
- }
1143
- const match = positional.join(" ");
1144
- if (!match) {
1145
- console.error('Usage: phren task promote <project> "<text>" [--active]');
1146
- process.exit(1);
1147
- }
1148
- const result = promoteTask(getPhrenPath(), project, match, moveToActive);
1149
- if (!result.ok) {
1150
- console.error(result.error);
1151
- process.exit(1);
1152
- }
1153
- console.log(`Promoted task "${result.data.line}" in ${project}${moveToActive ? " (moved to Active)" : ""}.`);
1154
- return;
1155
- }
1156
- if (subcommand === "tidy") {
1157
- const project = args[1];
1158
- if (!project) {
1159
- console.error("Usage: phren task tidy <project> [--keep=<n>] [--dry-run]");
1160
- process.exit(1);
1161
- }
1162
- let keep = 30;
1163
- let dryRun = false;
1164
- for (const arg of args.slice(2)) {
1165
- if (arg.startsWith("--keep=")) {
1166
- const n = Number.parseInt(arg.slice("--keep=".length), 10);
1167
- if (Number.isFinite(n) && n > 0)
1168
- keep = n;
1169
- }
1170
- else if (arg === "--dry-run") {
1171
- dryRun = true;
1172
- }
1173
- }
1174
- const result = tidyDoneTasks(getPhrenPath(), project, keep, dryRun);
1175
- if (!result.ok) {
1176
- console.error(result.error);
1177
- process.exit(1);
1178
- }
1179
- console.log(result.data);
1180
- return;
1181
- }
1182
- if (subcommand === "link") {
1183
- const project = args[1];
1184
- if (!project) {
1185
- printTaskUsage();
1186
- process.exit(1);
1187
- }
1188
- const positional = [];
1189
- let issueArg;
1190
- let urlArg;
1191
- let unlink = false;
1192
- const rest = args.slice(2);
1193
- for (let i = 0; i < rest.length; i++) {
1194
- const arg = rest[i];
1195
- if (arg === "--issue" || arg === "-i") {
1196
- issueArg = rest[++i];
1197
- }
1198
- else if (arg.startsWith("--issue=")) {
1199
- issueArg = arg.slice("--issue=".length);
1200
- }
1201
- else if (arg === "--url") {
1202
- urlArg = rest[++i];
1203
- }
1204
- else if (arg.startsWith("--url=")) {
1205
- urlArg = arg.slice("--url=".length);
1206
- }
1207
- else if (arg === "--unlink") {
1208
- unlink = true;
1209
- }
1210
- else if (!arg.startsWith("--")) {
1211
- positional.push(arg);
1212
- }
1213
- }
1214
- const match = positional.join(" ");
1215
- if (!match) {
1216
- console.error('Usage: phren task link <project> "<text>" --issue <number>');
1217
- process.exit(1);
1218
- }
1219
- if (!unlink && !issueArg && !urlArg) {
1220
- console.error("Provide --issue <number> or --url <url> to link, or --unlink to remove the link.");
1221
- process.exit(1);
1222
- }
1223
- if (urlArg) {
1224
- const parsed = parseGithubIssueUrl(urlArg);
1225
- if (!parsed) {
1226
- console.error("--url must be a valid GitHub issue URL.");
1227
- process.exit(1);
1228
- }
1229
- }
1230
- const result = linkTaskIssue(getPhrenPath(), project, match, {
1231
- github_issue: issueArg,
1232
- github_url: urlArg,
1233
- unlink: unlink,
1234
- });
1235
- if (!result.ok) {
1236
- console.error(result.error);
1237
- process.exit(1);
1238
- }
1239
- if (unlink) {
1240
- console.log(`Removed GitHub link from ${project} task.`);
1241
- }
1242
- else {
1243
- console.log(`Linked ${project} task to ${result.data.githubIssue ? `#${result.data.githubIssue}` : result.data.githubUrl}.`);
1244
- }
1245
- return;
1246
- }
1247
- if (subcommand === "create-issue") {
1248
- const project = args[1];
1249
- if (!project) {
1250
- printTaskUsage();
1251
- process.exit(1);
1252
- }
1253
- const positional = [];
1254
- let repoArg;
1255
- let titleArg;
1256
- let markDone = false;
1257
- const rest = args.slice(2);
1258
- for (let i = 0; i < rest.length; i++) {
1259
- const arg = rest[i];
1260
- if (arg === "--repo") {
1261
- repoArg = rest[++i];
1262
- }
1263
- else if (arg.startsWith("--repo=")) {
1264
- repoArg = arg.slice("--repo=".length);
1265
- }
1266
- else if (arg === "--title") {
1267
- titleArg = rest[++i];
1268
- }
1269
- else if (arg.startsWith("--title=")) {
1270
- titleArg = arg.slice("--title=".length);
1271
- }
1272
- else if (arg === "--done") {
1273
- markDone = true;
1274
- }
1275
- else if (!arg.startsWith("--")) {
1276
- positional.push(arg);
1277
- }
1278
- }
1279
- const match = positional.join(" ");
1280
- if (!match) {
1281
- console.error('Usage: phren task create-issue <project> "<text>" [--repo <owner/name>] [--title "<title>"] [--done]');
1282
- process.exit(1);
1283
- }
1284
- const phrenPath = getPhrenPath();
1285
- const resolved = resolveTaskItem(phrenPath, project, match);
1286
- if (!resolved.ok) {
1287
- console.error(resolved.error);
1288
- process.exit(1);
1289
- }
1290
- const targetRepo = repoArg || resolveProjectGithubRepo(phrenPath, project);
1291
- if (!targetRepo) {
1292
- console.error("Could not infer a GitHub repo. Provide --repo <owner/name> or add a GitHub URL to CLAUDE.md/summary.md.");
1293
- process.exit(1);
1294
- }
1295
- const created = createGithubIssueForTask({
1296
- repo: targetRepo,
1297
- title: titleArg?.trim() || resolved.data.line.replace(/\s*\[(high|medium|low)\]\s*$/i, "").trim(),
1298
- body: buildTaskIssueBody(project, resolved.data),
1299
- });
1300
- if (!created.ok) {
1301
- console.error(created.error);
1302
- process.exit(1);
1303
- }
1304
- const linked = linkTaskIssue(phrenPath, project, resolved.data.stableId ? `bid:${resolved.data.stableId}` : resolved.data.id, {
1305
- github_issue: created.data.issueNumber,
1306
- github_url: created.data.url,
1307
- });
1308
- if (!linked.ok) {
1309
- console.error(linked.error);
1310
- process.exit(1);
1311
- }
1312
- if (markDone) {
1313
- const completionMatch = linked.data.stableId ? `bid:${linked.data.stableId}` : linked.data.id;
1314
- const completed = completeTask(phrenPath, project, completionMatch);
1315
- if (!completed.ok) {
1316
- console.error(completed.error);
1317
- process.exit(1);
1318
- }
1319
- }
1320
- console.log(`Created GitHub issue ${created.data.issueNumber ? `#${created.data.issueNumber}` : created.data.url} for ${project} task.`);
1321
- return;
1322
- }
1323
- console.error(`Unknown task subcommand: ${subcommand}`);
1324
- printTaskUsage();
1325
- process.exit(1);
1326
- }
1327
- // ── Finding namespace ─────────────────────────────────────────────────────────
1328
- function printFindingUsage() {
1329
- console.log("Usage:");
1330
- console.log(' phren finding add <project> "<text>"');
1331
- console.log(' phren finding remove <project> "<text>"');
1332
- console.log(' phren finding supersede <project> "<text>" --by "<newer guidance>"');
1333
- console.log(' phren finding retract <project> "<text>" --reason "<reason>"');
1334
- console.log(' phren finding contradictions [project]');
1335
- console.log(' phren finding resolve <project> "<finding_text>" "<other_text>" <keep_a|keep_b|keep_both|retract_both>');
1336
- }
1337
- export async function handleFindingNamespace(args) {
1338
- const subcommand = args[0];
1339
- if (!subcommand || subcommand === "--help" || subcommand === "-h") {
1340
- printFindingUsage();
1341
- return;
1342
- }
1343
- if (subcommand === "list") {
1344
- const project = args[1];
1345
- if (!project) {
1346
- console.error("Usage: phren finding list <project>");
1347
- process.exit(1);
1348
- }
1349
- const phrenPath = getPhrenPath();
1350
- const { readFindings } = await import("../data/access.js");
1351
- const storePath = resolveProjectStorePath(phrenPath, project);
1352
- const result = readFindings(storePath, project);
1353
- if (!result.ok) {
1354
- console.error(result.error);
1355
- process.exit(1);
1356
- }
1357
- const items = result.data;
1358
- if (!items.length) {
1359
- console.log(`No findings found for "${project}".`);
1360
- return;
1361
- }
1362
- for (const entry of items.slice(0, 50)) {
1363
- console.log(`- [${entry.id}] ${entry.date}: ${entry.text}`);
1364
- }
1365
- return;
1366
- }
1367
- if (subcommand === "add") {
1368
- const project = args[1];
1369
- const text = args.slice(2).join(" ");
1370
- if (!project || !text) {
1371
- console.error('Usage: phren finding add <project> "<text>"');
1372
- process.exit(1);
1373
- }
1374
- const result = addFinding(getPhrenPath(), project, text);
1375
- if (!result.ok) {
1376
- console.error(result.message);
1377
- process.exit(1);
1378
- }
1379
- console.log(result.message);
1380
- return;
1381
- }
1382
- if (subcommand === "remove") {
1383
- const project = args[1];
1384
- const text = args.slice(2).join(" ");
1385
- if (!project || !text) {
1386
- console.error('Usage: phren finding remove <project> "<text>"');
1387
- process.exit(1);
1388
- }
1389
- const result = removeFinding(getPhrenPath(), project, text);
1390
- if (!result.ok) {
1391
- console.error(result.message);
1392
- process.exit(1);
1393
- }
1394
- console.log(result.message);
1395
- return;
1396
- }
1397
- if (subcommand === "supersede") {
1398
- const project = args[1];
1399
- if (!project) {
1400
- console.error('Usage: phren finding supersede <project> "<text>" --by "<newer guidance>"');
1401
- process.exit(1);
1402
- }
1403
- const rest = args.slice(2);
1404
- const byIdx = rest.indexOf("--by");
1405
- const byEqIdx = rest.findIndex(a => a.startsWith("--by="));
1406
- let text;
1407
- let byValue;
1408
- if (byEqIdx !== -1) {
1409
- byValue = rest[byEqIdx].slice("--by=".length);
1410
- text = rest.filter((_, i) => i !== byEqIdx && !rest[i].startsWith("--")).join(" ");
1411
- }
1412
- else if (byIdx !== -1) {
1413
- text = rest.slice(0, byIdx).join(" ");
1414
- byValue = rest.slice(byIdx + 1).join(" ");
1415
- }
1416
- else {
1417
- text = "";
1418
- byValue = "";
1419
- }
1420
- if (!text || !byValue) {
1421
- console.error('Usage: phren finding supersede <project> "<text>" --by "<newer guidance>"');
1422
- process.exit(1);
1423
- }
1424
- const result = supersedeFinding(getPhrenPath(), project, text, byValue);
1425
- if (!result.ok) {
1426
- console.error(result.error);
1427
- process.exit(1);
1428
- }
1429
- console.log(`Finding superseded: "${result.data.finding}" -> "${result.data.superseded_by}"`);
1430
- return;
1431
- }
1432
- if (subcommand === "retract") {
1433
- const project = args[1];
1434
- if (!project) {
1435
- console.error('Usage: phren finding retract <project> "<text>" --reason "<reason>"');
1436
- process.exit(1);
1437
- }
1438
- const rest = args.slice(2);
1439
- const reasonIdx = rest.indexOf("--reason");
1440
- const reasonEqIdx = rest.findIndex(a => a.startsWith("--reason="));
1441
- let text;
1442
- let reasonValue;
1443
- if (reasonEqIdx !== -1) {
1444
- reasonValue = rest[reasonEqIdx].slice("--reason=".length);
1445
- text = rest.filter((_, i) => i !== reasonEqIdx && !rest[i].startsWith("--")).join(" ");
1446
- }
1447
- else if (reasonIdx !== -1) {
1448
- text = rest.slice(0, reasonIdx).join(" ");
1449
- reasonValue = rest.slice(reasonIdx + 1).join(" ");
1450
- }
1451
- else {
1452
- text = "";
1453
- reasonValue = "";
1454
- }
1455
- if (!text || !reasonValue) {
1456
- console.error('Usage: phren finding retract <project> "<text>" --reason "<reason>"');
1457
- process.exit(1);
1458
- }
1459
- const result = retractFinding(getPhrenPath(), project, text, reasonValue);
1460
- if (!result.ok) {
1461
- console.error(result.error);
1462
- process.exit(1);
1463
- }
1464
- console.log(`Finding retracted: "${result.data.finding}" (reason: ${result.data.reason})`);
1465
- return;
1466
- }
1467
- if (subcommand === "contradictions") {
1468
- const project = args[1];
1469
- const phrenPath = getPhrenPath();
1470
- const RESERVED_DIRS = new Set(["global", ".runtime", ".sessions", ".config"]);
1471
- const { readFindings } = await import("../data/access.js");
1472
- const projects = project
1473
- ? [project]
1474
- : fs.readdirSync(phrenPath, { withFileTypes: true })
1475
- .filter((entry) => entry.isDirectory() && !RESERVED_DIRS.has(entry.name) && isValidProjectName(entry.name))
1476
- .map((entry) => entry.name);
1477
- const contradictions = [];
1478
- for (const p of projects) {
1479
- const result = readFindings(phrenPath, p);
1480
- if (!result.ok)
1481
- continue;
1482
- for (const finding of result.data) {
1483
- if (finding.status !== "contradicted")
1484
- continue;
1485
- contradictions.push({ project: p, id: finding.id, text: finding.text, date: finding.date, status_ref: finding.status_ref });
1486
- }
1487
- }
1488
- if (!contradictions.length) {
1489
- console.log("No unresolved contradictions found.");
1490
- return;
1491
- }
1492
- console.log(`${contradictions.length} unresolved contradiction(s):\n`);
1493
- for (const c of contradictions) {
1494
- console.log(`[${c.project}] ${c.date} ${c.id}`);
1495
- console.log(` ${c.text}`);
1496
- if (c.status_ref)
1497
- console.log(` contradicts: ${c.status_ref}`);
1498
- console.log("");
1499
- }
1500
- return;
1501
- }
1502
- if (subcommand === "resolve") {
1503
- const project = args[1];
1504
- const findingText = args[2];
1505
- const otherText = args[3];
1506
- const resolution = args[4];
1507
- const validResolutions = ["keep_a", "keep_b", "keep_both", "retract_both"];
1508
- if (!project || !findingText || !otherText || !resolution) {
1509
- console.error('Usage: phren finding resolve <project> "<finding_text>" "<other_text>" <keep_a|keep_b|keep_both|retract_both>');
1510
- process.exit(1);
1511
- }
1512
- if (!validResolutions.includes(resolution)) {
1513
- console.error(`Invalid resolution "${resolution}". Valid values: ${validResolutions.join(", ")}`);
1514
- process.exit(1);
1515
- }
1516
- const result = resolveFindingContradiction(getPhrenPath(), project, findingText, otherText, resolution);
1517
- if (!result.ok) {
1518
- console.error(result.error);
1519
- process.exit(1);
1520
- }
1521
- console.log(`Resolved contradiction in "${project}" with "${resolution}".`);
1522
- console.log(` finding_a: ${result.data.finding_a.text} → ${result.data.finding_a.status}`);
1523
- console.log(` finding_b: ${result.data.finding_b.text} → ${result.data.finding_b.status}`);
1524
- return;
1525
- }
1526
- console.error(`Unknown finding subcommand: ${subcommand}`);
1527
- printFindingUsage();
1528
- process.exit(1);
1529
- }
1530
- // ── Store namespace ──────────────────────────────────────────────────────────
1531
- function printStoreUsage() {
1532
- console.log("Usage:");
1533
- console.log(" phren store list List registered stores");
1534
- console.log(" phren store add <name> --remote <url> Add a team store");
1535
- console.log(" phren store remove <name> Remove a store (local only)");
1536
- console.log(" phren store sync Pull all stores");
1537
- console.log(" phren store activity [--limit N] Recent team findings");
1538
- console.log(" phren store subscribe <name> <project...> Subscribe store to projects");
1539
- console.log(" phren store unsubscribe <name> <project...> Unsubscribe store from projects");
1540
- }
1541
- export async function handleStoreNamespace(args) {
1542
- const subcommand = args[0];
1543
- if (!subcommand || subcommand === "--help" || subcommand === "-h") {
1544
- printStoreUsage();
1545
- return;
1546
- }
1547
- const phrenPath = getPhrenPath();
1548
- if (subcommand === "list") {
1549
- const stores = resolveAllStores(phrenPath);
1550
- if (stores.length === 0) {
1551
- console.log("No stores registered.");
1552
- return;
1553
- }
1554
- console.log(`${stores.length} store(s):\n`);
1555
- for (const store of stores) {
1556
- const exists = fs.existsSync(store.path) ? "ok" : "MISSING";
1557
- const syncInfo = store.remote ?? "(local)";
1558
- const projectCount = countStoreProjects(store);
1559
- console.log(` ${store.name} (${store.role})`);
1560
- console.log(` id: ${store.id}`);
1561
- console.log(` path: ${store.path} [${exists}]`);
1562
- console.log(` remote: ${syncInfo}`);
1563
- console.log(` sync: ${store.sync}`);
1564
- console.log(` projects: ${projectCount}`);
1565
- // Show last sync status if available
1566
- const health = readHealthForStore(store.path);
1567
- if (health) {
1568
- console.log(` last sync: ${health}`);
1569
- }
1570
- console.log();
1571
- }
1572
- return;
1573
- }
1574
- if (subcommand === "add") {
1575
- const name = args[1];
1576
- if (!name) {
1577
- console.error("Usage: phren store add <name> --remote <url> [--role team|readonly]");
1578
- process.exit(1);
1579
- }
1580
- // Validate store name to prevent path traversal
1581
- if (!isValidProjectName(name)) {
1582
- console.error(`Invalid store name: "${name}". Use lowercase letters, numbers, and hyphens.`);
1583
- process.exit(1);
1584
- }
1585
- const remote = getOptionValue(args.slice(2), "--remote");
1586
- if (!remote) {
1587
- console.error("--remote <url> is required. Provide the git clone URL for the team store.");
1588
- process.exit(1);
1589
- }
1590
- // Prevent git option injection via --remote
1591
- if (remote.startsWith("-")) {
1592
- console.error(`Invalid remote URL: "${remote}". URLs must not start with "-".`);
1593
- process.exit(1);
1594
- }
1595
- const roleArg = getOptionValue(args.slice(2), "--role") ?? "team";
1596
- if (roleArg !== "team" && roleArg !== "readonly") {
1597
- console.error(`Invalid role: "${roleArg}". Use "team" or "readonly".`);
1598
- process.exit(1);
1599
- }
1600
- const storesDir = path.join(path.dirname(phrenPath), ".phren-stores");
1601
- const storePath = path.join(storesDir, name);
1602
- if (fs.existsSync(storePath)) {
1603
- console.error(`Directory already exists: ${storePath}`);
1604
- process.exit(1);
1605
- }
1606
- // Clone the remote
1607
- console.log(`Cloning ${remote} into ${storePath}...`);
1608
- try {
1609
- fs.mkdirSync(storesDir, { recursive: true });
1610
- execFileSync("git", ["clone", "--", remote, storePath], {
1611
- stdio: "inherit",
1612
- timeout: 60_000,
1613
- });
1614
- }
1615
- catch (err) {
1616
- console.error(`Clone failed: ${errorMessage(err)}`);
1617
- process.exit(1);
1618
- }
1619
- // Read .phren-team.yaml if present
1620
- const bootstrap = readTeamBootstrap(storePath);
1621
- const storeName = bootstrap?.name ?? name;
1622
- const storeRole = bootstrap?.default_role ?? roleArg;
1623
- const entry = {
1624
- id: generateStoreId(),
1625
- name: storeName,
1626
- path: storePath,
1627
- role: storeRole === "primary" ? "team" : storeRole, // Never allow adding a second primary
1628
- sync: storeRole === "readonly" ? "pull-only" : "managed-git",
1629
- remote,
1630
- };
1631
- try {
1632
- addStoreToRegistry(phrenPath, entry);
1633
- }
1634
- catch (err) {
1635
- console.error(`Failed to register store: ${errorMessage(err)}`);
1636
- process.exit(1);
1637
- }
1638
- console.log(`\nStore "${storeName}" added (${entry.role}).`);
1639
- console.log(` id: ${entry.id}`);
1640
- console.log(` path: ${storePath}`);
1641
- return;
1642
- }
1643
- if (subcommand === "remove") {
1644
- const name = args[1];
1645
- if (!name) {
1646
- console.error("Usage: phren store remove <name>");
1647
- process.exit(1);
1648
- }
1649
- try {
1650
- const removed = removeStoreFromRegistry(phrenPath, name);
1651
- console.log(`Store "${name}" removed from registry.`);
1652
- console.log(` Local directory preserved at: ${removed.path}`);
1653
- console.log(` To delete: rm -rf "${removed.path}"`);
1654
- }
1655
- catch (err) {
1656
- console.error(`${errorMessage(err)}`);
1657
- process.exit(1);
1658
- }
1659
- return;
1660
- }
1661
- if (subcommand === "activity") {
1662
- const stores = resolveAllStores(phrenPath);
1663
- const teamStores = stores.filter((s) => s.role === "team");
1664
- if (teamStores.length === 0) {
1665
- console.log("No team stores registered. Add one with: phren store add <name> --remote <url>");
1666
- return;
1667
- }
1668
- const { readTeamJournalEntries } = await import("../finding/journal.js");
1669
- const limit = Number(getOptionValue(args.slice(1), "--limit") ?? "20");
1670
- const allEntries = [];
1671
- for (const store of teamStores) {
1672
- if (!fs.existsSync(store.path))
1673
- continue;
1674
- const { getStoreProjectDirs } = await import("../store-registry.js");
1675
- const projectDirs = getStoreProjectDirs(store);
1676
- for (const dir of projectDirs) {
1677
- const projectName = path.basename(dir);
1678
- const journalEntries = readTeamJournalEntries(store.path, projectName);
1679
- for (const je of journalEntries) {
1680
- for (const entry of je.entries) {
1681
- allEntries.push({ store: store.name, project: projectName, date: je.date, actor: je.actor, entry });
1682
- }
1683
- }
1684
- }
1685
- }
1686
- allEntries.sort((a, b) => b.date.localeCompare(a.date));
1687
- const capped = allEntries.slice(0, limit);
1688
- if (capped.length === 0) {
1689
- console.log("No team activity yet.");
1690
- return;
1691
- }
1692
- console.log(`Team activity (${capped.length}/${allEntries.length}):\n`);
1693
- let lastDate = "";
1694
- for (const e of capped) {
1695
- if (e.date !== lastDate) {
1696
- console.log(`## ${e.date}`);
1697
- lastDate = e.date;
1698
- }
1699
- console.log(` [${e.store}/${e.project}] ${e.actor}: ${e.entry}`);
1700
- }
1701
- return;
1702
- }
1703
- if (subcommand === "sync") {
1704
- const stores = resolveAllStores(phrenPath);
1705
- let hasErrors = false;
1706
- for (const store of stores) {
1707
- if (!fs.existsSync(store.path)) {
1708
- console.log(` ${store.name}: SKIP (path missing)`);
1709
- continue;
1710
- }
1711
- const gitDir = path.join(store.path, ".git");
1712
- if (!fs.existsSync(gitDir)) {
1713
- console.log(` ${store.name}: SKIP (not a git repo)`);
1714
- continue;
1715
- }
1716
- try {
1717
- execFileSync("git", ["pull", "--rebase", "--quiet"], {
1718
- cwd: store.path,
1719
- stdio: "pipe",
1720
- timeout: 30_000,
1721
- });
1722
- // Re-apply sparse-checkout after pull on primary store to avoid materializing all files
1723
- if (store.role === "primary") {
1724
- try {
1725
- const sparseList = execFileSync("git", ["sparse-checkout", "list"], {
1726
- cwd: store.path,
1727
- stdio: "pipe",
1728
- timeout: 10_000,
1729
- }).toString().trim();
1730
- if (sparseList) {
1731
- execFileSync("git", ["sparse-checkout", "reapply"], {
1732
- cwd: store.path,
1733
- stdio: "pipe",
1734
- timeout: 10_000,
1735
- });
1736
- }
1737
- }
1738
- catch {
1739
- // sparse-checkout not configured — nothing to reapply
1740
- }
1741
- }
1742
- console.log(` ${store.name}: ok`);
1743
- }
1744
- catch (err) {
1745
- console.log(` ${store.name}: FAILED (${errorMessage(err).split("\n")[0]})`);
1746
- hasErrors = true;
1747
- }
1748
- }
1749
- if (hasErrors) {
1750
- console.error("\nSome stores failed to sync. Run 'phren doctor' for details.");
1751
- }
1752
- return;
1753
- }
1754
- if (subcommand === "subscribe") {
1755
- const storeName = args[1];
1756
- const projects = args.slice(2);
1757
- if (!storeName || projects.length === 0) {
1758
- console.error("Usage: phren store subscribe <store-name> <project1> [project2...]");
1759
- process.exit(1);
1760
- }
1761
- try {
1762
- const { subscribeStoreProjects } = await import("../store-registry.js");
1763
- subscribeStoreProjects(phrenPath, storeName, projects);
1764
- console.log(`Added ${projects.length} project(s) to "${storeName}"`);
1765
- }
1766
- catch (err) {
1767
- console.error(`Failed to subscribe: ${errorMessage(err)}`);
1768
- process.exit(1);
1769
- }
1770
- return;
1771
- }
1772
- if (subcommand === "unsubscribe") {
1773
- const storeName = args[1];
1774
- const projects = args.slice(2);
1775
- if (!storeName || projects.length === 0) {
1776
- console.error("Usage: phren store unsubscribe <store-name> <project1> [project2...]");
1777
- process.exit(1);
1778
- }
1779
- try {
1780
- const { unsubscribeStoreProjects } = await import("../store-registry.js");
1781
- unsubscribeStoreProjects(phrenPath, storeName, projects);
1782
- console.log(`Removed ${projects.length} project(s) from "${storeName}"`);
1783
- }
1784
- catch (err) {
1785
- console.error(`Failed to unsubscribe: ${errorMessage(err)}`);
1786
- process.exit(1);
1787
- }
1788
- return;
1789
- }
1790
- console.error(`Unknown store subcommand: ${subcommand}`);
1791
- printStoreUsage();
1792
- process.exit(1);
1793
- }
1794
- // ── Profile namespace ────────────────────────────────────────────────────────
1795
- function printProfileUsage() {
1796
- console.log("Usage:");
1797
- console.log(" phren profile list List all available profiles");
1798
- console.log(" phren profile switch <name> Switch to an active profile");
1799
- }
1800
- export function handleProfileNamespace(args) {
1801
- const subcommand = args[0];
1802
- if (!subcommand || subcommand === "--help" || subcommand === "-h") {
1803
- printProfileUsage();
1804
- return;
1805
- }
1806
- const phrenPath = getPhrenPath();
1807
- if (subcommand === "list") {
1808
- const { listProfiles } = require("../profile-store.js");
1809
- const result = listProfiles(phrenPath);
1810
- if (!result.ok) {
1811
- console.error(`Failed to list profiles: ${result.error}`);
1812
- process.exit(1);
1813
- }
1814
- const profiles = result.data || [];
1815
- if (profiles.length === 0) {
1816
- console.log("No profiles available.");
1817
- return;
1818
- }
1819
- const { listMachines } = require("../profile-store.js");
1820
- const machinesResult = listMachines(phrenPath);
1821
- const machines = machinesResult.ok ? machinesResult.data : {};
1822
- const { getMachineName } = require("../machine-identity.js");
1823
- const currentMachine = getMachineName();
1824
- const activeProfile = machines[currentMachine];
1825
- console.log(`${profiles.length} profile(s):\n`);
1826
- for (const profile of profiles) {
1827
- const isCurrent = profile.name === activeProfile ? " (current)" : "";
1828
- const projectCount = profile.projects?.length ?? 0;
1829
- console.log(` ${profile.name}${isCurrent}`);
1830
- console.log(` projects: ${projectCount}`);
1831
- console.log();
1832
- }
1833
- return;
1834
- }
1835
- if (subcommand === "switch") {
1836
- const profileName = args[1];
1837
- if (!profileName) {
1838
- console.error("Usage: phren profile switch <name>");
1839
- process.exit(1);
1840
- }
1841
- const { setMachineProfile, getDefaultMachineAlias, listProfiles } = require("../profile-store.js");
1842
- // Validate that profile exists
1843
- const listResult = listProfiles(phrenPath);
1844
- if (!listResult.ok) {
1845
- console.error(`Failed to list profiles: ${listResult.error}`);
1846
- process.exit(1);
1847
- }
1848
- const profiles = listResult.data || [];
1849
- if (!profiles.some((p) => p.name === profileName)) {
1850
- console.error(`Profile not found: "${profileName}"`);
1851
- console.log("Available profiles:");
1852
- for (const p of profiles) {
1853
- console.log(` - ${p.name}`);
1854
- }
1855
- process.exit(1);
1856
- }
1857
- const machineAlias = getDefaultMachineAlias();
1858
- const result = setMachineProfile(phrenPath, machineAlias, profileName);
1859
- if (!result.ok) {
1860
- console.error(`Failed to switch profile: ${result.error}`);
1861
- process.exit(1);
1862
- }
1863
- console.log(`Switched to profile: ${profileName} (machine: ${machineAlias})`);
1864
- return;
1865
- }
1866
- console.error(`Unknown profile subcommand: ${subcommand}`);
1867
- printProfileUsage();
1868
- process.exit(1);
1869
- }
1870
- // ── Promote namespace ────────────────────────────────────────────────────────
1871
- export async function handlePromoteNamespace(args) {
1872
- if (!args[0] || args[0] === "--help" || args[0] === "-h") {
1873
- console.log("Usage:");
1874
- console.log(' phren promote <project> "finding text..." --to <store>');
1875
- console.log(" Copies a finding from the primary store to a team store.");
1876
- return;
1877
- }
1878
- const phrenPath = getPhrenPath();
1879
- const project = args[0];
1880
- if (!isValidProjectName(project)) {
1881
- console.error(`Invalid project name: "${project}"`);
1882
- process.exit(1);
1883
- }
1884
- const toStore = getOptionValue(args.slice(1), "--to");
1885
- if (!toStore) {
1886
- console.error("--to <store> is required. Specify the target team store.");
1887
- process.exit(1);
1888
- }
1889
- // Everything between project and --to is the finding text
1890
- const toIdx = args.indexOf("--to");
1891
- const findingText = args.slice(1, toIdx !== -1 ? toIdx : undefined).join(" ").trim();
1892
- if (!findingText) {
1893
- console.error("Finding text is required.");
1894
- process.exit(1);
1895
- }
1896
- const stores = resolveAllStores(phrenPath);
1897
- const targetStore = stores.find((s) => s.name === toStore);
1898
- if (!targetStore) {
1899
- const available = stores.map((s) => s.name).join(", ");
1900
- console.error(`Store "${toStore}" not found. Available: ${available}`);
1901
- process.exit(1);
1902
- }
1903
- if (targetStore.role === "readonly") {
1904
- console.error(`Store "${toStore}" is read-only.`);
1905
- process.exit(1);
1906
- }
1907
- if (targetStore.role === "primary") {
1908
- console.error(`Cannot promote to primary store — finding is already there.`);
1909
- process.exit(1);
1910
- }
1911
- // Find the matching finding in the primary store
1912
- const { readFindings } = await import("../data/access.js");
1913
- const findingsResult = readFindings(phrenPath, project);
1914
- if (!findingsResult.ok) {
1915
- console.error(`Could not read findings for project "${project}".`);
1916
- process.exit(1);
1917
- }
1918
- const match = findingsResult.data.find((item) => item.text.includes(findingText) || findingText.includes(item.text));
1919
- if (!match) {
1920
- console.error(`No finding matching "${findingText.slice(0, 80)}..." found in ${project}.`);
1921
- process.exit(1);
1922
- }
1923
- // Write to target store
1924
- const targetProjectDir = path.join(targetStore.path, project);
1925
- fs.mkdirSync(targetProjectDir, { recursive: true });
1926
- const { addFindingToFile } = await import("../shared/content.js");
1927
- const result = addFindingToFile(targetStore.path, project, match.text);
1928
- if (!result.ok) {
1929
- console.error(`Failed to add finding to ${toStore}: ${result.error}`);
1930
- process.exit(1);
1931
- }
1932
- console.log(`Promoted to ${toStore}/${project}:`);
1933
- console.log(` "${match.text.slice(0, 120)}${match.text.length > 120 ? "..." : ""}"`);
1934
- }
1935
- // ── Review namespace ────────────────────────────────────────────────────────
1936
- export async function handleReviewNamespace(args) {
1937
- const subcommand = args[0];
1938
- if (!subcommand || subcommand === "--help" || subcommand === "-h") {
1939
- console.log("Usage:");
1940
- console.log(" phren review [project] Show review queue items");
1941
- console.log(" phren review approve <project> <text> Approve and remove item");
1942
- console.log(" phren review reject <project> <text> Reject and remove item");
1943
- console.log("");
1944
- console.log("Examples:");
1945
- console.log(' phren review myproject');
1946
- console.log(' phren review approve myproject "Always validate input"');
1947
- console.log(' phren review reject myproject "Avoid async in loops"');
1948
- return;
1949
- }
1950
- // Handle "approve" and "reject" subcommands
1951
- if (subcommand === "approve" || subcommand === "reject") {
1952
- const action = subcommand;
1953
- const project = args[1];
1954
- const lineText = args.slice(2).join(" ");
1955
- if (!project || !lineText) {
1956
- console.error(`Usage: phren review ${action} <project> <text>`);
1957
- process.exit(1);
1958
- }
1959
- if (!isValidProjectName(project)) {
1960
- console.error(`Invalid project name: "${project}".`);
1961
- process.exit(1);
1962
- }
1963
- const phrenPath = getPhrenPath();
1964
- const { approveQueueItem, rejectQueueItem } = await import("../data/access.js");
1965
- const result = action === "approve"
1966
- ? approveQueueItem(phrenPath, project, lineText)
1967
- : rejectQueueItem(phrenPath, project, lineText);
1968
- if (!result.ok) {
1969
- console.error(`Failed to ${action} item: ${result.error ?? "Unknown error"}`);
1970
- process.exit(1);
1971
- }
1972
- console.log(`${action === "approve" ? "✓ Approved" : "✗ Rejected"}: ${lineText.slice(0, 100)}${lineText.length > 100 ? "..." : ""}`);
1973
- return;
1974
- }
1975
- // Default: show review queue (first arg is project name if not a subcommand)
1976
- const { handleReview } = await import("./actions.js");
1977
- return handleReview(args);
1978
- }
1979
- function countStoreProjects(store) {
1980
- if (!fs.existsSync(store.path))
1981
- return 0;
1982
- try {
1983
- const storeRegistry = require("../store-registry.js");
1984
- return storeRegistry.getStoreProjectDirs(store).length;
1985
- }
1986
- catch {
1987
- return 0;
1988
- }
1989
- }
1990
- function readHealthForStore(storePath) {
1991
- try {
1992
- const healthPath = path.join(storePath, ".runtime", "health.json");
1993
- if (!fs.existsSync(healthPath))
1994
- return null;
1995
- const raw = JSON.parse(fs.readFileSync(healthPath, "utf8"));
1996
- const lastSync = raw?.lastSync;
1997
- if (!lastSync)
1998
- return null;
1999
- const parts = [];
2000
- if (lastSync.lastPullStatus)
2001
- parts.push(`pull=${lastSync.lastPullStatus}`);
2002
- if (lastSync.lastPushStatus)
2003
- parts.push(`push=${lastSync.lastPushStatus}`);
2004
- if (lastSync.lastSuccessfulPullAt)
2005
- parts.push(`at=${lastSync.lastSuccessfulPullAt.slice(0, 16)}`);
2006
- return parts.join(", ") || null;
2007
- }
2008
- catch {
2009
- return null;
2010
- }
2011
- }
1
+ // Re-export all namespace handlers from domain-specific modules
2
+ export { handleSkillsNamespace, handleHooksNamespace, handleSkillList, handleDetectSkills } from "./namespaces-skills.js";
3
+ export { handleProjectsNamespace } from "./namespaces-projects.js";
4
+ export { handleTaskNamespace } from "./namespaces-tasks.js";
5
+ export { handleFindingNamespace } from "./namespaces-findings.js";
6
+ export { handleStoreNamespace, handlePromoteNamespace } from "./namespaces-store.js";
7
+ export { handleProfileNamespace } from "./namespaces-profile.js";
8
+ export { handleReviewNamespace } from "./namespaces-review.js";