pmpt-cli 1.5.1 → 1.5.2

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/index.js CHANGED
@@ -17,7 +17,7 @@ const program = new Command();
17
17
  program
18
18
  .name('pmpt')
19
19
  .description('pmpt — Record and share your AI-driven product development journey')
20
- .version('1.4.1')
20
+ .version('1.5.2')
21
21
  .addHelpText('after', `
22
22
  Examples:
23
23
  $ pmpt init Initialize project
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pmpt-cli",
3
- "version": "1.5.1",
3
+ "version": "1.5.2",
4
4
  "description": "Record and share your AI-driven product development journey",
5
5
  "type": "module",
6
6
  "bin": {
@@ -35,11 +35,9 @@
35
35
  },
36
36
  "dependencies": {
37
37
  "@clack/prompts": "^0.7.0",
38
- "@octokit/rest": "^21.0.0",
39
38
  "chokidar": "^3.6.0",
40
39
  "commander": "^12.0.0",
41
40
  "fast-glob": "^3.3.0",
42
- "gray-matter": "^4.0.3",
43
41
  "open": "^11.0.0",
44
42
  "zod": "^3.22.0"
45
43
  },
@@ -1,78 +0,0 @@
1
- import * as p from '@clack/prompts';
2
- import { writeFileSync, mkdirSync } from 'fs';
3
- import { dirname } from 'path';
4
- import { generateContent, generateFilePath } from '../lib/template.js';
5
- export async function cmdNew() {
6
- p.intro('pmptwiki — create new document');
7
- const answers = await p.group({
8
- lang: () => p.select({
9
- message: 'Select language',
10
- options: [
11
- { value: 'ko', label: 'Korean (ko)' },
12
- { value: 'en', label: 'English (en)' },
13
- ],
14
- }),
15
- purpose: () => p.select({
16
- message: 'Select document type',
17
- options: [
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
- ],
24
- }),
25
- level: () => p.select({
26
- message: 'Select difficulty',
27
- options: [
28
- { value: 'beginner', label: 'Beginner' },
29
- { value: 'intermediate', label: 'Intermediate' },
30
- { value: 'advanced', label: 'Advanced' },
31
- ],
32
- }),
33
- title: () => p.text({
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
- }),
38
- tags: () => p.text({
39
- message: 'Enter tags (comma-separated, optional)',
40
- placeholder: 'context, beginner, prompt',
41
- }),
42
- persona: () => p.multiselect({
43
- message: 'Select target audience (optional)',
44
- options: [
45
- { value: 'general', label: 'General' },
46
- { value: 'power-user', label: 'Power User' },
47
- { value: 'developer', label: 'Developer' },
48
- { value: 'organization', label: 'Organization' },
49
- ],
50
- required: false,
51
- }),
52
- }, {
53
- onCancel: () => {
54
- p.cancel('Cancelled');
55
- process.exit(0);
56
- },
57
- });
58
- const fm = {
59
- title: answers.title,
60
- purpose: answers.purpose,
61
- level: answers.level,
62
- lang: answers.lang,
63
- tags: answers.tags
64
- ? answers.tags.split(',').map((t) => t.trim()).filter(Boolean)
65
- : [],
66
- persona: answers.persona.length ? answers.persona : undefined,
67
- };
68
- const filePath = generateFilePath(fm);
69
- const content = generateContent(fm);
70
- mkdirSync(dirname(filePath), { recursive: true });
71
- writeFileSync(filePath, content, 'utf-8');
72
- p.outro(`File created: ${filePath}
73
-
74
- Next steps:
75
- 1. Open the file and write the content
76
- 2. pmpt validate ${filePath}
77
- 3. pmpt submit ${filePath}`);
78
- }
@@ -1,103 +0,0 @@
1
- import * as p from '@clack/prompts';
2
- import matter from 'gray-matter';
3
- import { readFileSync } from 'fs';
4
- import { loadAuth, saveAuth } from '../lib/auth.js';
5
- import { createClient, createBranch, createPR, ensureFork, getAuthUser, pushFile, } from '../lib/github.js';
6
- import { validate } from '../lib/schema.js';
7
- import { today } from '../lib/template.js';
8
- export async function cmdSubmit(filePath) {
9
- p.intro(`pmptwiki — submit: ${filePath}`);
10
- // 1. Validate
11
- const s1 = p.spinner();
12
- s1.start('Validating file...');
13
- const result = validate(filePath);
14
- if (!result.valid) {
15
- s1.stop('Validation failed');
16
- for (const err of result.errors)
17
- p.log.error(err);
18
- p.outro('Fix errors and retry: pmpt validate ' + filePath);
19
- process.exit(1);
20
- }
21
- s1.stop(`Validation passed${result.warnings.length ? ` (${result.warnings.length} warnings)` : ''}`);
22
- for (const warn of result.warnings)
23
- p.log.warn(warn);
24
- // 2. Auth
25
- let auth = loadAuth();
26
- if (!auth) {
27
- p.log.info('GitHub authentication required.');
28
- p.log.info('Create a Personal Access Token:\n https://github.com/settings/tokens/new\n Required scope: repo (full)');
29
- const token = await p.password({
30
- message: 'Enter your GitHub PAT:',
31
- validate: (v) => (v.trim().length < 10 ? 'Please enter a valid token' : undefined),
32
- });
33
- if (p.isCancel(token)) {
34
- p.cancel('Cancelled');
35
- process.exit(0);
36
- }
37
- const s2 = p.spinner();
38
- s2.start('Verifying authentication...');
39
- try {
40
- const octokit = createClient(token);
41
- const username = await getAuthUser(octokit);
42
- saveAuth({ token: token, username });
43
- auth = { token: token, username };
44
- s2.stop(`Authenticated — @${username}`);
45
- }
46
- catch {
47
- s2.stop('Authentication failed');
48
- p.outro('Invalid token. Please try again');
49
- process.exit(1);
50
- }
51
- }
52
- const octokit = createClient(auth.token);
53
- // 3. Generate branch name
54
- const { data: fm } = matter(readFileSync(filePath, 'utf-8'));
55
- const slug = filePath
56
- .replace(/^.*?(?=ko\/|en\/)/, '')
57
- .replace(/\.mdx?$/, '')
58
- .replace(/\//g, '-');
59
- const branchName = `content/${slug}-${today()}`;
60
- // 4. Check / create fork
61
- const s3 = p.spinner();
62
- s3.start('Checking fork...');
63
- await ensureFork(octokit, auth.username);
64
- s3.stop('Fork ready');
65
- // 5. Create branch
66
- const s4 = p.spinner();
67
- s4.start(`Creating branch: ${branchName}`);
68
- await createBranch(octokit, auth.username, branchName);
69
- s4.stop('Branch created');
70
- // 6. Push file
71
- const repoPath = filePath.replace(/^.*?(?=ko\/|en\/)/, '');
72
- const s5 = p.spinner();
73
- s5.start('Uploading file...');
74
- await pushFile(octokit, auth.username, branchName, repoPath, filePath, `docs: add ${repoPath}`);
75
- s5.stop('File uploaded');
76
- // 7. Create PR
77
- const prTitle = fm.purpose
78
- ? `[${fm.purpose}] ${fm.title}`
79
- : fm.title;
80
- const prBody = [
81
- `## Document Info`,
82
- `- **Title**: ${fm.title}`,
83
- `- **Type**: ${fm.purpose ?? '-'}`,
84
- `- **Level**: ${fm.level ?? '-'}`,
85
- `- **Language**: ${fm.lang ?? '-'}`,
86
- fm.tags?.length ? `- **Tags**: ${fm.tags.map((t) => `\`${t}\``).join(' ')}` : null,
87
- ``,
88
- `## Checklist`,
89
- `- [ ] Is the content clear and practical?`,
90
- `- [ ] Are examples included?`,
91
- `- [ ] Does the title match the content?`,
92
- ``,
93
- `---`,
94
- `_Submitted via pmpt-cli_`,
95
- ]
96
- .filter((l) => l !== null)
97
- .join('\n');
98
- const s6 = p.spinner();
99
- s6.start('Creating PR...');
100
- const prUrl = await createPR(octokit, auth.username, branchName, prTitle, prBody);
101
- s6.stop('PR created');
102
- p.outro(`Submitted!\n\n PR: ${prUrl}\n\nOnce reviewed and merged, it will be published on pmptwiki.com.`);
103
- }
@@ -1,23 +0,0 @@
1
- import * as p from '@clack/prompts';
2
- import { validate } from '../lib/schema.js';
3
- export function cmdValidate(filePath) {
4
- p.intro(`pmptwiki — validate: ${filePath}`);
5
- const result = validate(filePath);
6
- if (result.errors.length === 0 && result.warnings.length === 0) {
7
- p.outro('All validations passed');
8
- return true;
9
- }
10
- for (const err of result.errors) {
11
- p.log.error(err);
12
- }
13
- for (const warn of result.warnings) {
14
- p.log.warn(warn);
15
- }
16
- if (result.valid) {
17
- p.outro(`Validation passed (${result.warnings.length} warnings)`);
18
- }
19
- else {
20
- p.outro(`Validation failed (${result.errors.length} errors) — fix and retry`);
21
- }
22
- return result.valid;
23
- }
@@ -1,81 +0,0 @@
1
- import { Octokit } from '@octokit/rest';
2
- import { readFileSync } from 'fs';
3
- const CONTENT_OWNER = 'pmptwiki';
4
- const CONTENT_REPO = 'content';
5
- export function createClient(token) {
6
- return new Octokit({ auth: token });
7
- }
8
- export async function getAuthUser(octokit) {
9
- const { data } = await octokit.rest.users.getAuthenticated();
10
- return data.login;
11
- }
12
- /** Create fork if not exists, otherwise return existing */
13
- export async function ensureFork(octokit, username) {
14
- try {
15
- await octokit.rest.repos.get({ owner: username, repo: CONTENT_REPO });
16
- }
17
- catch {
18
- await octokit.rest.repos.createFork({
19
- owner: CONTENT_OWNER,
20
- repo: CONTENT_REPO,
21
- });
22
- // Fork creation is async - wait briefly
23
- await new Promise((r) => setTimeout(r, 3000));
24
- }
25
- }
26
- /** Create branch (based on upstream main) */
27
- export async function createBranch(octokit, username, branchName) {
28
- // Get sha of upstream main
29
- const { data: ref } = await octokit.rest.git.getRef({
30
- owner: CONTENT_OWNER,
31
- repo: CONTENT_REPO,
32
- ref: 'heads/main',
33
- });
34
- const sha = ref.object.sha;
35
- await octokit.rest.git.createRef({
36
- owner: username,
37
- repo: CONTENT_REPO,
38
- ref: `refs/heads/${branchName}`,
39
- sha,
40
- });
41
- }
42
- /** Commit file to fork branch */
43
- export async function pushFile(octokit, username, branchName, filePath, localFilePath, commitMessage) {
44
- const content = Buffer.from(readFileSync(localFilePath, 'utf-8')).toString('base64');
45
- // Check existing file sha (for update)
46
- let sha;
47
- try {
48
- const { data } = await octokit.rest.repos.getContent({
49
- owner: username,
50
- repo: CONTENT_REPO,
51
- path: filePath,
52
- ref: branchName,
53
- });
54
- if (!Array.isArray(data) && 'sha' in data)
55
- sha = data.sha;
56
- }
57
- catch {
58
- // New file
59
- }
60
- await octokit.rest.repos.createOrUpdateFileContents({
61
- owner: username,
62
- repo: CONTENT_REPO,
63
- path: filePath,
64
- message: commitMessage,
65
- content,
66
- branch: branchName,
67
- ...(sha ? { sha } : {}),
68
- });
69
- }
70
- /** Create PR to upstream */
71
- export async function createPR(octokit, username, branchName, title, body) {
72
- const { data } = await octokit.rest.pulls.create({
73
- owner: CONTENT_OWNER,
74
- repo: CONTENT_REPO,
75
- title,
76
- body,
77
- head: `${username}:${branchName}`,
78
- base: 'main',
79
- });
80
- return data.html_url;
81
- }
@@ -1,61 +0,0 @@
1
- import { z } from 'zod';
2
- import matter from 'gray-matter';
3
- import { readFileSync } from 'fs';
4
- const frontmatterSchema = z.object({
5
- title: z.string().min(5, 'Title must be at least 5 characters'),
6
- purpose: z.enum(['guide', 'rule', 'template', 'example', 'reference']),
7
- level: z.enum(['beginner', 'intermediate', 'advanced']),
8
- lang: z.enum(['ko', 'en']),
9
- persona: z.array(z.enum(['general', 'power-user', 'developer', 'organization'])).optional(),
10
- status: z.enum(['draft', 'review', 'stable', 'recommended', 'deprecated']).optional(),
11
- translationKey: z.string().optional(),
12
- tags: z.array(z.string()).optional(),
13
- created: z.string().optional(),
14
- updated: z.string().optional(),
15
- contributors: z.array(z.string()).optional(),
16
- });
17
- const FILE_PATH_RE = /^(ko|en)\/(guide|rule|template|example|reference)\/(beginner|intermediate|advanced)\/.+\.mdx?$/;
18
- export function validate(filePath) {
19
- const errors = [];
20
- const warnings = [];
21
- // 1. File path rules
22
- const relative = filePath.replace(/^.*?(?=ko\/|en\/)/, '');
23
- if (!FILE_PATH_RE.test(relative)) {
24
- errors.push(`File path does not match: {lang}/{purpose}/{level}/filename.md`);
25
- }
26
- // 2. Read file
27
- let raw;
28
- try {
29
- raw = readFileSync(filePath, 'utf-8');
30
- }
31
- catch {
32
- errors.push('Cannot read file');
33
- return { valid: false, errors, warnings };
34
- }
35
- // 3. Parse frontmatter
36
- const { data, content } = matter(raw);
37
- const result = frontmatterSchema.safeParse(data);
38
- if (!result.success) {
39
- for (const issue of result.error.issues) {
40
- const field = issue.path.join('.');
41
- errors.push(`[${field}] ${issue.message}`);
42
- }
43
- }
44
- // 4. Body length
45
- const bodyLength = content.trim().length;
46
- if (bodyLength < 200) {
47
- errors.push(`Content too short (${bodyLength} chars, minimum 200)`);
48
- }
49
- // 5. Warnings
50
- if (!data.tags || data.tags.length === 0) {
51
- warnings.push('Adding tags helps with search and related document links');
52
- }
53
- if (!data.persona) {
54
- warnings.push('Specifying a persona clarifies the target audience');
55
- }
56
- return {
57
- valid: errors.length === 0,
58
- errors,
59
- warnings,
60
- };
61
- }
@@ -1,37 +0,0 @@
1
- export function toSlug(title) {
2
- return title
3
- .toLowerCase()
4
- .replace(/[^\w\s-]/g, '')
5
- .trim()
6
- .replace(/\s+/g, '-')
7
- .replace(/-+/g, '-')
8
- .slice(0, 60);
9
- }
10
- export function today() {
11
- return new Date().toISOString().slice(0, 10);
12
- }
13
- export function generateFilePath(fm) {
14
- const slug = toSlug(fm.title);
15
- return `${fm.lang}/${fm.purpose}/${fm.level}/${slug}.md`;
16
- }
17
- export function generateContent(fm) {
18
- const frontmatter = [
19
- '---',
20
- `title: "${fm.title}"`,
21
- `purpose: ${fm.purpose}`,
22
- `level: ${fm.level}`,
23
- `lang: ${fm.lang}`,
24
- fm.persona?.length ? `persona: [${fm.persona.map((p) => `"${p}"`).join(', ')}]` : null,
25
- `status: draft`,
26
- fm.tags?.length ? `tags: [${fm.tags.map((t) => `"${t}"`).join(', ')}]` : null,
27
- `created: "${today()}"`,
28
- `updated: "${today()}"`,
29
- '---',
30
- ]
31
- .filter(Boolean)
32
- .join('\n');
33
- const body = fm.lang === 'ko'
34
- ? `\n## Why It Matters\n\n<!-- Explain why this document is needed -->\n\n## How To\n\n<!-- Explain step by step -->\n\n## Examples\n\n<!-- Add real examples -->\n`
35
- : `\n## Why it matters\n\n<!-- Explain why this document is needed -->\n\n## How to\n\n<!-- Explain step by step -->\n\n## Example\n\n<!-- Add a real example -->\n`;
36
- return frontmatter + body;
37
- }