pmpt-cli 1.9.0 → 1.10.0

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.
@@ -74,16 +74,23 @@ function printSummary(diffs) {
74
74
  p.log.info(`${pc.green(`+${additions}`)} additions, ${pc.red(`-${deletions}`)} deletions`);
75
75
  }
76
76
  export function cmdDiff(v1, v2, pathOrOptions, maybeOptions) {
77
- // Commander passes args differently depending on how many positionals are given.
78
- // pmpt diff v1 --file x → (v1, options)
79
- // pmpt diff v1 v2 → (v1, v2, options)
80
- // pmpt diff v1 v2 /path → (v1, v2, path, options)
77
+ // Commander passes args in order: pmpt diff v1 [v2] [path] [options]
78
+ // Smart parsing: if v2 looks like a path (not a version pattern), treat it as path.
81
79
  let v2Str;
82
80
  let path;
83
81
  let options = {};
82
+ const isVersion = (s) => /^v?\d+$/.test(s);
84
83
  if (typeof v2 === 'object') {
84
+ // pmpt diff v1 --file x → (v1, options)
85
85
  options = v2;
86
86
  }
87
+ else if (v2 !== undefined && !isVersion(v2)) {
88
+ // pmpt diff v1 /some/path → v2 is actually a path
89
+ path = v2;
90
+ if (typeof pathOrOptions === 'object') {
91
+ options = pathOrOptions;
92
+ }
93
+ }
87
94
  else {
88
95
  v2Str = v2;
89
96
  if (typeof pathOrOptions === 'object') {
@@ -0,0 +1,104 @@
1
+ import * as p from '@clack/prompts';
2
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync } from 'fs';
3
+ import { dirname, join, resolve } from 'path';
4
+ import { getDocsDir, initializeProject, isInitialized } from '../lib/config.js';
5
+ import { createFullSnapshot } from '../lib/history.js';
6
+ import { cmdPlan } from './plan.js';
7
+ import { cmdPublish } from './publish.js';
8
+ function assertInternalEnabled() {
9
+ if (process.env.PMPT_INTERNAL !== '1') {
10
+ p.log.error('internal-seed is disabled.');
11
+ p.log.info('Set PMPT_INTERNAL=1 to enable internal automation commands.');
12
+ process.exit(1);
13
+ }
14
+ }
15
+ function readJsonFile(filePath) {
16
+ if (!existsSync(filePath)) {
17
+ throw new Error(`File not found: ${filePath}`);
18
+ }
19
+ try {
20
+ return JSON.parse(readFileSync(filePath, 'utf-8'));
21
+ }
22
+ catch {
23
+ throw new Error(`Invalid JSON: ${filePath}`);
24
+ }
25
+ }
26
+ function writeDocFile(docsDir, fileName, content) {
27
+ const outPath = resolve(docsDir, fileName);
28
+ const docsRoot = resolve(docsDir);
29
+ if (!outPath.startsWith(docsRoot + '/') && outPath !== docsRoot) {
30
+ throw new Error(`Unsafe docs path: ${fileName}`);
31
+ }
32
+ mkdirSync(dirname(outPath), { recursive: true });
33
+ writeFileSync(outPath, content, 'utf-8');
34
+ }
35
+ function normalizeTags(tags) {
36
+ if (!tags)
37
+ return undefined;
38
+ if (Array.isArray(tags))
39
+ return tags.join(',');
40
+ return tags;
41
+ }
42
+ export async function cmdInternalSeed(options) {
43
+ assertInternalEnabled();
44
+ if (!options?.spec) {
45
+ p.log.error('Missing required option: --spec <file>');
46
+ process.exit(1);
47
+ }
48
+ const specPath = resolve(options.spec);
49
+ const specDir = dirname(specPath);
50
+ const spec = readJsonFile(specPath);
51
+ const projectPath = spec.projectPath ? resolve(spec.projectPath) : process.cwd();
52
+ p.intro('pmpt internal-seed');
53
+ if (!isInitialized(projectPath)) {
54
+ initializeProject(projectPath, { trackGit: true });
55
+ p.log.info(`Initialized: ${projectPath}`);
56
+ }
57
+ let answersFileForPlan;
58
+ if (spec.answers) {
59
+ const tempAnswersPath = join(projectPath, '.pmpt', '.internal-seed-answers.json');
60
+ mkdirSync(dirname(tempAnswersPath), { recursive: true });
61
+ writeFileSync(tempAnswersPath, JSON.stringify(spec.answers, null, 2), 'utf-8');
62
+ answersFileForPlan = tempAnswersPath;
63
+ }
64
+ else if (spec.answersFile) {
65
+ answersFileForPlan = resolve(specDir, spec.answersFile);
66
+ }
67
+ if (answersFileForPlan) {
68
+ await cmdPlan(projectPath, {
69
+ reset: spec.resetPlan ?? true,
70
+ answersFile: answersFileForPlan,
71
+ });
72
+ // Clean up temp answers file
73
+ const tempAnswersPath = join(projectPath, '.pmpt', '.internal-seed-answers.json');
74
+ if (spec.answers && existsSync(tempAnswersPath)) {
75
+ unlinkSync(tempAnswersPath);
76
+ }
77
+ }
78
+ const docsDir = getDocsDir(projectPath);
79
+ for (const step of spec.versions ?? []) {
80
+ for (const [fileName, content] of Object.entries(step.files ?? {})) {
81
+ writeDocFile(docsDir, fileName, content);
82
+ }
83
+ for (const [fileName, fromPath] of Object.entries(step.filesFrom ?? {})) {
84
+ const content = readFileSync(resolve(specDir, fromPath), 'utf-8');
85
+ writeDocFile(docsDir, fileName, content);
86
+ }
87
+ const entry = createFullSnapshot(projectPath);
88
+ const note = step.saveNote ? ` — ${step.saveNote}` : '';
89
+ p.log.success(`v${entry.version} saved${note}`);
90
+ }
91
+ if (spec.publish?.enabled) {
92
+ await cmdPublish(projectPath, {
93
+ force: spec.publish.force ?? false,
94
+ nonInteractive: true,
95
+ yes: spec.publish.yes ?? true,
96
+ metaFile: spec.publish.metaFile ? resolve(specDir, spec.publish.metaFile) : undefined,
97
+ slug: spec.publish.slug,
98
+ description: spec.publish.description,
99
+ tags: normalizeTags(spec.publish.tags),
100
+ category: spec.publish.category,
101
+ });
102
+ }
103
+ p.outro('internal-seed completed');
104
+ }
@@ -1,10 +1,30 @@
1
1
  import * as p from '@clack/prompts';
2
2
  import { resolve } from 'path';
3
- import { readFileSync } from 'fs';
3
+ import { existsSync, readFileSync } from 'fs';
4
4
  import { isInitialized } from '../lib/config.js';
5
5
  import { copyToClipboard } from '../lib/clipboard.js';
6
6
  import { cmdWatch } from './watch.js';
7
7
  import { PLAN_QUESTIONS, getPlanProgress, initPlanProgress, savePlanProgress, savePlanDocuments, } from '../lib/plan.js';
8
+ function loadAnswersFromFile(projectPath, inputPath) {
9
+ const filePath = resolve(projectPath, inputPath);
10
+ if (!existsSync(filePath)) {
11
+ throw new Error(`Answers file not found: ${filePath}`);
12
+ }
13
+ const raw = JSON.parse(readFileSync(filePath, 'utf-8'));
14
+ const requiredKeys = ['projectName', 'productIdea', 'coreFeatures'];
15
+ for (const key of requiredKeys) {
16
+ if (!raw[key] || String(raw[key]).trim().length === 0) {
17
+ throw new Error(`Missing required field in answers file: ${key}`);
18
+ }
19
+ }
20
+ return {
21
+ projectName: String(raw.projectName ?? '').trim(),
22
+ productIdea: String(raw.productIdea ?? '').trim(),
23
+ additionalContext: String(raw.additionalContext ?? '').trim(),
24
+ coreFeatures: String(raw.coreFeatures ?? '').trim(),
25
+ techStack: String(raw.techStack ?? '').trim(),
26
+ };
27
+ }
8
28
  export async function cmdPlan(path, options) {
9
29
  const projectPath = path ? resolve(path) : process.cwd();
10
30
  // Check initialization
@@ -17,20 +37,25 @@ export async function cmdPlan(path, options) {
17
37
  }
18
38
  // Reset option
19
39
  if (options?.reset) {
20
- const confirm = await p.confirm({
21
- message: 'Restart plan from scratch?',
22
- initialValue: false,
23
- });
24
- if (p.isCancel(confirm) || !confirm) {
25
- p.cancel('Cancelled');
26
- process.exit(0);
40
+ if (options.answersFile) {
41
+ initPlanProgress(projectPath);
42
+ }
43
+ else {
44
+ const confirm = await p.confirm({
45
+ message: 'Restart plan from scratch?',
46
+ initialValue: false,
47
+ });
48
+ if (p.isCancel(confirm) || !confirm) {
49
+ p.cancel('Cancelled');
50
+ process.exit(0);
51
+ }
52
+ initPlanProgress(projectPath);
27
53
  }
28
- initPlanProgress(projectPath);
29
54
  }
30
55
  // Check progress
31
56
  let progress = getPlanProgress(projectPath);
32
57
  // If already completed
33
- if (progress?.completed) {
58
+ if (progress?.completed && !options?.answersFile) {
34
59
  p.intro('pmpt plan');
35
60
  p.log.success('Plan already completed.');
36
61
  const action = await p.select({
@@ -122,6 +147,33 @@ export async function cmdPlan(path, options) {
122
147
  if (!progress) {
123
148
  progress = initPlanProgress(projectPath);
124
149
  }
150
+ // Non-interactive mode for agents/automation
151
+ if (options?.answersFile) {
152
+ p.log.info('Plan: non-interactive mode');
153
+ let answers;
154
+ try {
155
+ answers = loadAnswersFromFile(projectPath, options.answersFile);
156
+ }
157
+ catch (err) {
158
+ p.log.error(err instanceof Error ? err.message : 'Invalid answers file.');
159
+ p.outro('');
160
+ process.exit(1);
161
+ }
162
+ const s = p.spinner();
163
+ s.start('Generating documents from answers file...');
164
+ const { planPath, promptPath } = savePlanDocuments(projectPath, answers);
165
+ progress.completed = true;
166
+ progress.answers = answers;
167
+ savePlanProgress(projectPath, progress);
168
+ s.stop('Done!');
169
+ const pmptMdPath = promptPath.replace('pmpt.ai.md', 'pmpt.md');
170
+ p.note([
171
+ `plan.md: ${planPath}`,
172
+ `pmpt.md: ${pmptMdPath}`,
173
+ `pmpt.ai.md: ${promptPath}`,
174
+ ].join('\n'), 'Generated');
175
+ return;
176
+ }
125
177
  p.intro('pmpt plan — Your Product Journey Starts Here!');
126
178
  p.log.info(`Answer ${PLAN_QUESTIONS.length} quick questions to generate your AI prompt.`);
127
179
  p.log.message('You can answer in any language you prefer.');
@@ -11,6 +11,43 @@ import { computeQuality } from '../lib/quality.js';
11
11
  import pc from 'picocolors';
12
12
  import glob from 'fast-glob';
13
13
  import { join } from 'path';
14
+ const CATEGORY_OPTIONS = [
15
+ { value: 'web-app', label: 'Web App' },
16
+ { value: 'mobile-app', label: 'Mobile App' },
17
+ { value: 'cli-tool', label: 'CLI Tool' },
18
+ { value: 'api-backend', label: 'API/Backend' },
19
+ { value: 'ai-ml', label: 'AI/ML' },
20
+ { value: 'game', label: 'Game' },
21
+ { value: 'library', label: 'Library' },
22
+ { value: 'other', label: 'Other' },
23
+ ];
24
+ const VALID_CATEGORIES = new Set(CATEGORY_OPTIONS.map((o) => o.value));
25
+ function normalizeTags(value) {
26
+ if (Array.isArray(value)) {
27
+ return value
28
+ .map((v) => String(v).trim().toLowerCase())
29
+ .filter(Boolean);
30
+ }
31
+ if (typeof value === 'string') {
32
+ return value
33
+ .split(',')
34
+ .map((t) => t.trim().toLowerCase())
35
+ .filter(Boolean);
36
+ }
37
+ return [];
38
+ }
39
+ function loadMetaFile(projectPath, filePath) {
40
+ const resolved = resolve(projectPath, filePath);
41
+ if (!existsSync(resolved)) {
42
+ throw new Error(`Meta file not found: ${resolved}`);
43
+ }
44
+ try {
45
+ return JSON.parse(readFileSync(resolved, 'utf-8'));
46
+ }
47
+ catch {
48
+ throw new Error(`Invalid JSON in meta file: ${resolved}`);
49
+ }
50
+ }
14
51
  function readDocsFolder(docsDir) {
15
52
  const files = {};
16
53
  if (!existsSync(docsDir))
@@ -35,7 +72,12 @@ export async function cmdPublish(path, options) {
35
72
  p.log.error('Login required. Run `pmpt login` first.');
36
73
  process.exit(1);
37
74
  }
38
- p.intro('pmpt publish');
75
+ if (options?.nonInteractive) {
76
+ p.log.info('Publish: non-interactive mode');
77
+ }
78
+ else {
79
+ p.intro('pmpt publish');
80
+ }
39
81
  const config = loadConfig(projectPath);
40
82
  const snapshots = getAllSnapshots(projectPath);
41
83
  const planProgress = getPlanProgress(projectPath);
@@ -102,59 +144,92 @@ export async function cmdPublish(path, options) {
102
144
  const defaultSlug = savedSlug
103
145
  || projectName.toLowerCase().replace(/[^a-z0-9-]/g, '-').replace(/-+/g, '-');
104
146
  // Collect publish info
105
- const slug = await p.text({
106
- message: 'Project slug (used in URL):',
107
- placeholder: defaultSlug,
108
- defaultValue: savedSlug || '',
109
- validate: (v) => {
110
- if (!/^[a-z0-9][a-z0-9-]{1,48}[a-z0-9]$/.test(v)) {
111
- return '3-50 chars, lowercase letters, numbers, and hyphens only.';
147
+ let slug;
148
+ let description;
149
+ let tags;
150
+ let category;
151
+ if (options?.nonInteractive) {
152
+ let metaFromFile = {};
153
+ if (options.metaFile) {
154
+ try {
155
+ metaFromFile = loadMetaFile(projectPath, options.metaFile);
112
156
  }
113
- },
114
- });
115
- if (p.isCancel(slug)) {
116
- p.cancel('Cancelled');
117
- process.exit(0);
118
- }
119
- const description = await p.text({
120
- message: 'Project description (brief):',
121
- placeholder: existing?.description || planProgress?.answers?.productIdea?.slice(0, 100) || '',
122
- defaultValue: existing?.description || planProgress?.answers?.productIdea?.slice(0, 200) || '',
123
- });
124
- if (p.isCancel(description)) {
125
- p.cancel('Cancelled');
126
- process.exit(0);
127
- }
128
- const tagsInput = await p.text({
129
- message: 'Tags (comma-separated):',
130
- placeholder: 'react, saas, mvp',
131
- defaultValue: existing?.tags?.join(', ') || '',
132
- });
133
- if (p.isCancel(tagsInput)) {
134
- p.cancel('Cancelled');
135
- process.exit(0);
136
- }
137
- const tags = tagsInput
138
- .split(',')
139
- .map((t) => t.trim().toLowerCase())
140
- .filter(Boolean);
141
- const category = await p.select({
142
- message: 'Project category:',
143
- initialValue: existing?.category || 'other',
144
- options: [
145
- { value: 'web-app', label: 'Web App' },
146
- { value: 'mobile-app', label: 'Mobile App' },
147
- { value: 'cli-tool', label: 'CLI Tool' },
148
- { value: 'api-backend', label: 'API/Backend' },
149
- { value: 'ai-ml', label: 'AI/ML' },
150
- { value: 'game', label: 'Game' },
151
- { value: 'library', label: 'Library' },
152
- { value: 'other', label: 'Other' },
153
- ],
154
- });
155
- if (p.isCancel(category)) {
156
- p.cancel('Cancelled');
157
- process.exit(0);
157
+ catch (err) {
158
+ p.log.error(err instanceof Error ? err.message : 'Failed to load meta file.');
159
+ process.exit(1);
160
+ }
161
+ }
162
+ slug = String(options.slug
163
+ ?? metaFromFile.slug
164
+ ?? savedSlug
165
+ ?? defaultSlug).trim();
166
+ description = String(options.description
167
+ ?? metaFromFile.description
168
+ ?? existing?.description
169
+ ?? planProgress?.answers?.productIdea?.slice(0, 200)
170
+ ?? '').trim();
171
+ tags = normalizeTags(options.tags ?? metaFromFile.tags ?? existing?.tags ?? []);
172
+ category = String(options.category ?? metaFromFile.category ?? existing?.category ?? 'other').trim();
173
+ if (!/^[a-z0-9][a-z0-9-]{1,48}[a-z0-9]$/.test(slug)) {
174
+ p.log.error('Invalid slug. Use 3-50 chars, lowercase letters, numbers, and hyphens only.');
175
+ process.exit(1);
176
+ }
177
+ if (!description) {
178
+ p.log.error('Description is required in non-interactive mode.');
179
+ process.exit(1);
180
+ }
181
+ if (!VALID_CATEGORIES.has(category)) {
182
+ p.log.error(`Invalid category: ${category}`);
183
+ p.log.info(`Allowed: ${[...VALID_CATEGORIES].join(', ')}`);
184
+ process.exit(1);
185
+ }
186
+ }
187
+ else {
188
+ const slugInput = await p.text({
189
+ message: 'Project slug (used in URL):',
190
+ placeholder: defaultSlug,
191
+ defaultValue: savedSlug || '',
192
+ validate: (v) => {
193
+ if (!/^[a-z0-9][a-z0-9-]{1,48}[a-z0-9]$/.test(v)) {
194
+ return '3-50 chars, lowercase letters, numbers, and hyphens only.';
195
+ }
196
+ },
197
+ });
198
+ if (p.isCancel(slugInput)) {
199
+ p.cancel('Cancelled');
200
+ process.exit(0);
201
+ }
202
+ slug = slugInput;
203
+ const descriptionInput = await p.text({
204
+ message: 'Project description (brief):',
205
+ placeholder: existing?.description || planProgress?.answers?.productIdea?.slice(0, 100) || '',
206
+ defaultValue: existing?.description || planProgress?.answers?.productIdea?.slice(0, 200) || '',
207
+ });
208
+ if (p.isCancel(descriptionInput)) {
209
+ p.cancel('Cancelled');
210
+ process.exit(0);
211
+ }
212
+ description = descriptionInput;
213
+ const tagsInput = await p.text({
214
+ message: 'Tags (comma-separated):',
215
+ placeholder: 'react, saas, mvp',
216
+ defaultValue: existing?.tags?.join(', ') || '',
217
+ });
218
+ if (p.isCancel(tagsInput)) {
219
+ p.cancel('Cancelled');
220
+ process.exit(0);
221
+ }
222
+ tags = normalizeTags(tagsInput);
223
+ const categoryInput = await p.select({
224
+ message: 'Project category:',
225
+ initialValue: existing?.category || 'other',
226
+ options: CATEGORY_OPTIONS,
227
+ });
228
+ if (p.isCancel(categoryInput)) {
229
+ p.cancel('Cancelled');
230
+ process.exit(0);
231
+ }
232
+ category = categoryInput;
158
233
  }
159
234
  // Build .pmpt content (resolve from optimized snapshots)
160
235
  const history = snapshots.map((snapshot, i) => ({
@@ -191,13 +266,21 @@ export async function cmdPublish(path, options) {
191
266
  `Category: ${category}`,
192
267
  tags.length ? `Tags: ${tags.join(', ')}` : '',
193
268
  ].filter(Boolean).join('\n'), 'Publish Preview');
194
- const confirm = await p.confirm({
195
- message: 'Publish this project?',
196
- initialValue: true,
197
- });
198
- if (p.isCancel(confirm) || !confirm) {
199
- p.cancel('Cancelled');
200
- process.exit(0);
269
+ if (options?.nonInteractive) {
270
+ if (!options.yes) {
271
+ p.log.error('Non-interactive mode requires --yes to confirm publish.');
272
+ process.exit(1);
273
+ }
274
+ }
275
+ else if (!options?.yes) {
276
+ const confirm = await p.confirm({
277
+ message: 'Publish this project?',
278
+ initialValue: true,
279
+ });
280
+ if (p.isCancel(confirm) || !confirm) {
281
+ p.cancel('Cancelled');
282
+ process.exit(0);
283
+ }
201
284
  }
202
285
  // Upload
203
286
  const s = p.spinner();
@@ -206,9 +289,9 @@ export async function cmdPublish(path, options) {
206
289
  const result = await publishProject(auth.token, {
207
290
  slug: slug,
208
291
  pmptContent,
209
- description: description,
292
+ description,
210
293
  tags,
211
- category: category,
294
+ category,
212
295
  });
213
296
  s.stop('Published!');
214
297
  // Update config
package/dist/index.js CHANGED
@@ -40,6 +40,7 @@ import { cmdClone } from './commands/clone.js';
40
40
  import { cmdBrowse } from './commands/browse.js';
41
41
  import { cmdRecover } from './commands/recover.js';
42
42
  import { cmdDiff } from './commands/diff.js';
43
+ import { cmdInternalSeed } from './commands/internal-seed.js';
43
44
  import { createRequire } from 'module';
44
45
  const require = createRequire(import.meta.url);
45
46
  const { version } = require('../package.json');
@@ -114,6 +115,7 @@ program
114
115
  .command('plan [path]')
115
116
  .description('Quick product planning with 5 questions — auto-generate AI prompt')
116
117
  .option('--reset', 'Restart plan from scratch')
118
+ .option('--answers-file <file>', 'Load plan answers from JSON file (non-interactive)')
117
119
  .action(cmdPlan);
118
120
  program
119
121
  .command('logout')
@@ -132,6 +134,13 @@ program
132
134
  .command('publish [path]')
133
135
  .description('Publish project to pmptwiki platform')
134
136
  .option('--force', 'Publish even if quality score is below minimum')
137
+ .option('--non-interactive', 'Run without interactive prompts')
138
+ .option('--meta-file <file>', 'JSON file with slug, description, tags, category')
139
+ .option('--slug <slug>', 'Project slug')
140
+ .option('--description <text>', 'Project description')
141
+ .option('--tags <csv>', 'Comma-separated tags')
142
+ .option('--category <id>', 'Project category')
143
+ .option('--yes', 'Skip confirmation prompt')
135
144
  .action(cmdPublish);
136
145
  program
137
146
  .command('edit')
@@ -153,4 +162,9 @@ program
153
162
  .command('recover [path]')
154
163
  .description('Generate a recovery prompt to regenerate pmpt.md via AI')
155
164
  .action(cmdRecover);
165
+ // Internal automation command (hidden from help)
166
+ program
167
+ .command('internal-seed', { hidden: true })
168
+ .requiredOption('--spec <file>', 'Seed spec JSON file')
169
+ .action(cmdInternalSeed);
156
170
  program.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pmpt-cli",
3
- "version": "1.9.0",
3
+ "version": "1.10.0",
4
4
  "description": "Record and share your AI-driven product development journey",
5
5
  "type": "module",
6
6
  "bin": {