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.
- package/dist/commands/squash.js +92 -12
- package/dist/index.js +6 -3
- package/dist/mcp.js +88 -3
- package/package.json +1 -1
package/dist/commands/squash.js
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
123
|
-
.description('Squash
|
|
124
|
-
.
|
|
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.
|
|
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
|
-
|
|
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);
|