@postplus/cli 0.1.12 → 0.1.13
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 +2 -4
- 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
|
@@ -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
|
|
|
@@ -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
|
-
|
|
@@ -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.13",
|
|
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"
|