@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,367 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ import { execFileSync } from "child_process";
4
+ import { getPhrenPath } from "../shared.js";
5
+ import { isValidProjectName, errorMessage } from "../utils.js";
6
+ import { resolveAllStores, addStoreToRegistry, removeStoreFromRegistry, generateStoreId, readTeamBootstrap, } from "../store-registry.js";
7
+ import { getOptionValue } from "./namespaces-utils.js";
8
+ function printStoreUsage() {
9
+ console.log("Usage:");
10
+ console.log(" phren store list List registered stores");
11
+ console.log(" phren store add <name> --remote <url> Add a team store");
12
+ console.log(" phren store remove <name> Remove a store (local only)");
13
+ console.log(" phren store sync Pull all stores");
14
+ console.log(" phren store activity [--limit N] Recent team findings");
15
+ console.log(" phren store subscribe <name> <project...> Subscribe store to projects");
16
+ console.log(" phren store unsubscribe <name> <project...> Unsubscribe store from projects");
17
+ }
18
+ function countStoreProjects(store) {
19
+ if (!fs.existsSync(store.path))
20
+ return 0;
21
+ try {
22
+ const storeRegistry = require("../store-registry.js");
23
+ return storeRegistry.getStoreProjectDirs(store).length;
24
+ }
25
+ catch {
26
+ return 0;
27
+ }
28
+ }
29
+ function readHealthForStore(storePath) {
30
+ try {
31
+ const healthPath = path.join(storePath, ".runtime", "health.json");
32
+ if (!fs.existsSync(healthPath))
33
+ return null;
34
+ const raw = JSON.parse(fs.readFileSync(healthPath, "utf8"));
35
+ const lastSync = raw?.lastSync;
36
+ if (!lastSync)
37
+ return null;
38
+ const parts = [];
39
+ if (lastSync.lastPullStatus)
40
+ parts.push(`pull=${lastSync.lastPullStatus}`);
41
+ if (lastSync.lastPushStatus)
42
+ parts.push(`push=${lastSync.lastPushStatus}`);
43
+ if (lastSync.lastSuccessfulPullAt)
44
+ parts.push(`at=${lastSync.lastSuccessfulPullAt.slice(0, 16)}`);
45
+ return parts.join(", ") || null;
46
+ }
47
+ catch {
48
+ return null;
49
+ }
50
+ }
51
+ export async function handleStoreNamespace(args) {
52
+ const subcommand = args[0];
53
+ if (!subcommand || subcommand === "--help" || subcommand === "-h") {
54
+ printStoreUsage();
55
+ return;
56
+ }
57
+ const phrenPath = getPhrenPath();
58
+ if (subcommand === "list") {
59
+ const stores = resolveAllStores(phrenPath);
60
+ if (stores.length === 0) {
61
+ console.log("No stores registered.");
62
+ return;
63
+ }
64
+ console.log(`${stores.length} store(s):\n`);
65
+ for (const store of stores) {
66
+ const exists = fs.existsSync(store.path) ? "ok" : "MISSING";
67
+ const syncInfo = store.remote ?? "(local)";
68
+ const projectCount = countStoreProjects(store);
69
+ console.log(` ${store.name} (${store.role})`);
70
+ console.log(` id: ${store.id}`);
71
+ console.log(` path: ${store.path} [${exists}]`);
72
+ console.log(` remote: ${syncInfo}`);
73
+ console.log(` sync: ${store.sync}`);
74
+ console.log(` projects: ${projectCount}`);
75
+ // Show last sync status if available
76
+ const health = readHealthForStore(store.path);
77
+ if (health) {
78
+ console.log(` last sync: ${health}`);
79
+ }
80
+ console.log();
81
+ }
82
+ return;
83
+ }
84
+ if (subcommand === "add") {
85
+ const name = args[1];
86
+ if (!name) {
87
+ console.error("Usage: phren store add <name> --remote <url> [--role team|readonly]");
88
+ process.exit(1);
89
+ }
90
+ // Validate store name to prevent path traversal
91
+ if (!isValidProjectName(name)) {
92
+ console.error(`Invalid store name: "${name}". Use lowercase letters, numbers, and hyphens.`);
93
+ process.exit(1);
94
+ }
95
+ const remote = getOptionValue(args.slice(2), "--remote");
96
+ if (!remote) {
97
+ console.error("--remote <url> is required. Provide the git clone URL for the team store.");
98
+ process.exit(1);
99
+ }
100
+ // Prevent git option injection via --remote
101
+ if (remote.startsWith("-")) {
102
+ console.error(`Invalid remote URL: "${remote}". URLs must not start with "-".`);
103
+ process.exit(1);
104
+ }
105
+ const roleArg = getOptionValue(args.slice(2), "--role") ?? "team";
106
+ if (roleArg !== "team" && roleArg !== "readonly") {
107
+ console.error(`Invalid role: "${roleArg}". Use "team" or "readonly".`);
108
+ process.exit(1);
109
+ }
110
+ const storesDir = path.join(path.dirname(phrenPath), ".phren-stores");
111
+ const storePath = path.join(storesDir, name);
112
+ if (fs.existsSync(storePath)) {
113
+ console.error(`Directory already exists: ${storePath}`);
114
+ process.exit(1);
115
+ }
116
+ // Clone the remote
117
+ console.log(`Cloning ${remote} into ${storePath}...`);
118
+ try {
119
+ fs.mkdirSync(storesDir, { recursive: true });
120
+ execFileSync("git", ["clone", "--", remote, storePath], {
121
+ stdio: "inherit",
122
+ timeout: 60_000,
123
+ });
124
+ }
125
+ catch (err) {
126
+ console.error(`Clone failed: ${errorMessage(err)}`);
127
+ process.exit(1);
128
+ }
129
+ // Read .phren-team.yaml if present
130
+ const bootstrap = readTeamBootstrap(storePath);
131
+ const storeName = bootstrap?.name ?? name;
132
+ const storeRole = bootstrap?.default_role ?? roleArg;
133
+ const entry = {
134
+ id: generateStoreId(),
135
+ name: storeName,
136
+ path: storePath,
137
+ role: storeRole === "primary" ? "team" : storeRole, // Never allow adding a second primary
138
+ sync: storeRole === "readonly" ? "pull-only" : "managed-git",
139
+ remote,
140
+ };
141
+ try {
142
+ addStoreToRegistry(phrenPath, entry);
143
+ }
144
+ catch (err) {
145
+ console.error(`Failed to register store: ${errorMessage(err)}`);
146
+ process.exit(1);
147
+ }
148
+ console.log(`\nStore "${storeName}" added (${entry.role}).`);
149
+ console.log(` id: ${entry.id}`);
150
+ console.log(` path: ${storePath}`);
151
+ return;
152
+ }
153
+ if (subcommand === "remove") {
154
+ const name = args[1];
155
+ if (!name) {
156
+ console.error("Usage: phren store remove <name>");
157
+ process.exit(1);
158
+ }
159
+ try {
160
+ const removed = removeStoreFromRegistry(phrenPath, name);
161
+ console.log(`Store "${name}" removed from registry.`);
162
+ console.log(` Local directory preserved at: ${removed.path}`);
163
+ console.log(` To delete: rm -rf "${removed.path}"`);
164
+ }
165
+ catch (err) {
166
+ console.error(`${errorMessage(err)}`);
167
+ process.exit(1);
168
+ }
169
+ return;
170
+ }
171
+ if (subcommand === "activity") {
172
+ const stores = resolveAllStores(phrenPath);
173
+ const teamStores = stores.filter((s) => s.role === "team");
174
+ if (teamStores.length === 0) {
175
+ console.log("No team stores registered. Add one with: phren store add <name> --remote <url>");
176
+ return;
177
+ }
178
+ const { readTeamJournalEntries } = await import("../finding/journal.js");
179
+ const limit = Number(getOptionValue(args.slice(1), "--limit") ?? "20");
180
+ const allEntries = [];
181
+ for (const store of teamStores) {
182
+ if (!fs.existsSync(store.path))
183
+ continue;
184
+ const { getStoreProjectDirs } = await import("../store-registry.js");
185
+ const projectDirs = getStoreProjectDirs(store);
186
+ for (const dir of projectDirs) {
187
+ const projectName = path.basename(dir);
188
+ const journalEntries = readTeamJournalEntries(store.path, projectName);
189
+ for (const je of journalEntries) {
190
+ for (const entry of je.entries) {
191
+ allEntries.push({ store: store.name, project: projectName, date: je.date, actor: je.actor, entry });
192
+ }
193
+ }
194
+ }
195
+ }
196
+ allEntries.sort((a, b) => b.date.localeCompare(a.date));
197
+ const capped = allEntries.slice(0, limit);
198
+ if (capped.length === 0) {
199
+ console.log("No team activity yet.");
200
+ return;
201
+ }
202
+ console.log(`Team activity (${capped.length}/${allEntries.length}):\n`);
203
+ let lastDate = "";
204
+ for (const e of capped) {
205
+ if (e.date !== lastDate) {
206
+ console.log(`## ${e.date}`);
207
+ lastDate = e.date;
208
+ }
209
+ console.log(` [${e.store}/${e.project}] ${e.actor}: ${e.entry}`);
210
+ }
211
+ return;
212
+ }
213
+ if (subcommand === "sync") {
214
+ const stores = resolveAllStores(phrenPath);
215
+ let hasErrors = false;
216
+ for (const store of stores) {
217
+ if (!fs.existsSync(store.path)) {
218
+ console.log(` ${store.name}: SKIP (path missing)`);
219
+ continue;
220
+ }
221
+ const gitDir = path.join(store.path, ".git");
222
+ if (!fs.existsSync(gitDir)) {
223
+ console.log(` ${store.name}: SKIP (not a git repo)`);
224
+ continue;
225
+ }
226
+ try {
227
+ execFileSync("git", ["pull", "--rebase", "--quiet"], {
228
+ cwd: store.path,
229
+ stdio: "pipe",
230
+ timeout: 30_000,
231
+ });
232
+ // Re-apply sparse-checkout after pull on primary store to avoid materializing all files
233
+ if (store.role === "primary") {
234
+ try {
235
+ const sparseList = execFileSync("git", ["sparse-checkout", "list"], {
236
+ cwd: store.path,
237
+ stdio: "pipe",
238
+ timeout: 10_000,
239
+ }).toString().trim();
240
+ if (sparseList) {
241
+ execFileSync("git", ["sparse-checkout", "reapply"], {
242
+ cwd: store.path,
243
+ stdio: "pipe",
244
+ timeout: 10_000,
245
+ });
246
+ }
247
+ }
248
+ catch {
249
+ // sparse-checkout not configured — nothing to reapply
250
+ }
251
+ }
252
+ console.log(` ${store.name}: ok`);
253
+ }
254
+ catch (err) {
255
+ console.log(` ${store.name}: FAILED (${errorMessage(err).split("\n")[0]})`);
256
+ hasErrors = true;
257
+ }
258
+ }
259
+ if (hasErrors) {
260
+ console.error("\nSome stores failed to sync. Run 'phren doctor' for details.");
261
+ }
262
+ return;
263
+ }
264
+ if (subcommand === "subscribe") {
265
+ const storeName = args[1];
266
+ const projects = args.slice(2);
267
+ if (!storeName || projects.length === 0) {
268
+ console.error("Usage: phren store subscribe <store-name> <project1> [project2...]");
269
+ process.exit(1);
270
+ }
271
+ try {
272
+ const { subscribeStoreProjects } = await import("../store-registry.js");
273
+ subscribeStoreProjects(phrenPath, storeName, projects);
274
+ console.log(`Added ${projects.length} project(s) to "${storeName}"`);
275
+ }
276
+ catch (err) {
277
+ console.error(`Failed to subscribe: ${errorMessage(err)}`);
278
+ process.exit(1);
279
+ }
280
+ return;
281
+ }
282
+ if (subcommand === "unsubscribe") {
283
+ const storeName = args[1];
284
+ const projects = args.slice(2);
285
+ if (!storeName || projects.length === 0) {
286
+ console.error("Usage: phren store unsubscribe <store-name> <project1> [project2...]");
287
+ process.exit(1);
288
+ }
289
+ try {
290
+ const { unsubscribeStoreProjects } = await import("../store-registry.js");
291
+ unsubscribeStoreProjects(phrenPath, storeName, projects);
292
+ console.log(`Removed ${projects.length} project(s) from "${storeName}"`);
293
+ }
294
+ catch (err) {
295
+ console.error(`Failed to unsubscribe: ${errorMessage(err)}`);
296
+ process.exit(1);
297
+ }
298
+ return;
299
+ }
300
+ console.error(`Unknown store subcommand: ${subcommand}`);
301
+ printStoreUsage();
302
+ process.exit(1);
303
+ }
304
+ export async function handlePromoteNamespace(args) {
305
+ if (!args[0] || args[0] === "--help" || args[0] === "-h") {
306
+ console.log("Usage:");
307
+ console.log(' phren promote <project> "finding text..." --to <store>');
308
+ console.log(" Copies a finding from the primary store to a team store.");
309
+ return;
310
+ }
311
+ const phrenPath = getPhrenPath();
312
+ const project = args[0];
313
+ if (!isValidProjectName(project)) {
314
+ console.error(`Invalid project name: "${project}"`);
315
+ process.exit(1);
316
+ }
317
+ const toStore = getOptionValue(args.slice(1), "--to");
318
+ if (!toStore) {
319
+ console.error("--to <store> is required. Specify the target team store.");
320
+ process.exit(1);
321
+ }
322
+ // Everything between project and --to is the finding text
323
+ const toIdx = args.indexOf("--to");
324
+ const findingText = args.slice(1, toIdx !== -1 ? toIdx : undefined).join(" ").trim();
325
+ if (!findingText) {
326
+ console.error("Finding text is required.");
327
+ process.exit(1);
328
+ }
329
+ const stores = resolveAllStores(phrenPath);
330
+ const targetStore = stores.find((s) => s.name === toStore);
331
+ if (!targetStore) {
332
+ const available = stores.map((s) => s.name).join(", ");
333
+ console.error(`Store "${toStore}" not found. Available: ${available}`);
334
+ process.exit(1);
335
+ }
336
+ if (targetStore.role === "readonly") {
337
+ console.error(`Store "${toStore}" is read-only.`);
338
+ process.exit(1);
339
+ }
340
+ if (targetStore.role === "primary") {
341
+ console.error(`Cannot promote to primary store — finding is already there.`);
342
+ process.exit(1);
343
+ }
344
+ // Find the matching finding in the primary store
345
+ const { readFindings } = await import("../data/access.js");
346
+ const findingsResult = readFindings(phrenPath, project);
347
+ if (!findingsResult.ok) {
348
+ console.error(`Could not read findings for project "${project}".`);
349
+ process.exit(1);
350
+ }
351
+ const match = findingsResult.data.find((item) => item.text.includes(findingText) || findingText.includes(item.text));
352
+ if (!match) {
353
+ console.error(`No finding matching "${findingText.slice(0, 80)}..." found in ${project}.`);
354
+ process.exit(1);
355
+ }
356
+ // Write to target store
357
+ const targetProjectDir = path.join(targetStore.path, project);
358
+ fs.mkdirSync(targetProjectDir, { recursive: true });
359
+ const { addFindingToFile } = await import("../shared/content.js");
360
+ const result = addFindingToFile(targetStore.path, project, match.text);
361
+ if (!result.ok) {
362
+ console.error(`Failed to add finding to ${toStore}: ${result.error}`);
363
+ process.exit(1);
364
+ }
365
+ console.log(`Promoted to ${toStore}/${project}:`);
366
+ console.log(` "${match.text.slice(0, 120)}${match.text.length > 120 ? "..." : ""}"`);
367
+ }
@@ -0,0 +1 @@
1
+ export declare function handleTaskNamespace(args: string[]): Promise<void>;