@mnemahq/cli 0.15.0 → 0.15.1

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 (3) hide show
  1. package/package.json +1 -1
  2. package/src/cli.mjs +22 -2
  3. package/src/prd.mjs +59 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mnemahq/cli",
3
- "version": "0.15.0",
3
+ "version": "0.15.1",
4
4
  "description": "Mnema CLI — connect a repo to your Mnema workspace: install session capture, sweep past sessions, and search from the terminal.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli.mjs CHANGED
@@ -13,7 +13,7 @@
13
13
  import { existsSync, readFileSync, readdirSync } from 'node:fs';
14
14
  import { join } from 'node:path';
15
15
  import { homedir } from 'node:os';
16
- import { applyWrites, chooseProject, describeResult, planWrites } from './prd.mjs';
16
+ import { applyWrites, checkProjectMatchesRepo, chooseProject, declaredProjectId, describeResult, planWrites } from './prd.mjs';
17
17
  import { parseCodexRollout } from './codex-rollout.mjs';
18
18
  import { parseGeminiChat } from './gemini-chat.mjs';
19
19
  import { cmdLogin, cmdLogout, accessToken } from './login.mjs';
@@ -869,9 +869,19 @@ function cmdBinding(flags) {
869
869
  */
870
870
  async function cmdPrd(flags, rest) {
871
871
  const sub = (rest[0] || '').toLowerCase();
872
+ // ⚠️ `--project Display Share` without quotes passes "Display" and leaves
873
+ // "Share" here. It partial-matched the right project by luck once; silence
874
+ // would let the next one match the wrong project instead.
875
+ const stray = rest.slice(1);
876
+ if (sub === 'init' && stray.length > 0) {
877
+ console.error(c.red(`Unexpected argument: ${stray.join(' ')}`));
878
+ console.error(c.dim(' A project name with spaces needs quotes:'));
879
+ console.error(c.dim(` mnema prd init --project "${[flags.project, ...stray].filter(Boolean).join(' ')}"`));
880
+ process.exit(1);
881
+ }
872
882
  if (sub !== 'init') {
873
883
  console.error(c.red(`Unknown prd subcommand: ${sub || '(none)'}`));
874
- console.error(c.dim('Usage: mnema prd init [--project <name|id>] [--days 365] [--force]'));
884
+ console.error(c.dim('Usage: mnema prd init [--project <name|id>] [--days 365] [--force] [--allow-mismatch]'));
875
885
  process.exit(1);
876
886
  }
877
887
 
@@ -902,6 +912,16 @@ async function cmdPrd(flags, rest) {
902
912
  process.exit(1);
903
913
  }
904
914
 
915
+ // ⚠️ Before fetching anything: does this repo actually claim that project?
916
+ const match = checkProjectMatchesRepo(declaredProjectId(root), choice.project, {
917
+ allowMismatch: Boolean(flags['allow-mismatch']),
918
+ });
919
+ if (!match.ok) {
920
+ console.error('');
921
+ console.error(c.red(match.message));
922
+ process.exit(1);
923
+ }
924
+
905
925
  let draft;
906
926
  try {
907
927
  draft = await call({ origin, workspaceId }, (m) =>
package/src/prd.mjs CHANGED
@@ -38,6 +38,60 @@ export function chooseProject(projects, wanted) {
38
38
  return { ok: false, reason: 'no-match', projects };
39
39
  }
40
40
 
41
+
42
+ /**
43
+ * What the repo itself says it is (t-935).
44
+ *
45
+ * `.mnema/project.json` carries `projectId`, written when the repo was declared.
46
+ * It is the repo's own answer to "which project is this", and it was sitting
47
+ * there unread while the command drafted whichever project was asked for.
48
+ */
49
+ export function declaredProjectId(root) {
50
+ for (const rel of ['.mnema/project.json', '.mnema/config.json']) {
51
+ try {
52
+ const j = JSON.parse(readFileSync(join(root, rel), 'utf8'));
53
+ if (j && typeof j.projectId === 'string' && j.projectId) return j.projectId;
54
+ } catch { /* absent or unreadable — treated as "the repo does not say" */ }
55
+ }
56
+ return null;
57
+ }
58
+
59
+ /**
60
+ * Refuse to draft a project this repo did not declare.
61
+ *
62
+ * ⭐ THE FAILURE THIS PREVENTS, OBSERVED ON THE FIRST PROD RUN:
63
+ *
64
+ * cd project-x (declares projectId = Mnema)
65
+ * mnema prd init --project "Display Share"
66
+ * → 4 features drafted
67
+ *
68
+ * Display Share's features, drafted for the Mnema repo. Only the "file already
69
+ * exists" guard stopped the write; with --force it would have put another
70
+ * project's features into this repo's docs/prd.md, and the declaration sync
71
+ * would then have ingested them as this project's own.
72
+ *
73
+ * ⚠️ A REPO THAT DECLARES NOTHING IS ALLOWED. Requiring a declaration would break
74
+ * the first run in a fresh repo, which is exactly when this command is most
75
+ * useful. Silence is "unknown", not "mismatch".
76
+ */
77
+ export function checkProjectMatchesRepo(declaredId, chosen, { allowMismatch = false } = {}) {
78
+ if (!declaredId) return { ok: true, reason: 'repo declares no project' };
79
+ if (declaredId === chosen.id) return { ok: true, reason: 'matches the repo' };
80
+ if (allowMismatch) return { ok: true, reason: 'mismatch allowed explicitly' };
81
+ return {
82
+ ok: false,
83
+ declaredId,
84
+ chosen,
85
+ message:
86
+ `This repo declares a different project.\n` +
87
+ ` repo declares : ${declaredId}\n` +
88
+ ` you asked for : ${chosen.name} (${chosen.id})\n\n` +
89
+ `Drafting the wrong project would write its features into this repo's PRD,\n` +
90
+ `and the declaration sync would then treat them as this project's own.\n` +
91
+ `If you really mean it, pass --allow-mismatch.`,
92
+ };
93
+ }
94
+
41
95
  /**
42
96
  * Decide what to do with each file the server returned.
43
97
  *
@@ -70,6 +124,11 @@ export function applyWrites(plan) {
70
124
  /** The report a reader needs after a draft is written. */
71
125
  export function describeResult(draft, plan) {
72
126
  const lines = [];
127
+ // ⚠️ SAY WHAT WAS DRAFTED, FOR WHAT. "4 features drafted" does not say drafted
128
+ // for WHICH project, so drafting the wrong one reads exactly like success —
129
+ // which is how a cross-project draft went unnoticed on the first prod run.
130
+ lines.push(` project ${draft.project?.name ?? '(unnamed)'} ${draft.repo ? `· ${draft.repo}` : ''}`.trimEnd());
131
+ lines.push('');
73
132
  const wrote = plan.filter((p) => p.action === 'create' || p.action === 'overwrite');
74
133
  const skipped = plan.filter((p) => p.action === 'skip-exists');
75
134
  const same = plan.filter((p) => p.action === 'unchanged');