pmpt-cli 1.4.1 → 1.5.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/README.md +2 -4
- package/dist/commands/browse.js +12 -12
- package/dist/commands/clone.js +18 -18
- package/dist/commands/export.js +7 -26
- package/dist/commands/hist.js +9 -10
- package/dist/commands/login.js +55 -23
- package/dist/commands/new.js +26 -26
- package/dist/commands/publish.js +40 -34
- package/dist/commands/save.js +7 -1
- package/dist/commands/submit.js +41 -41
- package/dist/commands/validate.js +4 -4
- package/dist/index.js +0 -20
- package/dist/lib/api.js +14 -3
- package/dist/lib/auth.js +4 -1
- package/dist/lib/git.js +13 -13
- package/dist/lib/github.js +8 -8
- package/dist/lib/history.js +72 -28
- package/dist/lib/pmptFile.js +73 -1
- package/dist/lib/schema.js +11 -11
- package/dist/lib/template.js +1 -1
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -2,8 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
**Record and share your AI-driven product development journey.**
|
|
4
4
|
|
|
5
|
-
AI와 대화하며 제품을 만드는 여정을 기록하고 공유하세요.
|
|
6
|
-
|
|
7
5
|
[](https://www.npmjs.com/package/pmpt-cli)
|
|
8
6
|
|
|
9
7
|
**Website**: [pmptwiki.com](https://pmptwiki.com)
|
|
@@ -44,7 +42,7 @@ pmpt publish
|
|
|
44
42
|
- **5 questions** — Quick product planning with AI-ready prompts
|
|
45
43
|
- **Version history** — Track every step of your AI-assisted development
|
|
46
44
|
- **Share & reproduce** — Publish projects for others to learn from and clone
|
|
47
|
-
- **Project hub** — Browse and clone projects at [pmptwiki.com](https://pmptwiki.com/
|
|
45
|
+
- **Project hub** — Browse and clone projects at [pmptwiki.com](https://pmptwiki.com/explore)
|
|
48
46
|
|
|
49
47
|
---
|
|
50
48
|
|
|
@@ -141,7 +139,7 @@ Single JSON file containing your entire development journey:
|
|
|
141
139
|
## Links
|
|
142
140
|
|
|
143
141
|
- [Website](https://pmptwiki.com)
|
|
144
|
-
- [Explore Projects](https://pmptwiki.com/
|
|
142
|
+
- [Explore Projects](https://pmptwiki.com/explore)
|
|
145
143
|
- [GitHub](https://github.com/pmptwiki/pmpt-cli)
|
|
146
144
|
- [npm](https://www.npmjs.com/package/pmpt-cli)
|
|
147
145
|
|
package/dist/commands/browse.js
CHANGED
|
@@ -3,27 +3,27 @@ import { fetchProjects } from '../lib/api.js';
|
|
|
3
3
|
export async function cmdBrowse() {
|
|
4
4
|
p.intro('pmpt browse');
|
|
5
5
|
const s = p.spinner();
|
|
6
|
-
s.start('
|
|
6
|
+
s.start('Loading projects...');
|
|
7
7
|
let projects;
|
|
8
8
|
try {
|
|
9
9
|
const index = await fetchProjects();
|
|
10
10
|
projects = index.projects;
|
|
11
11
|
}
|
|
12
12
|
catch (err) {
|
|
13
|
-
s.stop('
|
|
14
|
-
p.log.error(err instanceof Error ? err.message : '
|
|
13
|
+
s.stop('Failed to load');
|
|
14
|
+
p.log.error(err instanceof Error ? err.message : 'Could not load project list.');
|
|
15
15
|
process.exit(1);
|
|
16
16
|
}
|
|
17
|
-
s.stop(`${projects.length}
|
|
17
|
+
s.stop(`${projects.length} projects`);
|
|
18
18
|
if (projects.length === 0) {
|
|
19
|
-
p.log.info('
|
|
20
|
-
p.log.message(' pmpt publish —
|
|
19
|
+
p.log.info('No published projects yet.');
|
|
20
|
+
p.log.message(' pmpt publish — share your first project!');
|
|
21
21
|
p.outro('');
|
|
22
22
|
return;
|
|
23
23
|
}
|
|
24
24
|
// Select project
|
|
25
25
|
const selected = await p.select({
|
|
26
|
-
message: '
|
|
26
|
+
message: 'Select a project:',
|
|
27
27
|
options: projects.map((proj) => ({
|
|
28
28
|
value: proj.slug,
|
|
29
29
|
label: proj.projectName,
|
|
@@ -47,11 +47,11 @@ export async function cmdBrowse() {
|
|
|
47
47
|
].filter(Boolean).join('\n'), 'Project Details');
|
|
48
48
|
// Action
|
|
49
49
|
const action = await p.select({
|
|
50
|
-
message: '
|
|
50
|
+
message: 'What would you like to do?',
|
|
51
51
|
options: [
|
|
52
|
-
{ value: 'clone', label: '
|
|
53
|
-
{ value: 'url', label: 'URL
|
|
54
|
-
{ value: 'back', label: '
|
|
52
|
+
{ value: 'clone', label: 'Clone this project', hint: 'pmpt clone' },
|
|
53
|
+
{ value: 'url', label: 'Show URL', hint: 'View in browser' },
|
|
54
|
+
{ value: 'back', label: 'Go back' },
|
|
55
55
|
],
|
|
56
56
|
});
|
|
57
57
|
if (p.isCancel(action) || action === 'back') {
|
|
@@ -67,7 +67,7 @@ export async function cmdBrowse() {
|
|
|
67
67
|
const url = `https://pmptwiki.com/ko/p/${project.slug}`;
|
|
68
68
|
p.log.info(`URL: ${url}`);
|
|
69
69
|
p.log.message(`Download: ${project.downloadUrl}`);
|
|
70
|
-
p.log.message(`\npmpt clone ${project.slug} —
|
|
70
|
+
p.log.message(`\npmpt clone ${project.slug} — clone via terminal`);
|
|
71
71
|
p.outro('');
|
|
72
72
|
}
|
|
73
73
|
}
|
package/dist/commands/clone.js
CHANGED
|
@@ -46,31 +46,31 @@ export function restoreDocs(docsDir, docs) {
|
|
|
46
46
|
}
|
|
47
47
|
export async function cmdClone(slug) {
|
|
48
48
|
if (!slug) {
|
|
49
|
-
p.log.error('slug
|
|
50
|
-
p.log.info('
|
|
49
|
+
p.log.error('Please provide a slug.');
|
|
50
|
+
p.log.info('Usage: pmpt clone <slug>');
|
|
51
51
|
process.exit(1);
|
|
52
52
|
}
|
|
53
53
|
p.intro(`pmpt clone — ${slug}`);
|
|
54
54
|
const s = p.spinner();
|
|
55
|
-
s.start('
|
|
55
|
+
s.start('Downloading project...');
|
|
56
56
|
let fileContent;
|
|
57
57
|
try {
|
|
58
58
|
fileContent = await fetchPmptFile(slug);
|
|
59
59
|
}
|
|
60
60
|
catch (err) {
|
|
61
|
-
s.stop('
|
|
62
|
-
p.log.error(err instanceof Error ? err.message : '
|
|
61
|
+
s.stop('Download failed');
|
|
62
|
+
p.log.error(err instanceof Error ? err.message : 'Project not found.');
|
|
63
63
|
process.exit(1);
|
|
64
64
|
}
|
|
65
|
-
s.message('
|
|
65
|
+
s.message('Validating...');
|
|
66
66
|
const validation = validatePmptFile(fileContent);
|
|
67
67
|
if (!validation.success || !validation.data) {
|
|
68
|
-
s.stop('
|
|
69
|
-
p.log.error(validation.error || '
|
|
68
|
+
s.stop('Validation failed');
|
|
69
|
+
p.log.error(validation.error || 'Invalid .pmpt file.');
|
|
70
70
|
process.exit(1);
|
|
71
71
|
}
|
|
72
72
|
const pmptData = validation.data;
|
|
73
|
-
s.stop('
|
|
73
|
+
s.stop('Download complete');
|
|
74
74
|
// Show summary
|
|
75
75
|
p.note([
|
|
76
76
|
`Project: ${pmptData.meta.projectName}`,
|
|
@@ -81,16 +81,16 @@ export async function cmdClone(slug) {
|
|
|
81
81
|
const projectPath = process.cwd();
|
|
82
82
|
if (isInitialized(projectPath)) {
|
|
83
83
|
const overwrite = await p.confirm({
|
|
84
|
-
message: '
|
|
84
|
+
message: 'Project already initialized. Merge history?',
|
|
85
85
|
initialValue: true,
|
|
86
86
|
});
|
|
87
87
|
if (p.isCancel(overwrite) || !overwrite) {
|
|
88
|
-
p.cancel('
|
|
88
|
+
p.cancel('Cancelled');
|
|
89
89
|
process.exit(0);
|
|
90
90
|
}
|
|
91
91
|
}
|
|
92
92
|
const importSpinner = p.spinner();
|
|
93
|
-
importSpinner.start('
|
|
93
|
+
importSpinner.start('Restoring project...');
|
|
94
94
|
if (!isInitialized(projectPath)) {
|
|
95
95
|
initializeProject(projectPath, { trackGit: true });
|
|
96
96
|
}
|
|
@@ -113,15 +113,15 @@ export async function cmdClone(slug) {
|
|
|
113
113
|
if (existsSync(historyDir)) {
|
|
114
114
|
versionCount = readdirSync(historyDir).filter((d) => d.startsWith('v')).length;
|
|
115
115
|
}
|
|
116
|
-
importSpinner.stop('
|
|
116
|
+
importSpinner.stop('Restore complete!');
|
|
117
117
|
p.note([
|
|
118
118
|
`Project: ${pmptData.meta.projectName}`,
|
|
119
119
|
`Versions: ${versionCount}`,
|
|
120
120
|
`Location: ${pmptDir}`,
|
|
121
121
|
].join('\n'), 'Clone Summary');
|
|
122
|
-
p.log.info('
|
|
123
|
-
p.log.message(' pmpt history —
|
|
124
|
-
p.log.message(' pmpt plan — AI
|
|
125
|
-
p.log.message(' pmpt save —
|
|
126
|
-
p.outro('
|
|
122
|
+
p.log.info('Next steps:');
|
|
123
|
+
p.log.message(' pmpt history — view version history');
|
|
124
|
+
p.log.message(' pmpt plan — view AI prompt');
|
|
125
|
+
p.log.message(' pmpt save — save a new snapshot');
|
|
126
|
+
p.outro('Project cloned!');
|
|
127
127
|
}
|
package/dist/commands/export.js
CHANGED
|
@@ -2,29 +2,10 @@ import * as p from '@clack/prompts';
|
|
|
2
2
|
import { resolve, join, basename } from 'path';
|
|
3
3
|
import { existsSync, readFileSync, writeFileSync, statSync } from 'fs';
|
|
4
4
|
import { isInitialized, getDocsDir, loadConfig } from '../lib/config.js';
|
|
5
|
-
import { getAllSnapshots } from '../lib/history.js';
|
|
5
|
+
import { getAllSnapshots, resolveFullSnapshot } from '../lib/history.js';
|
|
6
6
|
import { getPlanProgress } from '../lib/plan.js';
|
|
7
7
|
import { createPmptFile, SCHEMA_VERSION } from '../lib/pmptFile.js';
|
|
8
8
|
import glob from 'fast-glob';
|
|
9
|
-
/**
|
|
10
|
-
* Read all files from a snapshot directory
|
|
11
|
-
*/
|
|
12
|
-
function readSnapshotFiles(snapshotDir) {
|
|
13
|
-
const files = {};
|
|
14
|
-
if (!existsSync(snapshotDir))
|
|
15
|
-
return files;
|
|
16
|
-
const mdFiles = glob.sync('**/*.md', { cwd: snapshotDir });
|
|
17
|
-
for (const file of mdFiles) {
|
|
18
|
-
const filePath = join(snapshotDir, file);
|
|
19
|
-
try {
|
|
20
|
-
files[file] = readFileSync(filePath, 'utf-8');
|
|
21
|
-
}
|
|
22
|
-
catch {
|
|
23
|
-
// Skip files that can't be read
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
return files;
|
|
27
|
-
}
|
|
28
9
|
/**
|
|
29
10
|
* Read current docs folder
|
|
30
11
|
*/
|
|
@@ -69,15 +50,15 @@ export async function cmdExport(path, options) {
|
|
|
69
50
|
: resolve(projectPath, `${exportName}.pmpt`);
|
|
70
51
|
const s = p.spinner();
|
|
71
52
|
s.start('Creating .pmpt file...');
|
|
72
|
-
// Build history array with file contents
|
|
53
|
+
// Build history array with file contents (resolve from optimized snapshots)
|
|
73
54
|
const history = [];
|
|
74
|
-
for (
|
|
75
|
-
const files =
|
|
55
|
+
for (let i = 0; i < snapshots.length; i++) {
|
|
56
|
+
const files = resolveFullSnapshot(snapshots, i);
|
|
76
57
|
history.push({
|
|
77
|
-
version:
|
|
78
|
-
timestamp:
|
|
58
|
+
version: snapshots[i].version,
|
|
59
|
+
timestamp: snapshots[i].timestamp,
|
|
79
60
|
files,
|
|
80
|
-
git:
|
|
61
|
+
git: snapshots[i].git,
|
|
81
62
|
});
|
|
82
63
|
}
|
|
83
64
|
// Read current docs
|
package/dist/commands/hist.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import * as p from '@clack/prompts';
|
|
2
|
-
import { resolve
|
|
3
|
-
import { readFileSync, existsSync } from 'fs';
|
|
2
|
+
import { resolve } from 'path';
|
|
4
3
|
import { isInitialized } from '../lib/config.js';
|
|
5
|
-
import { getAllSnapshots } from '../lib/history.js';
|
|
4
|
+
import { getAllSnapshots, resolveFileContent } from '../lib/history.js';
|
|
6
5
|
// Simple diff calculation: count changed lines
|
|
7
6
|
function calculateDiffSize(oldContent, newContent) {
|
|
8
7
|
const oldLines = oldContent.split('\n');
|
|
@@ -16,15 +15,15 @@ function calculateDiffSize(oldContent, newContent) {
|
|
|
16
15
|
}
|
|
17
16
|
return changes;
|
|
18
17
|
}
|
|
19
|
-
// Get total diff between two snapshots
|
|
20
|
-
function getSnapshotDiff(
|
|
18
|
+
// Get total diff between two snapshots (supports optimized snapshots)
|
|
19
|
+
function getSnapshotDiff(snapshots, prevIndex, currIndex) {
|
|
20
|
+
const prev = snapshots[prevIndex];
|
|
21
|
+
const curr = snapshots[currIndex];
|
|
21
22
|
let totalChanges = 0;
|
|
22
23
|
const allFiles = new Set([...prev.files, ...curr.files]);
|
|
23
24
|
for (const file of allFiles) {
|
|
24
|
-
const
|
|
25
|
-
const
|
|
26
|
-
const prevContent = existsSync(prevPath) ? readFileSync(prevPath, 'utf-8') : '';
|
|
27
|
-
const currContent = existsSync(currPath) ? readFileSync(currPath, 'utf-8') : '';
|
|
25
|
+
const prevContent = resolveFileContent(snapshots, prevIndex, file) || '';
|
|
26
|
+
const currContent = resolveFileContent(snapshots, currIndex, file) || '';
|
|
28
27
|
totalChanges += calculateDiffSize(prevContent, currContent);
|
|
29
28
|
}
|
|
30
29
|
return totalChanges;
|
|
@@ -49,7 +48,7 @@ export function cmdHistory(path, options) {
|
|
|
49
48
|
if (options?.compact && snapshots.length > 1) {
|
|
50
49
|
displaySnapshots = [snapshots[0]]; // Always show first
|
|
51
50
|
for (let i = 1; i < snapshots.length; i++) {
|
|
52
|
-
const diffSize = getSnapshotDiff(snapshots
|
|
51
|
+
const diffSize = getSnapshotDiff(snapshots, i - 1, i);
|
|
53
52
|
// Threshold: hide if less than 5 lines changed
|
|
54
53
|
if (diffSize < 5) {
|
|
55
54
|
hiddenVersions.push(snapshots[i].version);
|
package/dist/commands/login.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as p from '@clack/prompts';
|
|
2
|
+
import open from 'open';
|
|
2
3
|
import { loadAuth, saveAuth } from '../lib/auth.js';
|
|
3
|
-
import {
|
|
4
|
+
import { requestDeviceCode, pollDeviceToken } from '../lib/api.js';
|
|
4
5
|
export async function cmdLogin() {
|
|
5
6
|
p.intro('pmpt login');
|
|
6
7
|
const existing = loadAuth();
|
|
@@ -15,32 +16,63 @@ export async function cmdLogin() {
|
|
|
15
16
|
return;
|
|
16
17
|
}
|
|
17
18
|
}
|
|
18
|
-
|
|
19
|
-
' https://github.com/settings/tokens/new\n' +
|
|
20
|
-
' 필요 권한: read:user');
|
|
21
|
-
const pat = await p.password({
|
|
22
|
-
message: 'GitHub PAT를 입력하세요:',
|
|
23
|
-
validate: (v) => (v.trim().length < 10 ? '올바른 토큰을 입력하세요' : undefined),
|
|
24
|
-
});
|
|
25
|
-
if (p.isCancel(pat)) {
|
|
26
|
-
p.cancel('취소됨');
|
|
27
|
-
process.exit(0);
|
|
28
|
-
}
|
|
19
|
+
// Step 1: Request device code
|
|
29
20
|
const s = p.spinner();
|
|
30
|
-
s.start('
|
|
21
|
+
s.start('Preparing GitHub authentication...');
|
|
22
|
+
let device;
|
|
31
23
|
try {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
token: result.token,
|
|
35
|
-
githubToken: pat,
|
|
36
|
-
username: result.username,
|
|
37
|
-
});
|
|
38
|
-
s.stop(`인증 완료 — @${result.username}`);
|
|
24
|
+
device = await requestDeviceCode();
|
|
25
|
+
s.stop('Verification code issued.');
|
|
39
26
|
}
|
|
40
27
|
catch (err) {
|
|
41
|
-
s.stop('
|
|
42
|
-
p.log.error(err instanceof Error ? err.message : '
|
|
28
|
+
s.stop('Failed to issue verification code');
|
|
29
|
+
p.log.error(err instanceof Error ? err.message : 'Failed to prepare authentication.');
|
|
43
30
|
process.exit(1);
|
|
44
31
|
}
|
|
45
|
-
|
|
32
|
+
// Step 2: Show code and open browser
|
|
33
|
+
p.log.info(`Enter this code on GitHub:\n\n` +
|
|
34
|
+
` Code: ${device.userCode}\n` +
|
|
35
|
+
` URL: ${device.verificationUri}`);
|
|
36
|
+
const shouldOpen = await p.confirm({
|
|
37
|
+
message: 'Open browser?',
|
|
38
|
+
initialValue: true,
|
|
39
|
+
});
|
|
40
|
+
if (p.isCancel(shouldOpen)) {
|
|
41
|
+
p.cancel('Cancelled');
|
|
42
|
+
process.exit(0);
|
|
43
|
+
}
|
|
44
|
+
if (shouldOpen) {
|
|
45
|
+
await open(device.verificationUri);
|
|
46
|
+
}
|
|
47
|
+
// Step 3: Poll for token
|
|
48
|
+
s.start('Waiting for GitHub authorization... (enter the code in your browser)');
|
|
49
|
+
let interval = device.interval * 1000; // seconds → ms
|
|
50
|
+
const deadline = Date.now() + device.expiresIn * 1000;
|
|
51
|
+
while (Date.now() < deadline) {
|
|
52
|
+
await sleep(interval);
|
|
53
|
+
try {
|
|
54
|
+
const result = await pollDeviceToken(device.deviceCode);
|
|
55
|
+
if (result.status === 'complete') {
|
|
56
|
+
saveAuth({ token: result.token, username: result.username });
|
|
57
|
+
s.stop(`Authenticated — @${result.username}`);
|
|
58
|
+
p.outro('Login complete! Build projects with AI using pmpt and share your vibe coding journey on pmptwiki.');
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (result.status === 'slow_down') {
|
|
62
|
+
interval = (result.interval ?? 10) * 1000;
|
|
63
|
+
}
|
|
64
|
+
// status === 'pending' → keep polling
|
|
65
|
+
}
|
|
66
|
+
catch (err) {
|
|
67
|
+
s.stop('Authentication failed');
|
|
68
|
+
p.log.error(err instanceof Error ? err.message : 'Authentication failed.');
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
s.stop('Verification code expired.');
|
|
73
|
+
p.log.error('Please run pmpt login again.');
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
function sleep(ms) {
|
|
77
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
46
78
|
}
|
package/dist/commands/new.js
CHANGED
|
@@ -3,55 +3,55 @@ import { writeFileSync, mkdirSync } from 'fs';
|
|
|
3
3
|
import { dirname } from 'path';
|
|
4
4
|
import { generateContent, generateFilePath } from '../lib/template.js';
|
|
5
5
|
export async function cmdNew() {
|
|
6
|
-
p.intro('pmptwiki —
|
|
6
|
+
p.intro('pmptwiki — create new document');
|
|
7
7
|
const answers = await p.group({
|
|
8
8
|
lang: () => p.select({
|
|
9
|
-
message: '
|
|
9
|
+
message: 'Select language',
|
|
10
10
|
options: [
|
|
11
|
-
{ value: 'ko', label: '
|
|
11
|
+
{ value: 'ko', label: 'Korean (ko)' },
|
|
12
12
|
{ value: 'en', label: 'English (en)' },
|
|
13
13
|
],
|
|
14
14
|
}),
|
|
15
15
|
purpose: () => p.select({
|
|
16
|
-
message: '
|
|
16
|
+
message: 'Select document type',
|
|
17
17
|
options: [
|
|
18
|
-
{ value: 'guide', label: '
|
|
19
|
-
{ value: 'rule', label: '
|
|
20
|
-
{ value: 'template', label: '
|
|
21
|
-
{ value: 'example', label: '
|
|
22
|
-
{ value: 'reference', label: '
|
|
18
|
+
{ value: 'guide', label: 'Guide', hint: 'Concept explanation + how-to' },
|
|
19
|
+
{ value: 'rule', label: 'Rule', hint: 'Do / Don\'t' },
|
|
20
|
+
{ value: 'template', label: 'Template', hint: 'Copy-paste prompt' },
|
|
21
|
+
{ value: 'example', label: 'Example', hint: 'Real-world use case' },
|
|
22
|
+
{ value: 'reference', label: 'Reference', hint: 'Resource collection' },
|
|
23
23
|
],
|
|
24
24
|
}),
|
|
25
25
|
level: () => p.select({
|
|
26
|
-
message: '
|
|
26
|
+
message: 'Select difficulty',
|
|
27
27
|
options: [
|
|
28
|
-
{ value: 'beginner', label: '
|
|
29
|
-
{ value: 'intermediate', label: '
|
|
30
|
-
{ value: 'advanced', label: '
|
|
28
|
+
{ value: 'beginner', label: 'Beginner' },
|
|
29
|
+
{ value: 'intermediate', label: 'Intermediate' },
|
|
30
|
+
{ value: 'advanced', label: 'Advanced' },
|
|
31
31
|
],
|
|
32
32
|
}),
|
|
33
33
|
title: () => p.text({
|
|
34
|
-
message: '
|
|
35
|
-
placeholder: '
|
|
36
|
-
validate: (v) => (v.trim().length < 5 ? '5
|
|
34
|
+
message: 'Enter a title',
|
|
35
|
+
placeholder: 'Providing enough context to AI changes the response',
|
|
36
|
+
validate: (v) => (v.trim().length < 5 ? 'At least 5 characters required' : undefined),
|
|
37
37
|
}),
|
|
38
38
|
tags: () => p.text({
|
|
39
|
-
message: '
|
|
39
|
+
message: 'Enter tags (comma-separated, optional)',
|
|
40
40
|
placeholder: 'context, beginner, prompt',
|
|
41
41
|
}),
|
|
42
42
|
persona: () => p.multiselect({
|
|
43
|
-
message: '
|
|
43
|
+
message: 'Select target audience (optional)',
|
|
44
44
|
options: [
|
|
45
|
-
{ value: 'general', label: '
|
|
46
|
-
{ value: 'power-user', label: '
|
|
47
|
-
{ value: 'developer', label: '
|
|
48
|
-
{ value: 'organization', label: '
|
|
45
|
+
{ value: 'general', label: 'General' },
|
|
46
|
+
{ value: 'power-user', label: 'Power User' },
|
|
47
|
+
{ value: 'developer', label: 'Developer' },
|
|
48
|
+
{ value: 'organization', label: 'Organization' },
|
|
49
49
|
],
|
|
50
50
|
required: false,
|
|
51
51
|
}),
|
|
52
52
|
}, {
|
|
53
53
|
onCancel: () => {
|
|
54
|
-
p.cancel('
|
|
54
|
+
p.cancel('Cancelled');
|
|
55
55
|
process.exit(0);
|
|
56
56
|
},
|
|
57
57
|
});
|
|
@@ -69,10 +69,10 @@ export async function cmdNew() {
|
|
|
69
69
|
const content = generateContent(fm);
|
|
70
70
|
mkdirSync(dirname(filePath), { recursive: true });
|
|
71
71
|
writeFileSync(filePath, content, 'utf-8');
|
|
72
|
-
p.outro(
|
|
72
|
+
p.outro(`File created: ${filePath}
|
|
73
73
|
|
|
74
|
-
|
|
75
|
-
1.
|
|
74
|
+
Next steps:
|
|
75
|
+
1. Open the file and write the content
|
|
76
76
|
2. pmpt validate ${filePath}
|
|
77
77
|
3. pmpt submit ${filePath}`);
|
|
78
78
|
}
|
package/dist/commands/publish.js
CHANGED
|
@@ -2,26 +2,13 @@ import * as p from '@clack/prompts';
|
|
|
2
2
|
import { resolve, basename } from 'path';
|
|
3
3
|
import { readFileSync, existsSync } from 'fs';
|
|
4
4
|
import { isInitialized, loadConfig, saveConfig, getDocsDir } from '../lib/config.js';
|
|
5
|
-
import { getAllSnapshots } from '../lib/history.js';
|
|
5
|
+
import { getAllSnapshots, resolveFullSnapshot } from '../lib/history.js';
|
|
6
6
|
import { getPlanProgress } from '../lib/plan.js';
|
|
7
7
|
import { createPmptFile } from '../lib/pmptFile.js';
|
|
8
8
|
import { loadAuth } from '../lib/auth.js';
|
|
9
9
|
import { publishProject } from '../lib/api.js';
|
|
10
10
|
import glob from 'fast-glob';
|
|
11
11
|
import { join } from 'path';
|
|
12
|
-
function readSnapshotFiles(snapshotDir) {
|
|
13
|
-
const files = {};
|
|
14
|
-
if (!existsSync(snapshotDir))
|
|
15
|
-
return files;
|
|
16
|
-
const mdFiles = glob.sync('**/*.md', { cwd: snapshotDir });
|
|
17
|
-
for (const file of mdFiles) {
|
|
18
|
-
try {
|
|
19
|
-
files[file] = readFileSync(join(snapshotDir, file), 'utf-8');
|
|
20
|
-
}
|
|
21
|
-
catch { /* skip */ }
|
|
22
|
-
}
|
|
23
|
-
return files;
|
|
24
|
-
}
|
|
25
12
|
function readDocsFolder(docsDir) {
|
|
26
13
|
const files = {};
|
|
27
14
|
if (!existsSync(docsDir))
|
|
@@ -38,12 +25,12 @@ function readDocsFolder(docsDir) {
|
|
|
38
25
|
export async function cmdPublish(path) {
|
|
39
26
|
const projectPath = path ? resolve(path) : process.cwd();
|
|
40
27
|
if (!isInitialized(projectPath)) {
|
|
41
|
-
p.log.error('
|
|
28
|
+
p.log.error('Project not initialized. Run `pmpt init` first.');
|
|
42
29
|
process.exit(1);
|
|
43
30
|
}
|
|
44
31
|
const auth = loadAuth();
|
|
45
32
|
if (!auth?.token || !auth?.username) {
|
|
46
|
-
p.log.error('
|
|
33
|
+
p.log.error('Login required. Run `pmpt login` first.');
|
|
47
34
|
process.exit(1);
|
|
48
35
|
}
|
|
49
36
|
p.intro('pmpt publish');
|
|
@@ -51,52 +38,69 @@ export async function cmdPublish(path) {
|
|
|
51
38
|
const snapshots = getAllSnapshots(projectPath);
|
|
52
39
|
const planProgress = getPlanProgress(projectPath);
|
|
53
40
|
if (snapshots.length === 0) {
|
|
54
|
-
p.log.warn('
|
|
41
|
+
p.log.warn('No snapshots found. Run `pmpt save` or `pmpt plan` first.');
|
|
55
42
|
p.outro('');
|
|
56
43
|
return;
|
|
57
44
|
}
|
|
58
45
|
const projectName = planProgress?.answers?.projectName || basename(projectPath);
|
|
59
46
|
// Collect publish info
|
|
60
47
|
const slug = await p.text({
|
|
61
|
-
message: '
|
|
48
|
+
message: 'Project slug (used in URL):',
|
|
62
49
|
placeholder: projectName.toLowerCase().replace(/[^a-z0-9-]/g, '-').replace(/-+/g, '-'),
|
|
63
50
|
validate: (v) => {
|
|
64
51
|
if (!/^[a-z0-9][a-z0-9-]{1,48}[a-z0-9]$/.test(v)) {
|
|
65
|
-
return '3
|
|
52
|
+
return '3-50 chars, lowercase letters, numbers, and hyphens only.';
|
|
66
53
|
}
|
|
67
54
|
},
|
|
68
55
|
});
|
|
69
56
|
if (p.isCancel(slug)) {
|
|
70
|
-
p.cancel('
|
|
57
|
+
p.cancel('Cancelled');
|
|
71
58
|
process.exit(0);
|
|
72
59
|
}
|
|
73
60
|
const description = await p.text({
|
|
74
|
-
message: '
|
|
61
|
+
message: 'Project description (brief):',
|
|
75
62
|
placeholder: planProgress?.answers?.productIdea?.slice(0, 100) || '',
|
|
76
63
|
defaultValue: planProgress?.answers?.productIdea?.slice(0, 200) || '',
|
|
77
64
|
});
|
|
78
65
|
if (p.isCancel(description)) {
|
|
79
|
-
p.cancel('
|
|
66
|
+
p.cancel('Cancelled');
|
|
80
67
|
process.exit(0);
|
|
81
68
|
}
|
|
82
69
|
const tagsInput = await p.text({
|
|
83
|
-
message: '
|
|
70
|
+
message: 'Tags (comma-separated):',
|
|
84
71
|
placeholder: 'react, saas, mvp',
|
|
85
72
|
defaultValue: '',
|
|
86
73
|
});
|
|
87
74
|
if (p.isCancel(tagsInput)) {
|
|
88
|
-
p.cancel('
|
|
75
|
+
p.cancel('Cancelled');
|
|
89
76
|
process.exit(0);
|
|
90
77
|
}
|
|
91
78
|
const tags = tagsInput
|
|
92
79
|
.split(',')
|
|
93
80
|
.map((t) => t.trim().toLowerCase())
|
|
94
81
|
.filter(Boolean);
|
|
95
|
-
|
|
96
|
-
|
|
82
|
+
const category = await p.select({
|
|
83
|
+
message: 'Project category:',
|
|
84
|
+
options: [
|
|
85
|
+
{ value: 'web-app', label: 'Web App' },
|
|
86
|
+
{ value: 'mobile-app', label: 'Mobile App' },
|
|
87
|
+
{ value: 'cli-tool', label: 'CLI Tool' },
|
|
88
|
+
{ value: 'api-backend', label: 'API/Backend' },
|
|
89
|
+
{ value: 'ai-ml', label: 'AI/ML' },
|
|
90
|
+
{ value: 'game', label: 'Game' },
|
|
91
|
+
{ value: 'library', label: 'Library' },
|
|
92
|
+
{ value: 'other', label: 'Other' },
|
|
93
|
+
],
|
|
94
|
+
});
|
|
95
|
+
if (p.isCancel(category)) {
|
|
96
|
+
p.cancel('Cancelled');
|
|
97
|
+
process.exit(0);
|
|
98
|
+
}
|
|
99
|
+
// Build .pmpt content (resolve from optimized snapshots)
|
|
100
|
+
const history = snapshots.map((snapshot, i) => ({
|
|
97
101
|
version: snapshot.version,
|
|
98
102
|
timestamp: snapshot.timestamp,
|
|
99
|
-
files:
|
|
103
|
+
files: resolveFullSnapshot(snapshots, i),
|
|
100
104
|
git: snapshot.git,
|
|
101
105
|
}));
|
|
102
106
|
const docsDir = getDocsDir(projectPath);
|
|
@@ -125,27 +129,29 @@ export async function cmdPublish(path) {
|
|
|
125
129
|
`Versions: ${snapshots.length}`,
|
|
126
130
|
`Size: ${(pmptContent.length / 1024).toFixed(1)} KB`,
|
|
127
131
|
`Author: @${auth.username}`,
|
|
132
|
+
`Category: ${category}`,
|
|
128
133
|
tags.length ? `Tags: ${tags.join(', ')}` : '',
|
|
129
134
|
].filter(Boolean).join('\n'), 'Publish Preview');
|
|
130
135
|
const confirm = await p.confirm({
|
|
131
|
-
message: '
|
|
136
|
+
message: 'Publish this project?',
|
|
132
137
|
initialValue: true,
|
|
133
138
|
});
|
|
134
139
|
if (p.isCancel(confirm) || !confirm) {
|
|
135
|
-
p.cancel('
|
|
140
|
+
p.cancel('Cancelled');
|
|
136
141
|
process.exit(0);
|
|
137
142
|
}
|
|
138
143
|
// Upload
|
|
139
144
|
const s = p.spinner();
|
|
140
|
-
s.start('
|
|
145
|
+
s.start('Uploading...');
|
|
141
146
|
try {
|
|
142
147
|
const result = await publishProject(auth.token, {
|
|
143
148
|
slug: slug,
|
|
144
149
|
pmptContent,
|
|
145
150
|
description: description,
|
|
146
151
|
tags,
|
|
152
|
+
category: category,
|
|
147
153
|
});
|
|
148
|
-
s.stop('
|
|
154
|
+
s.stop('Published!');
|
|
149
155
|
// Update config
|
|
150
156
|
if (config) {
|
|
151
157
|
config.lastPublished = new Date().toISOString();
|
|
@@ -155,12 +161,12 @@ export async function cmdPublish(path) {
|
|
|
155
161
|
`URL: ${result.url}`,
|
|
156
162
|
`Download: ${result.downloadUrl}`,
|
|
157
163
|
'',
|
|
158
|
-
`pmpt clone ${slug} —
|
|
164
|
+
`pmpt clone ${slug} — others can clone this project`,
|
|
159
165
|
].join('\n'), 'Published!');
|
|
160
166
|
}
|
|
161
167
|
catch (err) {
|
|
162
|
-
s.stop('
|
|
163
|
-
p.log.error(err instanceof Error ? err.message : '
|
|
168
|
+
s.stop('Publish failed');
|
|
169
|
+
p.log.error(err instanceof Error ? err.message : 'Failed to publish.');
|
|
164
170
|
process.exit(1);
|
|
165
171
|
}
|
|
166
172
|
p.outro('');
|
package/dist/commands/save.js
CHANGED
|
@@ -32,11 +32,17 @@ export async function cmdSave(fileOrPath) {
|
|
|
32
32
|
if (entry.git.dirty)
|
|
33
33
|
msg += ' (uncommitted)';
|
|
34
34
|
}
|
|
35
|
+
const changedCount = entry.changedFiles?.length ?? entry.files.length;
|
|
36
|
+
const unchangedCount = entry.files.length - changedCount;
|
|
37
|
+
if (unchangedCount > 0) {
|
|
38
|
+
msg += ` (${changedCount} changed, ${unchangedCount} skipped)`;
|
|
39
|
+
}
|
|
35
40
|
p.log.success(msg);
|
|
36
41
|
p.log.message('');
|
|
37
42
|
p.log.info('Files included:');
|
|
38
43
|
for (const file of entry.files) {
|
|
39
|
-
|
|
44
|
+
const isChanged = entry.changedFiles ? entry.changedFiles.includes(file) : true;
|
|
45
|
+
p.log.message(` - ${file}${isChanged ? '' : ' (unchanged)'}`);
|
|
40
46
|
}
|
|
41
47
|
}
|
|
42
48
|
catch (error) {
|