@postplus/cli 0.1.12 → 0.1.14
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 +7 -9
- package/build/auth-login.js +5 -3
- package/build/command-runner.js +60 -0
- package/build/index.js +20 -3
- package/build/skill-management.js +155 -0
- package/build/status.js +21 -4
- package/build/update-check.js +229 -0
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ Instead of asking you to choose tools first, PostPlus starts from the job you wa
|
|
|
11
11
|
```text
|
|
12
12
|
"Find creators for this product."
|
|
13
13
|
"Analyze why this competitor video works."
|
|
14
|
-
"Tell me whether this product has
|
|
14
|
+
"Tell me whether this product has marketplace potential."
|
|
15
15
|
"Turn these references into a short-form video brief."
|
|
16
16
|
"Package the research into a client-ready Feishu report."
|
|
17
17
|
```
|
|
@@ -23,7 +23,7 @@ The agent then routes the work, collects evidence, makes the judgment explicit,
|
|
|
23
23
|
PostPlus has three public surfaces that work together:
|
|
24
24
|
|
|
25
25
|
- `https://postplus.io/`: the hosted product surface for account access, subscription state, and cloud-backed capabilities.
|
|
26
|
-
- `https://github.com/
|
|
26
|
+
- `https://github.com/PostPlusAI/postplus-skills`: the public skill repository that installs local marketing workflows into agent tools.
|
|
27
27
|
- `https://github.com/PostPlusAI/postplus-cli`: the local command-line tool that signs you in, checks local readiness, and connects released skills to PostPlus account state.
|
|
28
28
|
|
|
29
29
|
## Install
|
|
@@ -39,8 +39,7 @@ npx -y skills add PostPlusAI/postplus-skills --full-depth --skill '*' --agent cl
|
|
|
39
39
|
Useful checks:
|
|
40
40
|
|
|
41
41
|
```bash
|
|
42
|
-
postplus
|
|
43
|
-
postplus doctor
|
|
42
|
+
postplus status
|
|
44
43
|
npx -y skills add PostPlusAI/postplus-skills --list --full-depth
|
|
45
44
|
```
|
|
46
45
|
|
|
@@ -163,7 +162,7 @@ Use PostPlus when you need to decide whether a product, category, or channel is
|
|
|
163
162
|
Example requests:
|
|
164
163
|
|
|
165
164
|
```text
|
|
166
|
-
"Does this product fit
|
|
165
|
+
"Does this product fit Amazon or a content-led launch better?"
|
|
167
166
|
"Find 1688 suppliers and compare them against demand signals."
|
|
168
167
|
"Analyze whether this category has enough content proof to test."
|
|
169
168
|
```
|
|
@@ -245,7 +244,7 @@ Typical outputs:
|
|
|
245
244
|
|
|
246
245
|
```text
|
|
247
246
|
Product idea
|
|
248
|
-
-> collect TikTok, Amazon,
|
|
247
|
+
-> collect TikTok, Amazon, Xiaohongshu, Google Trends, or 1688 evidence
|
|
249
248
|
-> compare demand, content fit, supply, price, and risk
|
|
250
249
|
-> produce a go / no-go / test-first recommendation
|
|
251
250
|
```
|
|
@@ -306,7 +305,7 @@ Examples: social media routing, creator discovery routing, media routing, patter
|
|
|
306
305
|
|
|
307
306
|
For collecting and analyzing public signals from platforms, marketplaces, search behavior, and social content.
|
|
308
307
|
|
|
309
|
-
Examples: TikTok, TikTok ads, Instagram, X, YouTube, LinkedIn, Facebook, Xiaohongshu, 1688, Amazon,
|
|
308
|
+
Examples: TikTok, TikTok ads, Instagram, X, YouTube, LinkedIn, Facebook, Xiaohongshu, 1688, Amazon, Google Trends.
|
|
310
309
|
|
|
311
310
|
### 3. Decide and Shortlist
|
|
312
311
|
|
|
@@ -346,7 +345,7 @@ This is not a full catalog. It is a practical map of the problems PostPlus is me
|
|
|
346
345
|
|---|---|---|
|
|
347
346
|
| Understand a market or audience | Topic listening, trend discovery, competitor snapshots, comment mining, audience language, demand signals | TikTok, Instagram, X, YouTube, LinkedIn, Facebook, Xiaohongshu, Google Trends, Amazon reviews |
|
|
348
347
|
| Find creators or KOL/KOC partners | Creator discovery, profile enrichment, content-fit scoring, shortlist building, contact signal extraction, outreach prep | TikTok creators, Instagram creators, Xiaohongshu accounts, X accounts, creator graph, follower bands, engagement proxy |
|
|
349
|
-
| Decide whether a product is worth testing | Product selection, marketplace comparison, channel fit, price bands, review analysis, supply-side checks, sourcing judgment | Amazon,
|
|
348
|
+
| Decide whether a product is worth testing | Product selection, marketplace comparison, channel fit, price bands, review analysis, supply-side checks, sourcing judgment | Amazon, 1688, Google Trends, Xiaohongshu commerce, supplier ranking, SKU, MOQ, margin risk |
|
|
350
349
|
| Turn references into creative direction | Reference decoding, hook analysis, visual grammar, benchmark-to-brief, persona packs, storyboard planning, prompt QA | TikTok videos, Reels, Xiaohongshu notes, short-form hooks, UGC, product demo, lifestyle, testimonial |
|
|
351
350
|
| Produce media assets | Transcription, subtitles, frame extraction, B-roll planning, image generation, video generation, voice generation, edit packaging | Whisper, SRT/VTT/ASS, B-roll, storyboard grid, hosted media generation, image prompts, video requests |
|
|
352
351
|
| Plan content and messaging | Positioning, content strategy, copywriting, social content, email sequences, SEO, AI search, launch planning | Blog, landing page, LinkedIn, X, Xiaohongshu, cold email, content pillars, hooks, objections, offers |
|
|
@@ -393,4 +392,3 @@ The best first prompt includes:
|
|
|
393
392
|
- the target platform if you already know it
|
|
394
393
|
- any reference links, files, accounts, or assets
|
|
395
394
|
- the artifact you want at the end
|
|
396
|
-
|
package/build/auth-login.js
CHANGED
|
@@ -2,6 +2,7 @@ import { randomUUID } from 'node:crypto';
|
|
|
2
2
|
import { createServer } from 'node:http';
|
|
3
3
|
import { requireHostedBaseUrl } from './hosted-release.js';
|
|
4
4
|
import { setLocalSession } from './local-state.js';
|
|
5
|
+
export const CLI_AUTH_HANDOFF_TIMEOUT_MS = 30 * 60 * 1000;
|
|
5
6
|
export async function loginWithBrowserHandoff() {
|
|
6
7
|
const baseUrl = await requireHostedBaseUrl();
|
|
7
8
|
const handoff = await createCliAuthHandoffServer({
|
|
@@ -18,7 +19,7 @@ export async function loginWithBrowserHandoff() {
|
|
|
18
19
|
'Open this URL in your browser to continue:',
|
|
19
20
|
loginUrl,
|
|
20
21
|
'',
|
|
21
|
-
'Waiting for browser sign-in...',
|
|
22
|
+
'Waiting for browser sign-in (up to 30 minutes)...',
|
|
22
23
|
'',
|
|
23
24
|
].join('\n'));
|
|
24
25
|
try {
|
|
@@ -84,7 +85,7 @@ export function formatCliSessionAuthError(payload) {
|
|
|
84
85
|
}
|
|
85
86
|
return 'Failed to validate the browser session for PostPlus CLI.';
|
|
86
87
|
}
|
|
87
|
-
async function createCliAuthHandoffServer(input) {
|
|
88
|
+
export async function createCliAuthHandoffServer(input) {
|
|
88
89
|
const requestId = randomUUID();
|
|
89
90
|
return new Promise((resolve, reject) => {
|
|
90
91
|
let settled = false;
|
|
@@ -107,6 +108,7 @@ async function createCliAuthHandoffServer(input) {
|
|
|
107
108
|
response.writeHead(204, {
|
|
108
109
|
'Access-Control-Allow-Headers': 'Content-Type',
|
|
109
110
|
'Access-Control-Allow-Methods': 'POST, OPTIONS',
|
|
111
|
+
'Access-Control-Allow-Private-Network': 'true',
|
|
110
112
|
'Access-Control-Allow-Origin': allowOrigin,
|
|
111
113
|
'Access-Control-Max-Age': '600',
|
|
112
114
|
Vary: 'Origin',
|
|
@@ -184,7 +186,7 @@ async function createCliAuthHandoffServer(input) {
|
|
|
184
186
|
rejectPayload?.(new Error('Timed out waiting for the browser sign-in handoff.'));
|
|
185
187
|
}
|
|
186
188
|
server.close();
|
|
187
|
-
},
|
|
189
|
+
}, CLI_AUTH_HANDOFF_TIMEOUT_MS);
|
|
188
190
|
resolve({
|
|
189
191
|
bridgeUrl: `http://127.0.0.1:${address.port}/handoff`,
|
|
190
192
|
close: async () => new Promise((innerResolve, innerReject) => {
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { mkdtemp, open, readFile, rm } from 'node:fs/promises';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
export async function runCommand(command, args, options = {}) {
|
|
6
|
+
const tempDir = await mkdtemp(join(tmpdir(), 'postplus-cli-command-'));
|
|
7
|
+
const stdoutPath = join(tempDir, 'stdout.txt');
|
|
8
|
+
const stdoutFile = await open(stdoutPath, 'w');
|
|
9
|
+
try {
|
|
10
|
+
const result = await new Promise((resolve, reject) => {
|
|
11
|
+
const child = spawn(command, args, {
|
|
12
|
+
stdio: ['ignore', stdoutFile.fd, 'pipe'],
|
|
13
|
+
});
|
|
14
|
+
const stderr = [];
|
|
15
|
+
const timer = setTimeout(() => {
|
|
16
|
+
child.kill('SIGTERM');
|
|
17
|
+
reject(new Error(`Command timed out: ${command} ${args.join(' ')}`));
|
|
18
|
+
}, options.timeoutMs ?? 60_000);
|
|
19
|
+
child.stderr?.on('data', (chunk) => {
|
|
20
|
+
stderr.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
21
|
+
});
|
|
22
|
+
child.on('error', (error) => {
|
|
23
|
+
clearTimeout(timer);
|
|
24
|
+
reject(error);
|
|
25
|
+
});
|
|
26
|
+
child.on('exit', (code) => {
|
|
27
|
+
clearTimeout(timer);
|
|
28
|
+
const stderrText = Buffer.concat(stderr).toString('utf8');
|
|
29
|
+
if (code === 0) {
|
|
30
|
+
resolve({
|
|
31
|
+
stderr: stderrText,
|
|
32
|
+
stdout: '',
|
|
33
|
+
});
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
reject(new Error(`Command failed (${code ?? 'unknown'}): ${command} ${args.join(' ')}${stderrText ? `\n${stderrText}` : ''}`));
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
await stdoutFile.close();
|
|
40
|
+
return {
|
|
41
|
+
...result,
|
|
42
|
+
stdout: await readFile(stdoutPath, 'utf8'),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
finally {
|
|
46
|
+
await stdoutFile.close().catch(() => { });
|
|
47
|
+
await rm(tempDir, { force: true, recursive: true });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
export async function runInteractiveCommand(command, args) {
|
|
51
|
+
return await new Promise((resolve, reject) => {
|
|
52
|
+
const child = spawn(command, args, {
|
|
53
|
+
stdio: 'inherit',
|
|
54
|
+
});
|
|
55
|
+
child.on('error', reject);
|
|
56
|
+
child.on('exit', (code) => {
|
|
57
|
+
resolve(code ?? 1);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
}
|
package/build/index.js
CHANGED
|
@@ -6,8 +6,9 @@ import { clearAuthState, formatAuthStatusReport, generateAuthStatusReport, } fro
|
|
|
6
6
|
import { formatDoctorReport, generateDoctorReport, } from './doctor.js';
|
|
7
7
|
import { assertConfigFilePermissions } from './local-state.js';
|
|
8
8
|
import { POSTPLUS_SKILLS_INSTALL_COMMAND, loadPublicSkillCatalog, } from './skill-catalog.js';
|
|
9
|
+
import { runPostPlusSkillUninstall, runPostPlusSkillUpdate, } from './skill-management.js';
|
|
9
10
|
import { formatStatusReport, generateStatusReport } from './status.js';
|
|
10
|
-
|
|
11
|
+
import { refreshUpdateCheckBaseline } from './update-check.js';
|
|
11
12
|
function printAuthHelp() {
|
|
12
13
|
process.stdout.write(`PostPlus CLI — auth commands
|
|
13
14
|
|
|
@@ -36,6 +37,8 @@ Usage:
|
|
|
36
37
|
postplus auth validate [--json]
|
|
37
38
|
postplus auth logout [--json]
|
|
38
39
|
postplus doctor [--json]
|
|
40
|
+
postplus update
|
|
41
|
+
postplus uninstall
|
|
39
42
|
postplus list [--json]
|
|
40
43
|
postplus status [--json]
|
|
41
44
|
postplus help
|
|
@@ -93,6 +96,16 @@ async function runList(json) {
|
|
|
93
96
|
process.stdout.write(`${lines.join('\n')}\n`);
|
|
94
97
|
return 0;
|
|
95
98
|
}
|
|
99
|
+
async function runSkillUpdateCommand() {
|
|
100
|
+
const exitCode = await runPostPlusSkillUpdate();
|
|
101
|
+
if (exitCode === 0) {
|
|
102
|
+
await refreshUpdateCheckBaseline().catch(() => { });
|
|
103
|
+
}
|
|
104
|
+
return exitCode;
|
|
105
|
+
}
|
|
106
|
+
async function runSkillUninstallCommand() {
|
|
107
|
+
return runPostPlusSkillUninstall();
|
|
108
|
+
}
|
|
96
109
|
function writeJson(value) {
|
|
97
110
|
process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
|
|
98
111
|
}
|
|
@@ -177,10 +190,14 @@ async function main() {
|
|
|
177
190
|
process.exitCode = await runDoctor(json);
|
|
178
191
|
return;
|
|
179
192
|
case 'install':
|
|
193
|
+
process.stderr.write(`PostPlus CLI does not install skills directly. Run \`${POSTPLUS_SKILLS_INSTALL_COMMAND}\`.\n`);
|
|
194
|
+
process.exitCode = 1;
|
|
195
|
+
return;
|
|
180
196
|
case 'update':
|
|
197
|
+
process.exitCode = await runSkillUpdateCommand();
|
|
198
|
+
return;
|
|
181
199
|
case 'uninstall':
|
|
182
|
-
process.
|
|
183
|
-
process.exitCode = 1;
|
|
200
|
+
process.exitCode = await runSkillUninstallCommand();
|
|
184
201
|
return;
|
|
185
202
|
case 'list':
|
|
186
203
|
process.exitCode = await runList(json);
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { POSTPLUS_SKILLS_INSTALL_COMMAND, POSTPLUS_SKILLS_REPO, loadPublicSkillCatalog, } from './skill-catalog.js';
|
|
2
|
+
import { runCommand, runInteractiveCommand } from './command-runner.js';
|
|
3
|
+
const SKILLS_AGENTS = ['claude-code', 'codex', 'cursor'];
|
|
4
|
+
const NPX_SKILLS = ['-y', 'skills'];
|
|
5
|
+
export async function runPostPlusSkillUpdate() {
|
|
6
|
+
const catalog = await loadPublicSkillCatalog();
|
|
7
|
+
const skillNames = catalog.skills.map((skill) => skill.skillId);
|
|
8
|
+
if (skillNames.length === 0) {
|
|
9
|
+
throw new Error('PostPlus public skill catalog has no released skills.');
|
|
10
|
+
}
|
|
11
|
+
return runInteractiveCommand('npx', buildPostPlusSkillUpdateArgs(skillNames));
|
|
12
|
+
}
|
|
13
|
+
export async function runPostPlusSkillUninstall() {
|
|
14
|
+
const catalog = await loadPublicSkillCatalog();
|
|
15
|
+
const skillNames = catalog.skills.map((skill) => skill.skillId);
|
|
16
|
+
if (skillNames.length === 0) {
|
|
17
|
+
throw new Error('PostPlus public skill catalog has no released skills.');
|
|
18
|
+
}
|
|
19
|
+
return runInteractiveCommand('npx', buildPostPlusSkillUninstallArgs(skillNames));
|
|
20
|
+
}
|
|
21
|
+
export async function generateSkillInstallStatusReport(dependencies = {
|
|
22
|
+
runCommand,
|
|
23
|
+
}) {
|
|
24
|
+
const catalog = await loadPublicSkillCatalog();
|
|
25
|
+
const requiredSkills = new Set(catalog.skills.map((skill) => skill.skillId));
|
|
26
|
+
try {
|
|
27
|
+
const installed = await listInstalledSkills(dependencies);
|
|
28
|
+
const postPlusInstalled = installed.filter((skill) => requiredSkills.has(skill.name));
|
|
29
|
+
const installedNames = new Set(postPlusInstalled.map((skill) => skill.name));
|
|
30
|
+
const missingSkills = [...requiredSkills].filter((skill) => !installedNames.has(skill));
|
|
31
|
+
const scopes = [
|
|
32
|
+
...new Set(postPlusInstalled
|
|
33
|
+
.map((skill) => skill.scope)
|
|
34
|
+
.filter((scope) => scope.trim().length > 0)),
|
|
35
|
+
].sort();
|
|
36
|
+
return {
|
|
37
|
+
ok: missingSkills.length === 0,
|
|
38
|
+
error: null,
|
|
39
|
+
installCommand: POSTPLUS_SKILLS_INSTALL_COMMAND,
|
|
40
|
+
installedCount: installedNames.size,
|
|
41
|
+
missingSkills,
|
|
42
|
+
requiredCount: requiredSkills.size,
|
|
43
|
+
scopes,
|
|
44
|
+
source: POSTPLUS_SKILLS_REPO,
|
|
45
|
+
updateCommand: formatPostPlusSkillUpdateCommand(),
|
|
46
|
+
uninstallCommand: formatPostPlusSkillUninstallCommand(),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
return {
|
|
51
|
+
ok: false,
|
|
52
|
+
error: error instanceof Error
|
|
53
|
+
? error.message
|
|
54
|
+
: 'Failed to inspect installed PostPlus skills.',
|
|
55
|
+
installCommand: POSTPLUS_SKILLS_INSTALL_COMMAND,
|
|
56
|
+
installedCount: 0,
|
|
57
|
+
missingSkills: [...requiredSkills],
|
|
58
|
+
requiredCount: requiredSkills.size,
|
|
59
|
+
scopes: [],
|
|
60
|
+
source: POSTPLUS_SKILLS_REPO,
|
|
61
|
+
updateCommand: formatPostPlusSkillUpdateCommand(),
|
|
62
|
+
uninstallCommand: formatPostPlusSkillUninstallCommand(),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
export function formatSkillInstallStatusReport(report) {
|
|
67
|
+
const lines = ['PostPlus skills status', ''];
|
|
68
|
+
if (report.error) {
|
|
69
|
+
lines.push(`[FAIL] Skill installer: ${report.error}`);
|
|
70
|
+
}
|
|
71
|
+
else if (report.ok) {
|
|
72
|
+
lines.push(`[PASS] Installed released skills: ${report.installedCount}/${report.requiredCount}`);
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
lines.push(`[FAIL] Installed released skills: ${report.installedCount}/${report.requiredCount}`);
|
|
76
|
+
}
|
|
77
|
+
lines.push(` Source: ${report.source}`);
|
|
78
|
+
lines.push(` Scope: ${report.scopes.length > 0 ? report.scopes.join(', ') : 'none detected'}`);
|
|
79
|
+
if (report.missingSkills.length > 0) {
|
|
80
|
+
lines.push(` Missing: ${formatSkillList(report.missingSkills, 8)}`, ` Fix: ${report.installCommand}`);
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
lines.push(` Update: ${report.updateCommand}`);
|
|
84
|
+
}
|
|
85
|
+
return lines.join('\n');
|
|
86
|
+
}
|
|
87
|
+
export function buildPostPlusSkillUpdateArgs(skillNames) {
|
|
88
|
+
return [...NPX_SKILLS, 'update', ...skillNames, '--yes'];
|
|
89
|
+
}
|
|
90
|
+
export function buildPostPlusSkillUninstallArgs(skillNames) {
|
|
91
|
+
return [
|
|
92
|
+
...NPX_SKILLS,
|
|
93
|
+
'remove',
|
|
94
|
+
...skillNames,
|
|
95
|
+
'--agent',
|
|
96
|
+
...SKILLS_AGENTS,
|
|
97
|
+
'--yes',
|
|
98
|
+
];
|
|
99
|
+
}
|
|
100
|
+
export function formatPostPlusSkillUpdateCommand() {
|
|
101
|
+
return 'postplus update';
|
|
102
|
+
}
|
|
103
|
+
export function formatPostPlusSkillUninstallCommand() {
|
|
104
|
+
return 'postplus uninstall';
|
|
105
|
+
}
|
|
106
|
+
async function listInstalledSkills(dependencies) {
|
|
107
|
+
const [project, global] = await Promise.all([
|
|
108
|
+
listInstalledSkillsForScope(dependencies, []),
|
|
109
|
+
listInstalledSkillsForScope(dependencies, ['--global']),
|
|
110
|
+
]);
|
|
111
|
+
const byKey = new Map();
|
|
112
|
+
for (const skill of [...project, ...global]) {
|
|
113
|
+
byKey.set(`${skill.scope}:${skill.name}:${skill.path}`, skill);
|
|
114
|
+
}
|
|
115
|
+
return [...byKey.values()];
|
|
116
|
+
}
|
|
117
|
+
async function listInstalledSkillsForScope(dependencies, scopeArgs) {
|
|
118
|
+
const result = await dependencies.runCommand('npx', [...NPX_SKILLS, 'list', '--json', ...scopeArgs], {
|
|
119
|
+
timeoutMs: 60_000,
|
|
120
|
+
});
|
|
121
|
+
const parsed = JSON.parse(result.stdout);
|
|
122
|
+
if (!Array.isArray(parsed)) {
|
|
123
|
+
throw new Error('`skills list --json` returned an invalid payload.');
|
|
124
|
+
}
|
|
125
|
+
return parsed.map(normalizeInstalledSkillEntry);
|
|
126
|
+
}
|
|
127
|
+
function normalizeInstalledSkillEntry(value) {
|
|
128
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
129
|
+
throw new Error('`skills list --json` returned an invalid skill entry.');
|
|
130
|
+
}
|
|
131
|
+
const record = value;
|
|
132
|
+
const name = typeof record.name === 'string' ? record.name.trim() : '';
|
|
133
|
+
const skillPath = typeof record.path === 'string' ? record.path.trim() : '';
|
|
134
|
+
const scope = typeof record.scope === 'string' ? record.scope.trim() : '';
|
|
135
|
+
const agents = Array.isArray(record.agents)
|
|
136
|
+
? record.agents
|
|
137
|
+
.filter((agent) => typeof agent === 'string')
|
|
138
|
+
.map((agent) => agent.trim())
|
|
139
|
+
.filter(Boolean)
|
|
140
|
+
: [];
|
|
141
|
+
if (!name || !skillPath || !scope) {
|
|
142
|
+
throw new Error('`skills list --json` returned an incomplete skill entry.');
|
|
143
|
+
}
|
|
144
|
+
return {
|
|
145
|
+
agents,
|
|
146
|
+
name,
|
|
147
|
+
path: skillPath,
|
|
148
|
+
scope,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
function formatSkillList(skills, limit) {
|
|
152
|
+
const visible = skills.slice(0, limit);
|
|
153
|
+
const rest = skills.length - visible.length;
|
|
154
|
+
return rest > 0 ? `${visible.join(', ')} (+${rest} more)` : visible.join(', ');
|
|
155
|
+
}
|
package/build/status.js
CHANGED
|
@@ -1,14 +1,27 @@
|
|
|
1
1
|
import { formatAuthStatusReport, generateAuthStatusReport, } from './auth.js';
|
|
2
2
|
import { formatDoctorReport, generateDoctorReport, } from './doctor.js';
|
|
3
|
+
import { formatSkillInstallStatusReport, generateSkillInstallStatusReport, } from './skill-management.js';
|
|
4
|
+
import { formatUpdateStatusReport, generateUpdateStatusReport, } from './update-check.js';
|
|
3
5
|
export async function generateStatusReport() {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
6
|
+
return generateStatusReportWithDependencies();
|
|
7
|
+
}
|
|
8
|
+
export async function generateStatusReportWithDependencies(dependencies = {}) {
|
|
9
|
+
const generateAuthStatus = dependencies.generateAuthStatus ?? generateAuthStatusReport;
|
|
10
|
+
const generateDoctor = dependencies.generateDoctor ?? generateDoctorReport;
|
|
11
|
+
const generateSkillStatus = dependencies.generateSkillStatus ?? generateSkillInstallStatusReport;
|
|
12
|
+
const generateUpdateStatus = dependencies.generateUpdateStatus ?? generateUpdateStatusReport;
|
|
13
|
+
const [doctor, auth, skills, updates] = await Promise.all([
|
|
14
|
+
generateDoctor(),
|
|
15
|
+
generateAuthStatus(),
|
|
16
|
+
generateSkillStatus(),
|
|
17
|
+
generateUpdateStatus(),
|
|
7
18
|
]);
|
|
8
19
|
return {
|
|
9
|
-
ok: doctor.ok && auth.ok,
|
|
20
|
+
ok: doctor.ok && auth.ok && skills.ok && updates.ok,
|
|
10
21
|
doctor,
|
|
11
22
|
auth,
|
|
23
|
+
skills,
|
|
24
|
+
updates,
|
|
12
25
|
};
|
|
13
26
|
}
|
|
14
27
|
export function formatStatusReport(report) {
|
|
@@ -20,5 +33,9 @@ export function formatStatusReport(report) {
|
|
|
20
33
|
formatDoctorReport(report.doctor),
|
|
21
34
|
'',
|
|
22
35
|
formatAuthStatusReport(report.auth),
|
|
36
|
+
'',
|
|
37
|
+
formatSkillInstallStatusReport(report.skills),
|
|
38
|
+
'',
|
|
39
|
+
formatUpdateStatusReport(report.updates),
|
|
23
40
|
].join('\n');
|
|
24
41
|
}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
import { getPostPlusConfigDir } from './local-state.js';
|
|
5
|
+
import { POSTPLUS_SKILLS_REPO } from './skill-catalog.js';
|
|
6
|
+
const UPDATE_CHECK_TTL_MS = 24 * 60 * 60 * 1000;
|
|
7
|
+
const UPDATE_CHECK_CACHE_FILE = 'update-check.json';
|
|
8
|
+
const NPM_PACKAGE_NAME = '@postplus/cli';
|
|
9
|
+
const NPM_LATEST_URL = `https://registry.npmjs.org/${encodeURIComponent(NPM_PACKAGE_NAME)}/latest`;
|
|
10
|
+
const POSTPLUS_SKILLS_MAIN_URL = 'https://api.github.com/repos/PostPlusAI/postplus-skills/commits/main';
|
|
11
|
+
export async function generateUpdateStatusReport(input = {}, dependencies = {
|
|
12
|
+
fetchFn: fetch,
|
|
13
|
+
}) {
|
|
14
|
+
const currentVersion = await readCurrentCliVersion();
|
|
15
|
+
const cache = await readUpdateCheckCache();
|
|
16
|
+
if (cache &&
|
|
17
|
+
!input.force &&
|
|
18
|
+
cache.cli.currentVersion === currentVersion &&
|
|
19
|
+
Date.now() - Date.parse(cache.checkedAt) < UPDATE_CHECK_TTL_MS) {
|
|
20
|
+
return buildUpdateReport({
|
|
21
|
+
cache,
|
|
22
|
+
currentVersion,
|
|
23
|
+
previousSkillRevision: cache.skills.latestRevision,
|
|
24
|
+
source: 'cache',
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
const [latestCliVersion, latestSkillRevision] = await Promise.all([
|
|
29
|
+
fetchLatestCliVersion(dependencies.fetchFn),
|
|
30
|
+
fetchLatestSkillRevision(dependencies.fetchFn),
|
|
31
|
+
]);
|
|
32
|
+
const nextCache = {
|
|
33
|
+
checkedAt: new Date().toISOString(),
|
|
34
|
+
cli: {
|
|
35
|
+
currentVersion,
|
|
36
|
+
latestVersion: latestCliVersion,
|
|
37
|
+
},
|
|
38
|
+
skills: {
|
|
39
|
+
latestRevision: latestSkillRevision,
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
await writeUpdateCheckCache(nextCache);
|
|
43
|
+
return buildUpdateReport({
|
|
44
|
+
cache: nextCache,
|
|
45
|
+
currentVersion,
|
|
46
|
+
previousSkillRevision: input.resetSkillBaseline
|
|
47
|
+
? latestSkillRevision
|
|
48
|
+
: cache?.skills.latestRevision ?? latestSkillRevision,
|
|
49
|
+
source: 'remote',
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
const warning = error instanceof Error ? error.message : 'Update check failed.';
|
|
54
|
+
if (cache) {
|
|
55
|
+
return {
|
|
56
|
+
...buildUpdateReport({
|
|
57
|
+
cache,
|
|
58
|
+
currentVersion,
|
|
59
|
+
previousSkillRevision: cache.skills.latestRevision,
|
|
60
|
+
source: 'cache',
|
|
61
|
+
}),
|
|
62
|
+
warning,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
checkedAt: null,
|
|
67
|
+
ok: true,
|
|
68
|
+
source: 'unavailable',
|
|
69
|
+
cli: {
|
|
70
|
+
currentVersion,
|
|
71
|
+
latestVersion: null,
|
|
72
|
+
updateAvailable: false,
|
|
73
|
+
updateCommand: 'npm install -g @postplus/cli',
|
|
74
|
+
},
|
|
75
|
+
skills: {
|
|
76
|
+
currentRevision: null,
|
|
77
|
+
latestRevision: null,
|
|
78
|
+
updateAvailable: false,
|
|
79
|
+
updateCommand: 'postplus update',
|
|
80
|
+
},
|
|
81
|
+
warning,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
export async function refreshUpdateCheckBaseline() {
|
|
86
|
+
await generateUpdateStatusReport({
|
|
87
|
+
force: true,
|
|
88
|
+
resetSkillBaseline: true,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
export function formatUpdateStatusReport(report) {
|
|
92
|
+
const lines = ['PostPlus update status', ''];
|
|
93
|
+
const cliMarker = report.cli.updateAvailable ? '[WARN]' : '[PASS]';
|
|
94
|
+
lines.push(`${cliMarker} CLI: ${report.cli.currentVersion}${report.cli.latestVersion ? ` (latest ${report.cli.latestVersion})` : ''}`);
|
|
95
|
+
if (report.cli.updateAvailable) {
|
|
96
|
+
lines.push(` Update: ${report.cli.updateCommand}`);
|
|
97
|
+
}
|
|
98
|
+
const skillMarker = report.skills.updateAvailable ? '[WARN]' : '[PASS]';
|
|
99
|
+
lines.push(`${skillMarker} Skills: ${report.skills.latestRevision
|
|
100
|
+
? `release ${shortRevision(report.skills.latestRevision)}`
|
|
101
|
+
: 'release unknown'}`);
|
|
102
|
+
if (report.skills.updateAvailable) {
|
|
103
|
+
lines.push(` Update: ${report.skills.updateCommand}`);
|
|
104
|
+
}
|
|
105
|
+
lines.push(` Checked: ${report.checkedAt ?? 'not checked'} (${report.source})`);
|
|
106
|
+
if (report.warning) {
|
|
107
|
+
lines.push(` Warning: ${report.warning}`);
|
|
108
|
+
}
|
|
109
|
+
return lines.join('\n');
|
|
110
|
+
}
|
|
111
|
+
function buildUpdateReport(input) {
|
|
112
|
+
return {
|
|
113
|
+
checkedAt: input.cache.checkedAt,
|
|
114
|
+
ok: true,
|
|
115
|
+
source: input.source,
|
|
116
|
+
cli: {
|
|
117
|
+
currentVersion: input.currentVersion,
|
|
118
|
+
latestVersion: input.cache.cli.latestVersion,
|
|
119
|
+
updateAvailable: compareVersions(input.cache.cli.latestVersion, input.currentVersion) > 0,
|
|
120
|
+
updateCommand: 'npm install -g @postplus/cli',
|
|
121
|
+
},
|
|
122
|
+
skills: {
|
|
123
|
+
currentRevision: input.previousSkillRevision,
|
|
124
|
+
latestRevision: input.cache.skills.latestRevision,
|
|
125
|
+
updateAvailable: input.cache.skills.latestRevision !== input.previousSkillRevision,
|
|
126
|
+
updateCommand: 'postplus update',
|
|
127
|
+
},
|
|
128
|
+
warning: null,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
async function readCurrentCliVersion() {
|
|
132
|
+
const packageJsonPath = new URL('../package.json', import.meta.url);
|
|
133
|
+
const raw = await readFile(packageJsonPath, 'utf8');
|
|
134
|
+
const parsed = JSON.parse(raw);
|
|
135
|
+
if (typeof parsed.version !== 'string' || !parsed.version.trim()) {
|
|
136
|
+
throw new Error('Could not read the current PostPlus CLI version.');
|
|
137
|
+
}
|
|
138
|
+
return parsed.version.trim();
|
|
139
|
+
}
|
|
140
|
+
async function fetchLatestCliVersion(fetchFn) {
|
|
141
|
+
const response = await fetchFn(NPM_LATEST_URL, {
|
|
142
|
+
headers: {
|
|
143
|
+
accept: 'application/json',
|
|
144
|
+
'user-agent': `postplus-cli-update-check/${await readCurrentCliVersion()}`,
|
|
145
|
+
},
|
|
146
|
+
signal: AbortSignal.timeout(15_000),
|
|
147
|
+
});
|
|
148
|
+
if (!response.ok) {
|
|
149
|
+
throw new Error(`Failed to check latest PostPlus CLI version (${response.status}).`);
|
|
150
|
+
}
|
|
151
|
+
const payload = (await response.json());
|
|
152
|
+
if (typeof payload.version !== 'string' || !payload.version.trim()) {
|
|
153
|
+
throw new Error('NPM returned an invalid PostPlus CLI version payload.');
|
|
154
|
+
}
|
|
155
|
+
return payload.version.trim();
|
|
156
|
+
}
|
|
157
|
+
async function fetchLatestSkillRevision(fetchFn) {
|
|
158
|
+
const response = await fetchFn(POSTPLUS_SKILLS_MAIN_URL, {
|
|
159
|
+
headers: {
|
|
160
|
+
accept: 'application/vnd.github+json',
|
|
161
|
+
'user-agent': `postplus-cli-update-check/${await readCurrentCliVersion()}`,
|
|
162
|
+
},
|
|
163
|
+
signal: AbortSignal.timeout(15_000),
|
|
164
|
+
});
|
|
165
|
+
if (!response.ok) {
|
|
166
|
+
throw new Error(`Failed to check latest ${POSTPLUS_SKILLS_REPO} revision (${response.status}).`);
|
|
167
|
+
}
|
|
168
|
+
const payload = (await response.json());
|
|
169
|
+
if (typeof payload.sha === 'string' && payload.sha.trim()) {
|
|
170
|
+
return payload.sha.trim();
|
|
171
|
+
}
|
|
172
|
+
return createHash('sha256')
|
|
173
|
+
.update(JSON.stringify(payload))
|
|
174
|
+
.digest('hex');
|
|
175
|
+
}
|
|
176
|
+
async function readUpdateCheckCache() {
|
|
177
|
+
try {
|
|
178
|
+
const raw = await readFile(getUpdateCheckCachePath(), 'utf8');
|
|
179
|
+
const parsed = JSON.parse(raw);
|
|
180
|
+
if (typeof parsed.checkedAt !== 'string' ||
|
|
181
|
+
typeof parsed.cli?.currentVersion !== 'string' ||
|
|
182
|
+
typeof parsed.cli?.latestVersion !== 'string' ||
|
|
183
|
+
typeof parsed.skills?.latestRevision !== 'string') {
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
return parsed;
|
|
187
|
+
}
|
|
188
|
+
catch (error) {
|
|
189
|
+
const nodeError = error;
|
|
190
|
+
if (nodeError.code === 'ENOENT') {
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
throw error;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
async function writeUpdateCheckCache(cache) {
|
|
197
|
+
const cachePath = getUpdateCheckCachePath();
|
|
198
|
+
await mkdir(dirname(cachePath), { recursive: true });
|
|
199
|
+
await writeFile(cachePath, `${JSON.stringify(cache, null, 2)}\n`, 'utf8');
|
|
200
|
+
}
|
|
201
|
+
function getUpdateCheckCachePath() {
|
|
202
|
+
return join(getPostPlusConfigDir(), UPDATE_CHECK_CACHE_FILE);
|
|
203
|
+
}
|
|
204
|
+
function compareVersions(a, b) {
|
|
205
|
+
const left = parseVersion(a);
|
|
206
|
+
const right = parseVersion(b);
|
|
207
|
+
const length = Math.max(left.length, right.length);
|
|
208
|
+
for (let index = 0; index < length; index += 1) {
|
|
209
|
+
const leftPart = left[index] ?? 0;
|
|
210
|
+
const rightPart = right[index] ?? 0;
|
|
211
|
+
if (leftPart > rightPart) {
|
|
212
|
+
return 1;
|
|
213
|
+
}
|
|
214
|
+
if (leftPart < rightPart) {
|
|
215
|
+
return -1;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return 0;
|
|
219
|
+
}
|
|
220
|
+
function parseVersion(value) {
|
|
221
|
+
return value
|
|
222
|
+
.replace(/^[^\d]*/, '')
|
|
223
|
+
.split(/[.-]/)
|
|
224
|
+
.map((part) => Number.parseInt(part, 10))
|
|
225
|
+
.map((part) => (Number.isFinite(part) ? part : 0));
|
|
226
|
+
}
|
|
227
|
+
function shortRevision(revision) {
|
|
228
|
+
return revision.length > 12 ? revision.slice(0, 12) : revision;
|
|
229
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@postplus/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.14",
|
|
4
4
|
"packageManager": "pnpm@10.30.3+sha512.c961d1e0a2d8e354ecaa5166b822516668b7f44cb5bd95122d590dd81922f606f5473b6d23ec4a5be05e7fcd18e8488d47d978bbe981872f1145d06e9a740017",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "PostPlus CLI for PostPlus Cloud auth, status, and diagnostics.",
|
|
@@ -10,12 +10,15 @@
|
|
|
10
10
|
"build/auth-login.js",
|
|
11
11
|
"build/auth-validate.js",
|
|
12
12
|
"build/auth.js",
|
|
13
|
+
"build/command-runner.js",
|
|
13
14
|
"build/doctor.js",
|
|
14
15
|
"build/hosted-release.js",
|
|
15
16
|
"build/index.js",
|
|
16
17
|
"build/local-state.js",
|
|
17
18
|
"build/skill-catalog.js",
|
|
19
|
+
"build/skill-management.js",
|
|
18
20
|
"build/status.js",
|
|
21
|
+
"build/update-check.js",
|
|
19
22
|
"LICENSE",
|
|
20
23
|
"NOTICE",
|
|
21
24
|
"README.md"
|