pmpt-cli 1.9.1 → 1.11.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.
- package/dist/commands/internal-seed.js +104 -0
- package/dist/commands/plan.js +62 -10
- package/dist/commands/publish.js +190 -62
- package/dist/index.js +16 -0
- package/package.json +1 -1
|
@@ -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
|
+
}
|
package/dist/commands/plan.js
CHANGED
|
@@ -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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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.');
|
package/dist/commands/publish.js
CHANGED
|
@@ -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
|
-
|
|
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,135 @@ 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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
147
|
+
let slug;
|
|
148
|
+
let description;
|
|
149
|
+
let tags;
|
|
150
|
+
let category;
|
|
151
|
+
let productUrl = '';
|
|
152
|
+
let productUrlType = '';
|
|
153
|
+
if (options?.nonInteractive) {
|
|
154
|
+
let metaFromFile = {};
|
|
155
|
+
if (options.metaFile) {
|
|
156
|
+
try {
|
|
157
|
+
metaFromFile = loadMetaFile(projectPath, options.metaFile);
|
|
112
158
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
p.
|
|
157
|
-
|
|
159
|
+
catch (err) {
|
|
160
|
+
p.log.error(err instanceof Error ? err.message : 'Failed to load meta file.');
|
|
161
|
+
process.exit(1);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
slug = String(options.slug
|
|
165
|
+
?? metaFromFile.slug
|
|
166
|
+
?? savedSlug
|
|
167
|
+
?? defaultSlug).trim();
|
|
168
|
+
description = String(options.description
|
|
169
|
+
?? metaFromFile.description
|
|
170
|
+
?? existing?.description
|
|
171
|
+
?? planProgress?.answers?.productIdea?.slice(0, 200)
|
|
172
|
+
?? '').trim();
|
|
173
|
+
tags = normalizeTags(options.tags ?? metaFromFile.tags ?? existing?.tags ?? []);
|
|
174
|
+
category = String(options.category ?? metaFromFile.category ?? existing?.category ?? 'other').trim();
|
|
175
|
+
productUrl = String(options.productUrl ?? metaFromFile.productUrl ?? existing?.productUrl ?? '').trim();
|
|
176
|
+
productUrlType = String(options.productUrlType ?? metaFromFile.productUrlType ?? existing?.productUrlType ?? '').trim();
|
|
177
|
+
if (!/^[a-z0-9][a-z0-9-]{1,48}[a-z0-9]$/.test(slug)) {
|
|
178
|
+
p.log.error('Invalid slug. Use 3-50 chars, lowercase letters, numbers, and hyphens only.');
|
|
179
|
+
process.exit(1);
|
|
180
|
+
}
|
|
181
|
+
if (!description) {
|
|
182
|
+
p.log.error('Description is required in non-interactive mode.');
|
|
183
|
+
process.exit(1);
|
|
184
|
+
}
|
|
185
|
+
if (!VALID_CATEGORIES.has(category)) {
|
|
186
|
+
p.log.error(`Invalid category: ${category}`);
|
|
187
|
+
p.log.info(`Allowed: ${[...VALID_CATEGORIES].join(', ')}`);
|
|
188
|
+
process.exit(1);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
const slugInput = await p.text({
|
|
193
|
+
message: 'Project slug (used in URL):',
|
|
194
|
+
placeholder: defaultSlug,
|
|
195
|
+
defaultValue: savedSlug || '',
|
|
196
|
+
validate: (v) => {
|
|
197
|
+
if (!/^[a-z0-9][a-z0-9-]{1,48}[a-z0-9]$/.test(v)) {
|
|
198
|
+
return '3-50 chars, lowercase letters, numbers, and hyphens only.';
|
|
199
|
+
}
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
if (p.isCancel(slugInput)) {
|
|
203
|
+
p.cancel('Cancelled');
|
|
204
|
+
process.exit(0);
|
|
205
|
+
}
|
|
206
|
+
slug = slugInput;
|
|
207
|
+
const descriptionInput = await p.text({
|
|
208
|
+
message: 'Project description (brief):',
|
|
209
|
+
placeholder: existing?.description || planProgress?.answers?.productIdea?.slice(0, 100) || '',
|
|
210
|
+
defaultValue: existing?.description || planProgress?.answers?.productIdea?.slice(0, 200) || '',
|
|
211
|
+
});
|
|
212
|
+
if (p.isCancel(descriptionInput)) {
|
|
213
|
+
p.cancel('Cancelled');
|
|
214
|
+
process.exit(0);
|
|
215
|
+
}
|
|
216
|
+
description = descriptionInput;
|
|
217
|
+
const tagsInput = await p.text({
|
|
218
|
+
message: 'Tags (comma-separated):',
|
|
219
|
+
placeholder: 'react, saas, mvp',
|
|
220
|
+
defaultValue: existing?.tags?.join(', ') || '',
|
|
221
|
+
});
|
|
222
|
+
if (p.isCancel(tagsInput)) {
|
|
223
|
+
p.cancel('Cancelled');
|
|
224
|
+
process.exit(0);
|
|
225
|
+
}
|
|
226
|
+
tags = normalizeTags(tagsInput);
|
|
227
|
+
const categoryInput = await p.select({
|
|
228
|
+
message: 'Project category:',
|
|
229
|
+
initialValue: existing?.category || 'other',
|
|
230
|
+
options: CATEGORY_OPTIONS,
|
|
231
|
+
});
|
|
232
|
+
if (p.isCancel(categoryInput)) {
|
|
233
|
+
p.cancel('Cancelled');
|
|
234
|
+
process.exit(0);
|
|
235
|
+
}
|
|
236
|
+
category = categoryInput;
|
|
237
|
+
// Product link (optional)
|
|
238
|
+
const linkTypeInput = await p.select({
|
|
239
|
+
message: 'Product link (optional):',
|
|
240
|
+
initialValue: existing?.productUrlType || 'none',
|
|
241
|
+
options: [
|
|
242
|
+
{ value: 'none', label: 'No link' },
|
|
243
|
+
{ value: 'git', label: 'Git Repository' },
|
|
244
|
+
{ value: 'url', label: 'Website / URL' },
|
|
245
|
+
],
|
|
246
|
+
});
|
|
247
|
+
if (p.isCancel(linkTypeInput)) {
|
|
248
|
+
p.cancel('Cancelled');
|
|
249
|
+
process.exit(0);
|
|
250
|
+
}
|
|
251
|
+
if (linkTypeInput !== 'none') {
|
|
252
|
+
productUrlType = linkTypeInput;
|
|
253
|
+
const productUrlInput = await p.text({
|
|
254
|
+
message: 'Product URL:',
|
|
255
|
+
placeholder: linkTypeInput === 'git'
|
|
256
|
+
? `https://github.com/${auth.username}/${slug}`
|
|
257
|
+
: 'https://...',
|
|
258
|
+
defaultValue: existing?.productUrl || '',
|
|
259
|
+
validate: (v) => {
|
|
260
|
+
if (!v.trim())
|
|
261
|
+
return 'URL is required when link type is selected.';
|
|
262
|
+
try {
|
|
263
|
+
new URL(v);
|
|
264
|
+
}
|
|
265
|
+
catch {
|
|
266
|
+
return 'Invalid URL format.';
|
|
267
|
+
}
|
|
268
|
+
},
|
|
269
|
+
});
|
|
270
|
+
if (p.isCancel(productUrlInput)) {
|
|
271
|
+
p.cancel('Cancelled');
|
|
272
|
+
process.exit(0);
|
|
273
|
+
}
|
|
274
|
+
productUrl = productUrlInput;
|
|
275
|
+
}
|
|
158
276
|
}
|
|
159
277
|
// Build .pmpt content (resolve from optimized snapshots)
|
|
160
278
|
const history = snapshots.map((snapshot, i) => ({
|
|
@@ -190,14 +308,23 @@ export async function cmdPublish(path, options) {
|
|
|
190
308
|
`Author: @${auth.username}`,
|
|
191
309
|
`Category: ${category}`,
|
|
192
310
|
tags.length ? `Tags: ${tags.join(', ')}` : '',
|
|
311
|
+
productUrl ? `Product: ${productUrl} (${productUrlType})` : '',
|
|
193
312
|
].filter(Boolean).join('\n'), 'Publish Preview');
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
313
|
+
if (options?.nonInteractive) {
|
|
314
|
+
if (!options.yes) {
|
|
315
|
+
p.log.error('Non-interactive mode requires --yes to confirm publish.');
|
|
316
|
+
process.exit(1);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
else if (!options?.yes) {
|
|
320
|
+
const confirm = await p.confirm({
|
|
321
|
+
message: 'Publish this project?',
|
|
322
|
+
initialValue: true,
|
|
323
|
+
});
|
|
324
|
+
if (p.isCancel(confirm) || !confirm) {
|
|
325
|
+
p.cancel('Cancelled');
|
|
326
|
+
process.exit(0);
|
|
327
|
+
}
|
|
201
328
|
}
|
|
202
329
|
// Upload
|
|
203
330
|
const s = p.spinner();
|
|
@@ -206,9 +333,10 @@ export async function cmdPublish(path, options) {
|
|
|
206
333
|
const result = await publishProject(auth.token, {
|
|
207
334
|
slug: slug,
|
|
208
335
|
pmptContent,
|
|
209
|
-
description
|
|
336
|
+
description,
|
|
210
337
|
tags,
|
|
211
|
-
category
|
|
338
|
+
category,
|
|
339
|
+
...(productUrl && { productUrl, productUrlType }),
|
|
212
340
|
});
|
|
213
341
|
s.stop('Published!');
|
|
214
342
|
// 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,15 @@ 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('--product-url <url>', 'Product link URL')
|
|
144
|
+
.option('--product-url-type <type>', 'Product link type: git or url')
|
|
145
|
+
.option('--yes', 'Skip confirmation prompt')
|
|
135
146
|
.action(cmdPublish);
|
|
136
147
|
program
|
|
137
148
|
.command('edit')
|
|
@@ -153,4 +164,9 @@ program
|
|
|
153
164
|
.command('recover [path]')
|
|
154
165
|
.description('Generate a recovery prompt to regenerate pmpt.md via AI')
|
|
155
166
|
.action(cmdRecover);
|
|
167
|
+
// Internal automation command (hidden from help)
|
|
168
|
+
program
|
|
169
|
+
.command('internal-seed', { hidden: true })
|
|
170
|
+
.requiredOption('--spec <file>', 'Seed spec JSON file')
|
|
171
|
+
.action(cmdInternalSeed);
|
|
156
172
|
program.parse();
|