@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 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 TikTok Shop potential."
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/RealProductStudio/postplus-skills`: the public skill repository that installs local marketing workflows into agent tools.
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 auth status
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 TikTok Shop or Amazon better?"
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, TikTok Shop, Xiaohongshu, Google Trends, or 1688 evidence
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, TikTok Shop, Google Trends.
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, TikTok Shop, 1688, Google Trends, Xiaohongshu commerce, supplier ranking, SKU, MOQ, margin risk |
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
-
@@ -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
- }, 5 * 60 * 1000);
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
- const REMOVED_SKILL_COMMAND_MESSAGE = `PostPlus CLI no longer installs skills directly. Run \`${POSTPLUS_SKILLS_INSTALL_COMMAND}\`.`;
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.stderr.write(`${REMOVED_SKILL_COMMAND_MESSAGE}\n`);
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
- const [doctor, auth] = await Promise.all([
5
- generateDoctorReport(),
6
- generateAuthStatusReport(),
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.12",
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"