pmpt-cli 1.13.0 → 1.14.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.
@@ -1,15 +1,99 @@
1
1
  import * as p from '@clack/prompts';
2
- import { resolve, join } from 'path';
3
- import { existsSync, rmSync, writeFileSync, readFileSync } from 'fs';
2
+ import { resolve, join, basename } from 'path';
3
+ import { existsSync, rmSync, renameSync, writeFileSync, readFileSync } from 'fs';
4
4
  import { isInitialized, getHistoryDir } from '../lib/config.js';
5
5
  import { getAllSnapshots } from '../lib/history.js';
6
- export async function cmdSquash(from, to, path) {
7
- const projectPath = path ? resolve(path) : process.cwd();
6
+ export async function cmdSquash(from, to, opts) {
7
+ const projectPath = opts?.path ? resolve(opts.path) : process.cwd();
8
8
  if (!isInitialized(projectPath)) {
9
9
  p.log.error('Project not initialized. Run `pmpt init` first.');
10
10
  process.exit(1);
11
11
  }
12
- // Parse version numbers
12
+ const snapshots = getAllSnapshots(projectPath);
13
+ if (snapshots.length === 0) {
14
+ p.log.error('No snapshots found.');
15
+ process.exit(1);
16
+ }
17
+ if (opts?.auto) {
18
+ return autoSquash(projectPath, snapshots);
19
+ }
20
+ // Manual squash: require from and to
21
+ if (!from || !to) {
22
+ p.log.error('Usage: pmpt squash v2 v5 or pmpt squash --auto');
23
+ process.exit(1);
24
+ }
25
+ return manualSquash(projectPath, snapshots, from, to);
26
+ }
27
+ /**
28
+ * Renumber remaining snapshots to v1, v2, v3... sequentially.
29
+ * Renames directories and updates .meta.json version fields.
30
+ */
31
+ function renumberSnapshots(projectPath) {
32
+ const historyDir = getHistoryDir(projectPath);
33
+ const remaining = getAllSnapshots(projectPath); // sorted by version
34
+ for (let i = 0; i < remaining.length; i++) {
35
+ const snap = remaining[i];
36
+ const newVersion = i + 1;
37
+ if (snap.version === newVersion)
38
+ continue; // already correct
39
+ // Rename directory: v5-20260228T164006 → v2-20260228T164006
40
+ const dirName = basename(snap.snapshotDir);
41
+ const newDirName = dirName.replace(/^v\d+/, `v${newVersion}`);
42
+ const newDir = join(historyDir, newDirName);
43
+ renameSync(snap.snapshotDir, newDir);
44
+ // Update .meta.json
45
+ const metaPath = join(newDir, '.meta.json');
46
+ if (existsSync(metaPath)) {
47
+ const meta = JSON.parse(readFileSync(metaPath, 'utf-8'));
48
+ meta.version = newVersion;
49
+ writeFileSync(metaPath, JSON.stringify(meta, null, 2), 'utf-8');
50
+ }
51
+ }
52
+ }
53
+ async function autoSquash(projectPath, snapshots) {
54
+ // Find empty snapshots (no changed files)
55
+ const emptySnapshots = snapshots.filter(s => s.changedFiles && s.changedFiles.length === 0);
56
+ if (emptySnapshots.length === 0) {
57
+ p.log.info('No empty snapshots found. Nothing to clean up.');
58
+ return;
59
+ }
60
+ p.intro('pmpt squash --auto');
61
+ p.log.info(`Found ${emptySnapshots.length} empty snapshot(s) (no file changes):`);
62
+ for (const s of emptySnapshots) {
63
+ const git = s.git ? ` [${s.git.commit}]` : '';
64
+ p.log.message(` v${s.version} — ${s.timestamp.slice(0, 16)}${git}`);
65
+ }
66
+ const confirm = await p.confirm({
67
+ message: `Delete ${emptySnapshots.length} empty snapshot(s) and renumber?`,
68
+ initialValue: true,
69
+ });
70
+ if (p.isCancel(confirm) || !confirm) {
71
+ p.cancel('Cancelled');
72
+ process.exit(0);
73
+ }
74
+ const sp = p.spinner();
75
+ sp.start('Removing empty snapshots...');
76
+ try {
77
+ let deleted = 0;
78
+ for (const snapshot of emptySnapshots) {
79
+ if (existsSync(snapshot.snapshotDir)) {
80
+ rmSync(snapshot.snapshotDir, { recursive: true });
81
+ deleted++;
82
+ }
83
+ }
84
+ renumberSnapshots(projectPath);
85
+ sp.stop('Cleaned up');
86
+ const remaining = snapshots.length - deleted;
87
+ p.log.success(`Removed ${deleted} empty snapshot(s), renumbered to v1-v${remaining}`);
88
+ }
89
+ catch (error) {
90
+ sp.stop('Failed');
91
+ p.log.error(error.message);
92
+ process.exit(1);
93
+ }
94
+ p.outro('View history: pmpt history');
95
+ }
96
+ async function manualSquash(projectPath, snapshots, from, to) {
13
97
  const fromVersion = parseInt(from.replace(/^v/, ''), 10);
14
98
  const toVersion = parseInt(to.replace(/^v/, ''), 10);
15
99
  if (isNaN(fromVersion) || isNaN(toVersion)) {
@@ -20,11 +104,6 @@ export async function cmdSquash(from, to, path) {
20
104
  p.log.error('First version must be less than second version.');
21
105
  process.exit(1);
22
106
  }
23
- const snapshots = getAllSnapshots(projectPath);
24
- if (snapshots.length === 0) {
25
- p.log.error('No snapshots found.');
26
- process.exit(1);
27
- }
28
107
  const versionList = snapshots.map(s => `v${s.version}`).join(', ');
29
108
  // Find snapshots to squash
30
109
  const toSquash = snapshots.filter(s => s.version >= fromVersion && s.version <= toVersion);
@@ -46,7 +125,6 @@ export async function cmdSquash(from, to, path) {
46
125
  const s = p.spinner();
47
126
  s.start('Squashing versions...');
48
127
  try {
49
- const historyDir = getHistoryDir(projectPath);
50
128
  const keepSnapshot = toSquash[0]; // Keep the first one
51
129
  const deleteSnapshots = toSquash.slice(1); // Delete the rest
52
130
  // Delete the snapshots we're squashing
@@ -64,8 +142,10 @@ export async function cmdSquash(from, to, path) {
64
142
  meta.squashedAt = new Date().toISOString();
65
143
  writeFileSync(metaPath, JSON.stringify(meta, null, 2), 'utf-8');
66
144
  }
145
+ renumberSnapshots(projectPath);
67
146
  s.stop('Squashed');
68
- p.log.success(`Squashed v${fromVersion}-v${toVersion} into v${fromVersion}`);
147
+ const remaining = getAllSnapshots(projectPath);
148
+ p.log.success(`Squashed v${fromVersion}-v${toVersion}, renumbered to v1-v${remaining.length}`);
69
149
  p.log.info(`Deleted ${deleteSnapshots.length} version(s)`);
70
150
  }
71
151
  catch (error) {
package/dist/index.js CHANGED
@@ -68,6 +68,7 @@ Examples:
68
68
  $ pmpt diff v1 v2 Compare two versions
69
69
  $ pmpt diff v3 Compare v3 to working copy
70
70
  $ pmpt squash v2 v5 Merge versions v2-v5 into v2
71
+ $ pmpt squash --auto Auto-remove empty snapshots
71
72
  $ pmpt export Export as .pmpt file (single JSON)
72
73
  $ pmpt import <file.pmpt> Import from .pmpt file
73
74
  $ pmpt login Authenticate with pmptwiki
@@ -119,9 +120,11 @@ program
119
120
  .option('-f, --file <name>', 'Compare specific file only')
120
121
  .action(cmdDiff);
121
122
  program
122
- .command('squash <from> <to> [path]')
123
- .description('Squash multiple versions into one (e.g., pmpt squash v2 v5)')
124
- .action(cmdSquash);
123
+ .command('squash [from] [to]')
124
+ .description('Squash versions: pmpt squash v2 v5 or pmpt squash --auto')
125
+ .option('--auto', 'Auto-remove empty snapshots (no file changes)')
126
+ .option('-p, --path <path>', 'Project path')
127
+ .action((from, to, opts) => cmdSquash(from, to, opts));
125
128
  program
126
129
  .command('export [path]')
127
130
  .description('Export project history as a shareable .pmpt file')
package/dist/mcp.js CHANGED
@@ -12,7 +12,7 @@ import { resolve, join } from 'path';
12
12
  import { existsSync, readFileSync, writeFileSync } from 'fs';
13
13
  import glob from 'fast-glob';
14
14
  import { createRequire } from 'module';
15
- import { isInitialized, loadConfig, getDocsDir } from './lib/config.js';
15
+ import { isInitialized, loadConfig, getDocsDir, getHistoryDir } from './lib/config.js';
16
16
  import { createFullSnapshot, getAllSnapshots, getTrackedFiles, resolveFullSnapshot } from './lib/history.js';
17
17
  import { computeQuality } from './lib/quality.js';
18
18
  import { getPlanProgress, savePlanProgress, savePlanDocuments, PLAN_QUESTIONS } from './lib/plan.js';
@@ -98,7 +98,10 @@ function formatDiffs(diffs) {
98
98
  return lines.join('\n');
99
99
  }
100
100
  // ── Tools ───────────────────────────────────────────
101
- server.tool('pmpt_save', 'Save a snapshot of .pmpt/docs/ files. Call after completing features, fixes, or milestones.', { projectPath: z.string().optional().describe('Project root path. Defaults to cwd.') }, async ({ projectPath }) => {
101
+ server.tool('pmpt_save', 'Save a snapshot of .pmpt/docs/ files. Call after completing features, fixes, or milestones. IMPORTANT: Always provide a summary describing what was accomplished this gets recorded in the project development log.', {
102
+ projectPath: z.string().optional().describe('Project root path. Defaults to cwd.'),
103
+ summary: z.string().optional().describe('What was accomplished since the last save. This is recorded in pmpt.md as a development log entry. Examples: "Implemented user auth with JWT", "Fixed responsive layout on mobile", "Added search filtering by category".'),
104
+ }, async ({ projectPath, summary }) => {
102
105
  try {
103
106
  const pp = resolveProjectPath(projectPath);
104
107
  assertInitialized(pp);
@@ -106,13 +109,37 @@ server.tool('pmpt_save', 'Save a snapshot of .pmpt/docs/ files. Call after compl
106
109
  if (tracked.length === 0) {
107
110
  return { content: [{ type: 'text', text: 'No files to save. Add .md files to .pmpt/docs/ first.' }] };
108
111
  }
109
- const entry = createFullSnapshot(pp);
112
+ // Auto-update pmpt.md with summary before snapshot
113
+ if (summary) {
114
+ const docsDir = getDocsDir(pp);
115
+ const pmptMdPath = join(docsDir, 'pmpt.md');
116
+ if (existsSync(pmptMdPath)) {
117
+ let content = readFileSync(pmptMdPath, 'utf-8');
118
+ const snapshots = getAllSnapshots(pp);
119
+ const nextVersion = snapshots.length + 1;
120
+ const date = new Date().toISOString().slice(0, 10);
121
+ const entry = `\n### v${nextVersion} — ${date}\n- ${summary}\n`;
122
+ const logIndex = content.indexOf('## Snapshot Log');
123
+ if (logIndex !== -1) {
124
+ const afterHeader = content.indexOf('\n', logIndex);
125
+ const nextSection = content.indexOf('\n## ', afterHeader + 1);
126
+ const insertPos = nextSection !== -1 ? nextSection : content.length;
127
+ content = content.slice(0, insertPos) + entry + content.slice(insertPos);
128
+ }
129
+ else {
130
+ content += `\n## Snapshot Log${entry}`;
131
+ }
132
+ writeFileSync(pmptMdPath, content, 'utf-8');
133
+ }
134
+ }
135
+ const entry = createFullSnapshot(pp, summary ? { note: summary } : undefined);
110
136
  const changedCount = entry.changedFiles?.length ?? entry.files.length;
111
137
  return {
112
138
  content: [{
113
139
  type: 'text',
114
140
  text: [
115
141
  `Snapshot v${entry.version} saved (${changedCount} changed, ${entry.files.length - changedCount} unchanged).`,
142
+ summary ? `Summary: ${summary}` : '',
116
143
  '',
117
144
  `Files: ${entry.files.join(', ')}`,
118
145
  entry.changedFiles ? `Changed: ${entry.changedFiles.join(', ')}` : '',
@@ -218,6 +245,64 @@ server.tool('pmpt_diff', 'Compare two versions, or a version against the current
218
245
  return { content: [{ type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}` }], isError: true };
219
246
  }
220
247
  });
248
+ server.tool('pmpt_squash', 'Remove empty snapshots (no file changes) and renumber remaining versions sequentially. Use this to clean up history when there are snapshots with no actual content changes.', {
249
+ projectPath: z.string().optional().describe('Project root path. Defaults to cwd.'),
250
+ }, async ({ projectPath }) => {
251
+ try {
252
+ const pp = resolveProjectPath(projectPath);
253
+ assertInitialized(pp);
254
+ const snapshots = getAllSnapshots(pp);
255
+ if (snapshots.length === 0) {
256
+ return { content: [{ type: 'text', text: 'No snapshots found.' }] };
257
+ }
258
+ const emptySnapshots = snapshots.filter(s => s.changedFiles && s.changedFiles.length === 0);
259
+ if (emptySnapshots.length === 0) {
260
+ return { content: [{ type: 'text', text: 'No empty snapshots found. History is clean.' }] };
261
+ }
262
+ // Delete empty snapshots
263
+ const { rmSync, renameSync } = await import('fs');
264
+ const { basename } = await import('path');
265
+ for (const snap of emptySnapshots) {
266
+ if (existsSync(snap.snapshotDir)) {
267
+ rmSync(snap.snapshotDir, { recursive: true });
268
+ }
269
+ }
270
+ // Renumber remaining snapshots
271
+ const historyDir = getHistoryDir(pp);
272
+ const remaining = getAllSnapshots(pp);
273
+ for (let i = 0; i < remaining.length; i++) {
274
+ const snap = remaining[i];
275
+ const newVersion = i + 1;
276
+ if (snap.version === newVersion)
277
+ continue;
278
+ const dirName = basename(snap.snapshotDir);
279
+ const newDirName = dirName.replace(/^v\d+/, `v${newVersion}`);
280
+ const newDir = join(historyDir, newDirName);
281
+ renameSync(snap.snapshotDir, newDir);
282
+ const metaPath = join(newDir, '.meta.json');
283
+ if (existsSync(metaPath)) {
284
+ const meta = JSON.parse(readFileSync(metaPath, 'utf-8'));
285
+ meta.version = newVersion;
286
+ writeFileSync(metaPath, JSON.stringify(meta, null, 2), 'utf-8');
287
+ }
288
+ }
289
+ const finalCount = getAllSnapshots(pp).length;
290
+ return {
291
+ content: [{
292
+ type: 'text',
293
+ text: [
294
+ `Squashed: removed ${emptySnapshots.length} empty snapshot(s).`,
295
+ `Remaining: ${finalCount} snapshot(s), renumbered v1-v${finalCount}.`,
296
+ '',
297
+ `Removed versions: ${emptySnapshots.map(s => `v${s.version}`).join(', ')}`,
298
+ ].join('\n'),
299
+ }],
300
+ };
301
+ }
302
+ catch (error) {
303
+ return { content: [{ type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}` }], isError: true };
304
+ }
305
+ });
221
306
  server.tool('pmpt_quality', 'Check project quality score and publish readiness.', { projectPath: z.string().optional().describe('Project root path. Defaults to cwd.') }, async ({ projectPath }) => {
222
307
  try {
223
308
  const pp = resolveProjectPath(projectPath);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pmpt-cli",
3
- "version": "1.13.0",
3
+ "version": "1.14.1",
4
4
  "description": "Record and share your AI-driven product development journey",
5
5
  "type": "module",
6
6
  "bin": {