@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.
- package/package.json +1 -1
- package/src/cli.mjs +22 -2
- package/src/prd.mjs +59 -0
package/package.json
CHANGED
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');
|