@phren/cli 0.1.12 → 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 (37) 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 -1991
  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/profile-store.js +14 -1
  34. package/dist/shared/index.js +22 -3
  35. package/dist/shared/retrieval.js +10 -9
  36. package/dist/tools/search.js +1 -1
  37. package/package.json +1 -1
@@ -0,0 +1 @@
1
+ export declare function handleFindingNamespace(args: string[]): Promise<void>;
@@ -0,0 +1,208 @@
1
+ import * as fs from "fs";
2
+ import { getPhrenPath } from "../shared.js";
3
+ import { isValidProjectName } from "../utils.js";
4
+ import { addFinding, removeFinding } from "../core/finding.js";
5
+ import { supersedeFinding, retractFinding, resolveFindingContradiction } from "../finding/lifecycle.js";
6
+ import { resolveProjectStorePath } from "./namespaces-utils.js";
7
+ function printFindingUsage() {
8
+ console.log("Usage:");
9
+ console.log(' phren finding add <project> "<text>"');
10
+ console.log(' phren finding remove <project> "<text>"');
11
+ console.log(' phren finding supersede <project> "<text>" --by "<newer guidance>"');
12
+ console.log(' phren finding retract <project> "<text>" --reason "<reason>"');
13
+ console.log(' phren finding contradictions [project]');
14
+ console.log(' phren finding resolve <project> "<finding_text>" "<other_text>" <keep_a|keep_b|keep_both|retract_both>');
15
+ }
16
+ export async function handleFindingNamespace(args) {
17
+ const subcommand = args[0];
18
+ if (!subcommand || subcommand === "--help" || subcommand === "-h") {
19
+ printFindingUsage();
20
+ return;
21
+ }
22
+ if (subcommand === "list") {
23
+ const project = args[1];
24
+ if (!project) {
25
+ console.error("Usage: phren finding list <project>");
26
+ process.exit(1);
27
+ }
28
+ const phrenPath = getPhrenPath();
29
+ const { readFindings } = await import("../data/access.js");
30
+ const storePath = resolveProjectStorePath(phrenPath, project);
31
+ const result = readFindings(storePath, project);
32
+ if (!result.ok) {
33
+ console.error(result.error);
34
+ process.exit(1);
35
+ }
36
+ const items = result.data;
37
+ if (!items.length) {
38
+ console.log(`No findings found for "${project}".`);
39
+ return;
40
+ }
41
+ for (const entry of items.slice(0, 50)) {
42
+ console.log(`- [${entry.id}] ${entry.date}: ${entry.text}`);
43
+ }
44
+ return;
45
+ }
46
+ if (subcommand === "add") {
47
+ const project = args[1];
48
+ const text = args.slice(2).join(" ");
49
+ if (!project || !text) {
50
+ console.error('Usage: phren finding add <project> "<text>"');
51
+ process.exit(1);
52
+ }
53
+ const result = addFinding(getPhrenPath(), project, text);
54
+ if (!result.ok) {
55
+ console.error(result.message);
56
+ process.exit(1);
57
+ }
58
+ console.log(result.message);
59
+ return;
60
+ }
61
+ if (subcommand === "remove") {
62
+ const project = args[1];
63
+ const text = args.slice(2).join(" ");
64
+ if (!project || !text) {
65
+ console.error('Usage: phren finding remove <project> "<text>"');
66
+ process.exit(1);
67
+ }
68
+ const result = removeFinding(getPhrenPath(), project, text);
69
+ if (!result.ok) {
70
+ console.error(result.message);
71
+ process.exit(1);
72
+ }
73
+ console.log(result.message);
74
+ return;
75
+ }
76
+ if (subcommand === "supersede") {
77
+ const project = args[1];
78
+ if (!project) {
79
+ console.error('Usage: phren finding supersede <project> "<text>" --by "<newer guidance>"');
80
+ process.exit(1);
81
+ }
82
+ const rest = args.slice(2);
83
+ const byIdx = rest.indexOf("--by");
84
+ const byEqIdx = rest.findIndex(a => a.startsWith("--by="));
85
+ let text;
86
+ let byValue;
87
+ if (byEqIdx !== -1) {
88
+ byValue = rest[byEqIdx].slice("--by=".length);
89
+ text = rest.filter((_, i) => i !== byEqIdx && !rest[i].startsWith("--")).join(" ");
90
+ }
91
+ else if (byIdx !== -1) {
92
+ text = rest.slice(0, byIdx).join(" ");
93
+ byValue = rest.slice(byIdx + 1).join(" ");
94
+ }
95
+ else {
96
+ text = "";
97
+ byValue = "";
98
+ }
99
+ if (!text || !byValue) {
100
+ console.error('Usage: phren finding supersede <project> "<text>" --by "<newer guidance>"');
101
+ process.exit(1);
102
+ }
103
+ const result = supersedeFinding(getPhrenPath(), project, text, byValue);
104
+ if (!result.ok) {
105
+ console.error(result.error);
106
+ process.exit(1);
107
+ }
108
+ console.log(`Finding superseded: "${result.data.finding}" -> "${result.data.superseded_by}"`);
109
+ return;
110
+ }
111
+ if (subcommand === "retract") {
112
+ const project = args[1];
113
+ if (!project) {
114
+ console.error('Usage: phren finding retract <project> "<text>" --reason "<reason>"');
115
+ process.exit(1);
116
+ }
117
+ const rest = args.slice(2);
118
+ const reasonIdx = rest.indexOf("--reason");
119
+ const reasonEqIdx = rest.findIndex(a => a.startsWith("--reason="));
120
+ let text;
121
+ let reasonValue;
122
+ if (reasonEqIdx !== -1) {
123
+ reasonValue = rest[reasonEqIdx].slice("--reason=".length);
124
+ text = rest.filter((_, i) => i !== reasonEqIdx && !rest[i].startsWith("--")).join(" ");
125
+ }
126
+ else if (reasonIdx !== -1) {
127
+ text = rest.slice(0, reasonIdx).join(" ");
128
+ reasonValue = rest.slice(reasonIdx + 1).join(" ");
129
+ }
130
+ else {
131
+ text = "";
132
+ reasonValue = "";
133
+ }
134
+ if (!text || !reasonValue) {
135
+ console.error('Usage: phren finding retract <project> "<text>" --reason "<reason>"');
136
+ process.exit(1);
137
+ }
138
+ const result = retractFinding(getPhrenPath(), project, text, reasonValue);
139
+ if (!result.ok) {
140
+ console.error(result.error);
141
+ process.exit(1);
142
+ }
143
+ console.log(`Finding retracted: "${result.data.finding}" (reason: ${result.data.reason})`);
144
+ return;
145
+ }
146
+ if (subcommand === "contradictions") {
147
+ const project = args[1];
148
+ const phrenPath = getPhrenPath();
149
+ const RESERVED_DIRS = new Set(["global", ".runtime", ".sessions", ".config"]);
150
+ const { readFindings } = await import("../data/access.js");
151
+ const projects = project
152
+ ? [project]
153
+ : fs.readdirSync(phrenPath, { withFileTypes: true })
154
+ .filter((entry) => entry.isDirectory() && !RESERVED_DIRS.has(entry.name) && isValidProjectName(entry.name))
155
+ .map((entry) => entry.name);
156
+ const contradictions = [];
157
+ for (const p of projects) {
158
+ const result = readFindings(phrenPath, p);
159
+ if (!result.ok)
160
+ continue;
161
+ for (const finding of result.data) {
162
+ if (finding.status !== "contradicted")
163
+ continue;
164
+ contradictions.push({ project: p, id: finding.id, text: finding.text, date: finding.date, status_ref: finding.status_ref });
165
+ }
166
+ }
167
+ if (!contradictions.length) {
168
+ console.log("No unresolved contradictions found.");
169
+ return;
170
+ }
171
+ console.log(`${contradictions.length} unresolved contradiction(s):\n`);
172
+ for (const c of contradictions) {
173
+ console.log(`[${c.project}] ${c.date} ${c.id}`);
174
+ console.log(` ${c.text}`);
175
+ if (c.status_ref)
176
+ console.log(` contradicts: ${c.status_ref}`);
177
+ console.log("");
178
+ }
179
+ return;
180
+ }
181
+ if (subcommand === "resolve") {
182
+ const project = args[1];
183
+ const findingText = args[2];
184
+ const otherText = args[3];
185
+ const resolution = args[4];
186
+ const validResolutions = ["keep_a", "keep_b", "keep_both", "retract_both"];
187
+ if (!project || !findingText || !otherText || !resolution) {
188
+ console.error('Usage: phren finding resolve <project> "<finding_text>" "<other_text>" <keep_a|keep_b|keep_both|retract_both>');
189
+ process.exit(1);
190
+ }
191
+ if (!validResolutions.includes(resolution)) {
192
+ console.error(`Invalid resolution "${resolution}". Valid values: ${validResolutions.join(", ")}`);
193
+ process.exit(1);
194
+ }
195
+ const result = resolveFindingContradiction(getPhrenPath(), project, findingText, otherText, resolution);
196
+ if (!result.ok) {
197
+ console.error(result.error);
198
+ process.exit(1);
199
+ }
200
+ console.log(`Resolved contradiction in "${project}" with "${resolution}".`);
201
+ console.log(` finding_a: ${result.data.finding_a.text} → ${result.data.finding_a.status}`);
202
+ console.log(` finding_b: ${result.data.finding_b.text} → ${result.data.finding_b.status}`);
203
+ return;
204
+ }
205
+ console.error(`Unknown finding subcommand: ${subcommand}`);
206
+ printFindingUsage();
207
+ process.exit(1);
208
+ }
@@ -0,0 +1 @@
1
+ export declare function handleProfileNamespace(args: string[]): void;
@@ -0,0 +1,76 @@
1
+ import { getPhrenPath } from "../shared.js";
2
+ function printProfileUsage() {
3
+ console.log("Usage:");
4
+ console.log(" phren profile list List all available profiles");
5
+ console.log(" phren profile switch <name> Switch to an active profile");
6
+ }
7
+ export function handleProfileNamespace(args) {
8
+ const subcommand = args[0];
9
+ if (!subcommand || subcommand === "--help" || subcommand === "-h") {
10
+ printProfileUsage();
11
+ return;
12
+ }
13
+ const phrenPath = getPhrenPath();
14
+ if (subcommand === "list") {
15
+ const { listProfiles } = require("../profile-store.js");
16
+ const result = listProfiles(phrenPath);
17
+ if (!result.ok) {
18
+ console.error(`Failed to list profiles: ${result.error}`);
19
+ process.exit(1);
20
+ }
21
+ const profiles = result.data || [];
22
+ if (profiles.length === 0) {
23
+ console.log("No profiles available.");
24
+ return;
25
+ }
26
+ const { listMachines } = require("../profile-store.js");
27
+ const machinesResult = listMachines(phrenPath);
28
+ const machines = machinesResult.ok ? machinesResult.data : {};
29
+ const { getMachineName } = require("../machine-identity.js");
30
+ const currentMachine = getMachineName();
31
+ const activeProfile = machines[currentMachine];
32
+ console.log(`${profiles.length} profile(s):\n`);
33
+ for (const profile of profiles) {
34
+ const isCurrent = profile.name === activeProfile ? " (current)" : "";
35
+ const projectCount = profile.projects?.length ?? 0;
36
+ console.log(` ${profile.name}${isCurrent}`);
37
+ console.log(` projects: ${projectCount}`);
38
+ console.log();
39
+ }
40
+ return;
41
+ }
42
+ if (subcommand === "switch") {
43
+ const profileName = args[1];
44
+ if (!profileName) {
45
+ console.error("Usage: phren profile switch <name>");
46
+ process.exit(1);
47
+ }
48
+ const { setMachineProfile, getDefaultMachineAlias, listProfiles } = require("../profile-store.js");
49
+ // Validate that profile exists
50
+ const listResult = listProfiles(phrenPath);
51
+ if (!listResult.ok) {
52
+ console.error(`Failed to list profiles: ${listResult.error}`);
53
+ process.exit(1);
54
+ }
55
+ const profiles = listResult.data || [];
56
+ if (!profiles.some((p) => p.name === profileName)) {
57
+ console.error(`Profile not found: "${profileName}"`);
58
+ console.log("Available profiles:");
59
+ for (const p of profiles) {
60
+ console.log(` - ${p.name}`);
61
+ }
62
+ process.exit(1);
63
+ }
64
+ const machineAlias = getDefaultMachineAlias();
65
+ const result = setMachineProfile(phrenPath, machineAlias, profileName);
66
+ if (!result.ok) {
67
+ console.error(`Failed to switch profile: ${result.error}`);
68
+ process.exit(1);
69
+ }
70
+ console.log(`Switched to profile: ${profileName} (machine: ${machineAlias})`);
71
+ return;
72
+ }
73
+ console.error(`Unknown profile subcommand: ${subcommand}`);
74
+ printProfileUsage();
75
+ process.exit(1);
76
+ }
@@ -0,0 +1 @@
1
+ export declare function handleProjectsNamespace(args: string[], profile: string): Promise<void>;
@@ -0,0 +1,370 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ import { expandHomePath, findArchivedProjectNameCaseInsensitive, findProjectNameCaseInsensitive, getPhrenPath, getProjectDirs, normalizeProjectNameForCreate, readRootManifest, } from "../shared.js";
4
+ import { isValidProjectName, errorMessage } from "../utils.js";
5
+ import { logger } from "../logger.js";
6
+ import { TASK_FILE_ALIASES } from "../data/tasks.js";
7
+ import { PROJECT_OWNERSHIP_MODES, parseProjectOwnershipMode, writeProjectConfig, writeProjectHookConfig, } from "../project-config.js";
8
+ import { resolveProjectStorePath, parseMcpToggle } from "./namespaces-utils.js";
9
+ export async function handleProjectsNamespace(args, profile) {
10
+ const subcommand = args[0];
11
+ if (!subcommand || subcommand === "list" || subcommand === "--help" || subcommand === "-h") {
12
+ if (subcommand === "--help" || subcommand === "-h") {
13
+ console.log("Usage:");
14
+ console.log(" phren projects list List all projects");
15
+ console.log(" phren projects configure <name> Update per-project enrollment settings");
16
+ console.log(" flags: --ownership=<mode> --hooks=on|off");
17
+ console.log(" phren projects remove <name> Remove a project (asks for confirmation)");
18
+ console.log(" phren projects export <name> Export project data as JSON to stdout");
19
+ console.log(" phren projects import <file> Import project from a JSON file");
20
+ console.log(" phren projects archive <name> Archive a project (removes from active index)");
21
+ console.log(" phren projects unarchive <name> Restore an archived project");
22
+ return;
23
+ }
24
+ return handleProjectsList(profile);
25
+ }
26
+ if (subcommand === "add") {
27
+ console.error("`phren projects add` has been removed from the supported workflow.");
28
+ console.error("Use `cd ~/your-project && phren add` so enrollment stays path-based.");
29
+ process.exit(1);
30
+ }
31
+ if (subcommand === "remove") {
32
+ const manifest = readRootManifest(getPhrenPath());
33
+ if (manifest?.installMode === "project-local") {
34
+ console.error("projects remove is unsupported in project-local mode. Use `phren uninstall`.");
35
+ process.exit(1);
36
+ }
37
+ const name = args[1];
38
+ if (!name) {
39
+ console.error("Usage: phren projects remove <name>");
40
+ process.exit(1);
41
+ }
42
+ return handleProjectsRemove(name, profile);
43
+ }
44
+ if (subcommand === "configure") {
45
+ const name = args[1];
46
+ if (!name) {
47
+ console.error(`Usage: phren projects configure <name> [--ownership=${PROJECT_OWNERSHIP_MODES.join("|")}] [--hooks=on|off]`);
48
+ process.exit(1);
49
+ }
50
+ if (!isValidProjectName(name)) {
51
+ console.error(`Invalid project name: "${name}".`);
52
+ process.exit(1);
53
+ }
54
+ if (!fs.existsSync(path.join(getPhrenPath(), name))) {
55
+ console.error(`Project "${name}" not found.`);
56
+ process.exit(1);
57
+ }
58
+ const ownershipArg = args.find((arg) => arg.startsWith("--ownership="))?.slice("--ownership=".length);
59
+ const hooksArg = args.find((arg) => arg.startsWith("--hooks="))?.slice("--hooks=".length);
60
+ const ownership = ownershipArg ? parseProjectOwnershipMode(ownershipArg) : undefined;
61
+ const hooksEnabled = parseMcpToggle(hooksArg);
62
+ if (!ownershipArg && hooksArg === undefined) {
63
+ console.error(`Usage: phren projects configure <name> [--ownership=${PROJECT_OWNERSHIP_MODES.join("|")}] [--hooks=on|off]`);
64
+ process.exit(1);
65
+ }
66
+ if (ownershipArg && !ownership) {
67
+ console.error(`Usage: phren projects configure <name> [--ownership=${PROJECT_OWNERSHIP_MODES.join("|")}] [--hooks=on|off]`);
68
+ process.exit(1);
69
+ }
70
+ if (hooksArg !== undefined && hooksEnabled === undefined) {
71
+ console.error(`Invalid --hooks value "${hooksArg}". Use on or off.`);
72
+ process.exit(1);
73
+ }
74
+ const updates = [];
75
+ if (ownership) {
76
+ writeProjectConfig(getPhrenPath(), name, { ownership });
77
+ updates.push(`ownership=${ownership}`);
78
+ }
79
+ if (hooksEnabled !== undefined) {
80
+ writeProjectHookConfig(getPhrenPath(), name, { enabled: hooksEnabled });
81
+ updates.push(`hooks=${hooksEnabled ? "on" : "off"}`);
82
+ }
83
+ console.log(`Updated ${name}: ${updates.join(", ")}`);
84
+ return;
85
+ }
86
+ if (subcommand === "export") {
87
+ const name = args[1];
88
+ if (!name) {
89
+ console.error("Usage: phren projects export <name>");
90
+ process.exit(1);
91
+ }
92
+ if (!isValidProjectName(name)) {
93
+ console.error(`Invalid project name: "${name}".`);
94
+ process.exit(1);
95
+ }
96
+ const phrenPath = getPhrenPath();
97
+ const storePath = resolveProjectStorePath(phrenPath, name);
98
+ const projectDir = path.join(storePath, name);
99
+ if (!fs.existsSync(projectDir)) {
100
+ console.error(`Project "${name}" not found.`);
101
+ process.exit(1);
102
+ }
103
+ const { readFindings, readTasks, resolveTaskFilePath } = await import("../data/access.js");
104
+ const exported = { project: name, exportedAt: new Date().toISOString(), version: 1 };
105
+ const summaryPath = path.join(projectDir, "summary.md");
106
+ if (fs.existsSync(summaryPath))
107
+ exported.summary = fs.readFileSync(summaryPath, "utf8");
108
+ const learningsResult = readFindings(storePath, name);
109
+ if (learningsResult.ok)
110
+ exported.learnings = learningsResult.data;
111
+ const findingsPath = path.join(projectDir, "FINDINGS.md");
112
+ if (fs.existsSync(findingsPath))
113
+ exported.findingsRaw = fs.readFileSync(findingsPath, "utf8");
114
+ const taskResult = readTasks(storePath, name);
115
+ if (taskResult.ok) {
116
+ exported.task = taskResult.data.items;
117
+ const taskRawPath = resolveTaskFilePath(storePath, name);
118
+ if (taskRawPath && fs.existsSync(taskRawPath))
119
+ exported.taskRaw = fs.readFileSync(taskRawPath, "utf8");
120
+ }
121
+ const claudePath = path.join(projectDir, "CLAUDE.md");
122
+ if (fs.existsSync(claudePath))
123
+ exported.claudeMd = fs.readFileSync(claudePath, "utf8");
124
+ process.stdout.write(JSON.stringify(exported, null, 2) + "\n");
125
+ return;
126
+ }
127
+ if (subcommand === "import") {
128
+ const filePath = args[1];
129
+ if (!filePath) {
130
+ console.error("Usage: phren projects import <file>");
131
+ process.exit(1);
132
+ }
133
+ const resolvedPath = path.resolve(expandHomePath(filePath));
134
+ if (!fs.existsSync(resolvedPath)) {
135
+ console.error(`File not found: ${resolvedPath}`);
136
+ process.exit(1);
137
+ }
138
+ let rawData;
139
+ try {
140
+ rawData = fs.readFileSync(resolvedPath, "utf8");
141
+ }
142
+ catch (err) {
143
+ console.error(`Failed to read file: ${errorMessage(err)}`);
144
+ process.exit(1);
145
+ }
146
+ let decoded;
147
+ try {
148
+ decoded = JSON.parse(rawData);
149
+ }
150
+ catch {
151
+ console.error("Invalid JSON in file.");
152
+ process.exit(1);
153
+ }
154
+ if (!decoded || typeof decoded !== "object" || !decoded.project) {
155
+ console.error("Invalid import payload: missing project field.");
156
+ process.exit(1);
157
+ }
158
+ const { TASKS_FILENAME } = await import("../data/access.js");
159
+ const phrenPath = getPhrenPath();
160
+ const projectName = normalizeProjectNameForCreate(String(decoded.project));
161
+ if (!isValidProjectName(projectName)) {
162
+ console.error(`Invalid project name: "${decoded.project}".`);
163
+ process.exit(1);
164
+ }
165
+ const existingProject = findProjectNameCaseInsensitive(phrenPath, projectName);
166
+ if (existingProject && existingProject !== projectName) {
167
+ console.error(`Project "${existingProject}" already exists with different casing. Refusing to import "${projectName}".`);
168
+ process.exit(1);
169
+ }
170
+ const projectDir = path.join(phrenPath, projectName);
171
+ if (fs.existsSync(projectDir)) {
172
+ console.error(`Project "${projectName}" already exists. Remove it first or use the MCP tool with overwrite:true.`);
173
+ process.exit(1);
174
+ }
175
+ const imported = [];
176
+ const stagingRoot = fs.mkdtempSync(path.join(phrenPath, `.phren-import-${projectName}-`));
177
+ const stagedProjectDir = path.join(stagingRoot, projectName);
178
+ try {
179
+ fs.mkdirSync(stagedProjectDir, { recursive: true });
180
+ if (typeof decoded.summary === "string") {
181
+ fs.writeFileSync(path.join(stagedProjectDir, "summary.md"), decoded.summary);
182
+ imported.push("summary.md");
183
+ }
184
+ if (typeof decoded.claudeMd === "string") {
185
+ fs.writeFileSync(path.join(stagedProjectDir, "CLAUDE.md"), decoded.claudeMd);
186
+ imported.push("CLAUDE.md");
187
+ }
188
+ if (typeof decoded.findingsRaw === "string") {
189
+ fs.writeFileSync(path.join(stagedProjectDir, "FINDINGS.md"), decoded.findingsRaw);
190
+ imported.push("FINDINGS.md");
191
+ }
192
+ else if (Array.isArray(decoded.learnings) && decoded.learnings.length > 0) {
193
+ const date = new Date().toISOString().slice(0, 10);
194
+ const lines = [`# ${projectName} Findings`, "", `## ${date}`, ""];
195
+ for (const item of decoded.learnings) {
196
+ if (item.text)
197
+ lines.push(`- ${item.text}`);
198
+ }
199
+ lines.push("");
200
+ fs.writeFileSync(path.join(stagedProjectDir, "FINDINGS.md"), lines.join("\n"));
201
+ imported.push("FINDINGS.md");
202
+ }
203
+ if (typeof decoded.taskRaw === "string") {
204
+ fs.writeFileSync(path.join(stagedProjectDir, TASKS_FILENAME), decoded.taskRaw);
205
+ imported.push(TASKS_FILENAME);
206
+ }
207
+ fs.renameSync(stagedProjectDir, projectDir);
208
+ fs.rmSync(stagingRoot, { recursive: true, force: true });
209
+ console.log(`Imported project "${projectName}": ${imported.join(", ") || "(no files)"}`);
210
+ }
211
+ catch (err) {
212
+ try {
213
+ fs.rmSync(stagingRoot, { recursive: true, force: true });
214
+ }
215
+ catch { /* best-effort */ }
216
+ console.error(`Import failed: ${errorMessage(err)}`);
217
+ process.exit(1);
218
+ }
219
+ return;
220
+ }
221
+ if (subcommand === "archive" || subcommand === "unarchive") {
222
+ const name = args[1];
223
+ if (!name) {
224
+ console.error(`Usage: phren projects ${subcommand} <name>`);
225
+ process.exit(1);
226
+ }
227
+ if (!isValidProjectName(name)) {
228
+ console.error(`Invalid project name: "${name}".`);
229
+ process.exit(1);
230
+ }
231
+ const phrenPath = getPhrenPath();
232
+ if (subcommand === "archive") {
233
+ const activeProject = findProjectNameCaseInsensitive(phrenPath, name);
234
+ const storePath = resolveProjectStorePath(phrenPath, activeProject ?? name);
235
+ const projectDir = activeProject ? path.join(storePath, activeProject) : path.join(storePath, name);
236
+ const archiveDir = path.join(storePath, `${activeProject ?? name}.archived`);
237
+ if (!fs.existsSync(projectDir)) {
238
+ console.error(`Project "${name}" not found.`);
239
+ process.exit(1);
240
+ }
241
+ if (fs.existsSync(archiveDir)) {
242
+ console.error(`Archive "${name}.archived" already exists. Unarchive or remove it first.`);
243
+ process.exit(1);
244
+ }
245
+ try {
246
+ fs.renameSync(projectDir, archiveDir);
247
+ console.log(`Archived project "${name}". Data preserved at ${archiveDir}.`);
248
+ console.log("Note: the search index will be updated on next search.");
249
+ }
250
+ catch (err) {
251
+ console.error(`Archive failed: ${errorMessage(err)}`);
252
+ process.exit(1);
253
+ }
254
+ }
255
+ else {
256
+ const activeProject = findProjectNameCaseInsensitive(phrenPath, name);
257
+ if (activeProject) {
258
+ console.error(`Project "${activeProject}" already exists as an active project.`);
259
+ process.exit(1);
260
+ }
261
+ const archivedProject = findArchivedProjectNameCaseInsensitive(phrenPath, name);
262
+ const storePath = resolveProjectStorePath(phrenPath, archivedProject ?? name);
263
+ const projectDir = path.join(storePath, archivedProject ?? name);
264
+ const archiveDir = path.join(storePath, `${archivedProject ?? name}.archived`);
265
+ if (!fs.existsSync(archiveDir)) {
266
+ const available = fs.readdirSync(phrenPath)
267
+ .filter((e) => e.endsWith(".archived"))
268
+ .map((e) => e.replace(/\.archived$/, ""));
269
+ if (available.length > 0) {
270
+ console.error(`No archive found for "${name}". Available archives: ${available.join(", ")}`);
271
+ }
272
+ else {
273
+ console.error(`No archive found for "${name}".`);
274
+ }
275
+ process.exit(1);
276
+ }
277
+ try {
278
+ fs.renameSync(archiveDir, projectDir);
279
+ console.log(`Unarchived project "${archivedProject ?? name}". It is now active again.`);
280
+ console.log("Note: the search index will be updated on next search.");
281
+ }
282
+ catch (err) {
283
+ console.error(`Unarchive failed: ${errorMessage(err)}`);
284
+ process.exit(1);
285
+ }
286
+ }
287
+ return;
288
+ }
289
+ console.error(`Unknown subcommand: ${subcommand}`);
290
+ console.error("Usage: phren projects [list|configure|remove|export|import|archive|unarchive]");
291
+ process.exit(1);
292
+ }
293
+ function handleProjectsList(profile) {
294
+ const phrenPath = getPhrenPath();
295
+ const projectDirs = getProjectDirs(phrenPath, profile);
296
+ const projects = projectDirs
297
+ .map((dir) => path.basename(dir))
298
+ .filter((name) => name !== "global")
299
+ .sort();
300
+ if (!projects.length) {
301
+ console.log("No projects found. Run: cd ~/your-project && phren add");
302
+ return;
303
+ }
304
+ console.log(`\nProjects in ${phrenPath}:\n`);
305
+ for (const name of projects) {
306
+ const projectDir = path.join(phrenPath, name);
307
+ let dirFiles;
308
+ try {
309
+ dirFiles = new Set(fs.readdirSync(projectDir));
310
+ }
311
+ catch (err) {
312
+ if ((process.env.PHREN_DEBUG))
313
+ logger.debug("cli-namespaces", `projects list readdir: ${errorMessage(err)}`);
314
+ dirFiles = new Set();
315
+ }
316
+ const tags = [];
317
+ if (dirFiles.has("FINDINGS.md"))
318
+ tags.push("findings");
319
+ if (TASK_FILE_ALIASES.some((filename) => dirFiles.has(filename)))
320
+ tags.push("tasks");
321
+ const tagStr = tags.length ? ` [${tags.join(", ")}]` : "";
322
+ console.log(` ${name}${tagStr}`);
323
+ }
324
+ console.log(`\n${projects.length} project(s) total.`);
325
+ console.log("Add another project: cd ~/your-project && phren add");
326
+ }
327
+ async function handleProjectsRemove(name, profile) {
328
+ if (!isValidProjectName(name)) {
329
+ console.error(`Invalid project name: "${name}".`);
330
+ process.exit(1);
331
+ }
332
+ if (name === "global") {
333
+ console.error('Cannot remove the "global" project.');
334
+ process.exit(1);
335
+ }
336
+ const phrenPath = getPhrenPath();
337
+ const projectDir = path.join(phrenPath, name);
338
+ if (!fs.existsSync(projectDir)) {
339
+ console.error(`Project "${name}" not found at ${projectDir}`);
340
+ process.exit(1);
341
+ }
342
+ let fileCount = 0;
343
+ const countFiles = (dir) => {
344
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
345
+ if (entry.isDirectory())
346
+ countFiles(path.join(dir, entry.name));
347
+ else
348
+ fileCount++;
349
+ }
350
+ };
351
+ try {
352
+ countFiles(projectDir);
353
+ }
354
+ catch (err) {
355
+ if ((process.env.PHREN_DEBUG))
356
+ logger.debug("cli-namespaces", `projects remove countFiles: ${errorMessage(err)}`);
357
+ }
358
+ const readline = await import("readline");
359
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
360
+ const answer = await new Promise((resolve) => {
361
+ 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()); });
362
+ });
363
+ if (answer !== name) {
364
+ console.log("Aborted.");
365
+ return;
366
+ }
367
+ fs.rmSync(projectDir, { recursive: true, force: true });
368
+ console.log(`Removed project "${name}".`);
369
+ console.log(`If this project was in a profile, remove it from profiles/${profile || "personal"}.yaml manually.`);
370
+ }
@@ -0,0 +1 @@
1
+ export declare function handleReviewNamespace(args: string[]): Promise<void>;