@postplus/cli 0.1.18 → 0.1.20

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
@@ -33,7 +33,7 @@ Requires Node.js and npm.
33
33
  ```bash
34
34
  npm install -g @postplus/cli
35
35
  postplus auth login
36
- npx -y skills add PostPlusAI/postplus-skills --full-depth --skill '*' --agent claude-code codex cursor github-copilot windsurf trae trae-cn --yes
36
+ npx -y skills add PostPlusAI/postplus-skills --global --full-depth --skill '*' --agent claude-code codex cursor github-copilot windsurf trae trae-cn --yes
37
37
  ```
38
38
 
39
39
  Useful checks:
package/build/doctor.js CHANGED
@@ -1,20 +1,24 @@
1
1
  import { resolveFreshRemoteAuth, } from './auth-session.js';
2
2
  import { resolveHostedBaseUrl } from './hosted-release.js';
3
- function createPass(id, label, detail) {
3
+ import { formatLocalDependencyReport, generateLocalDependencyReport, } from './local-dependencies.js';
4
+ function createPass(id, label, detail, severity = 'required') {
4
5
  return {
5
6
  id,
6
7
  label,
7
8
  status: 'pass',
9
+ severity,
8
10
  detail,
9
11
  };
10
12
  }
11
- function createFail(id, label, detail, fix) {
13
+ function createFail(id, label, detail, fix, input = {}) {
12
14
  return {
13
15
  id,
14
16
  label,
15
17
  status: 'fail',
18
+ severity: input.severity ?? 'required',
16
19
  detail,
17
20
  fix,
21
+ metadata: input.metadata,
18
22
  };
19
23
  }
20
24
  export async function generateDoctorReport() {
@@ -22,6 +26,7 @@ export async function generateDoctorReport() {
22
26
  const checks = [
23
27
  createPass('hosted_base_url', 'PostPlus Cloud', `Using ${hostedBaseUrl ?? 'https://postplus.io'}`),
24
28
  ];
29
+ checks.push(await checkLocalDependencies());
25
30
  if (!hostedBaseUrl) {
26
31
  checks.push(createFail('remote_auth', 'Remote auth', 'PostPlus Cloud base URL could not be resolved.', 'Configure POSTPLUS_API_BASE_URL or run `postplus auth login`.'));
27
32
  return buildDoctorReport(checks);
@@ -43,9 +48,39 @@ export async function generateDoctorReport() {
43
48
  }
44
49
  return buildDoctorReport(checks);
45
50
  }
51
+ async function checkLocalDependencies() {
52
+ try {
53
+ const report = await generateLocalDependencyReport();
54
+ const detail = formatLocalDependencyReport(report);
55
+ if (!report.ok) {
56
+ return createFail('local_dependencies', 'Task-specific local media dependencies', detail, 'Run the affected PostPlus skill in a local agent. The installed postplus-shared rules tell the agent how to bootstrap approved missing media dependencies.', {
57
+ severity: 'task_specific',
58
+ metadata: {
59
+ bootstrapRule: 'postplus-shared',
60
+ missingDependencies: report.checks
61
+ .filter((check) => !check.ok)
62
+ .map((check) => ({
63
+ dependency: check.dependency,
64
+ detail: check.detail,
65
+ skillIds: check.skillIds,
66
+ })),
67
+ },
68
+ });
69
+ }
70
+ return createPass('local_dependencies', 'Local dependencies', detail);
71
+ }
72
+ catch (error) {
73
+ return createFail('local_dependencies', 'Local dependencies', error instanceof Error
74
+ ? error.message
75
+ : 'Failed to check local dependencies.');
76
+ }
77
+ }
46
78
  function buildDoctorReport(checks) {
79
+ const requiredOk = checks.every((check) => check.severity !== 'required' || check.status === 'pass');
47
80
  return {
81
+ schemaVersion: 1,
48
82
  ok: checks.every((check) => check.status === 'pass'),
83
+ requiredOk,
49
84
  checks,
50
85
  };
51
86
  }
@@ -120,11 +155,33 @@ function readCapabilityFailureLabel(value) {
120
155
  if (record.ok === true || record.required === false) {
121
156
  return null;
122
157
  }
123
- return typeof record.label === 'string'
158
+ const label = typeof record.label === 'string'
124
159
  ? record.label
125
160
  : typeof record.id === 'string'
126
161
  ? record.id
127
162
  : 'unknown capability';
163
+ const failedChecks = Array.isArray(record.checks)
164
+ ? record.checks
165
+ .map(readReadinessCheckFailureLabel)
166
+ .filter((check) => check !== null)
167
+ : [];
168
+ return failedChecks.length > 0
169
+ ? `${label} (${failedChecks.join(', ')})`
170
+ : label;
171
+ }
172
+ function readReadinessCheckFailureLabel(value) {
173
+ if (!value || typeof value !== 'object') {
174
+ return 'invalid readiness check';
175
+ }
176
+ const record = value;
177
+ if (record.ok === true || record.required === false) {
178
+ return null;
179
+ }
180
+ return typeof record.label === 'string'
181
+ ? record.label
182
+ : typeof record.id === 'string'
183
+ ? record.id
184
+ : 'unknown check';
128
185
  }
129
186
  function readErrorMessage(payload, fallback) {
130
187
  return typeof payload.error === 'string' && payload.error.trim().length > 0
@@ -143,12 +200,20 @@ function requestWithAuth(input, path) {
143
200
  export function formatDoctorReport(report) {
144
201
  const lines = ['PostPlus CLI doctor', ''];
145
202
  for (const check of report.checks) {
146
- const marker = check.status === 'pass' ? '[PASS]' : '[FAIL]';
203
+ const marker = check.status === 'pass'
204
+ ? '[PASS]'
205
+ : check.severity === 'task_specific'
206
+ ? '[WARN]'
207
+ : '[FAIL]';
147
208
  lines.push(`${marker} ${check.label}: ${check.detail}`);
148
209
  if (check.fix) {
149
210
  lines.push(` Fix: ${check.fix}`);
150
211
  }
151
212
  }
152
- lines.push('', report.ok ? 'Doctor passed.' : 'Doctor failed.');
213
+ lines.push('', report.ok
214
+ ? 'Doctor passed.'
215
+ : report.requiredOk
216
+ ? 'Doctor incomplete: task-specific checks need attention.'
217
+ : 'Doctor failed.');
153
218
  return lines.join('\n');
154
219
  }
package/build/index.js CHANGED
@@ -8,7 +8,7 @@ import { assertConfigFilePermissions } from './local-state.js';
8
8
  import { POSTPLUS_SKILLS_INSTALL_COMMAND, loadPublicSkillCatalog, } from './skill-catalog.js';
9
9
  import { runPostPlusSkillUninstall, runPostPlusSkillUpdate, } from './skill-management.js';
10
10
  import { formatStatusReport, generateStatusReport } from './status.js';
11
- import { refreshUpdateCheckBaseline } from './update-check.js';
11
+ import { readCurrentCliVersion, refreshUpdateCheckCache, } from './update-check.js';
12
12
  function printAuthHelp() {
13
13
  process.stdout.write(`PostPlus CLI — auth commands
14
14
 
@@ -41,6 +41,7 @@ Usage:
41
41
  postplus uninstall
42
42
  postplus list [--json]
43
43
  postplus status [--json]
44
+ postplus version
44
45
  postplus help
45
46
 
46
47
  Skills:
@@ -96,10 +97,14 @@ async function runList(json) {
96
97
  process.stdout.write(`${lines.join('\n')}\n`);
97
98
  return 0;
98
99
  }
100
+ async function runVersion() {
101
+ process.stdout.write(`${await readCurrentCliVersion()}\n`);
102
+ return 0;
103
+ }
99
104
  async function runSkillUpdateCommand() {
100
105
  const exitCode = await runPostPlusSkillUpdate();
101
106
  if (exitCode === 0) {
102
- await refreshUpdateCheckBaseline().catch(() => { });
107
+ await refreshUpdateCheckCache().catch(() => { });
103
108
  }
104
109
  return exitCode;
105
110
  }
@@ -172,6 +177,11 @@ async function main() {
172
177
  printHelp();
173
178
  process.exitCode = 0;
174
179
  return;
180
+ case '--version':
181
+ case '-v':
182
+ case 'version':
183
+ process.exitCode = await runVersion();
184
+ return;
175
185
  case 'help': {
176
186
  const [helpTopic] = rest;
177
187
  if (helpTopic === 'auth') {
@@ -0,0 +1,101 @@
1
+ import { runCommand } from './command-runner.js';
2
+ import { loadPublicSkillCatalog, } from './skill-catalog.js';
3
+ const LOCAL_DEPENDENCY_CHECK_TIMEOUT_MS = 10_000;
4
+ export async function generateLocalDependencyReport(options = {}) {
5
+ const loadCatalog = options.loadCatalog ?? loadPublicSkillCatalog;
6
+ const runDependencyCheck = options.runDependencyCheck ?? runLocalDependencyCommand;
7
+ const catalog = await loadCatalog();
8
+ const requirements = collectLocalDependencyRequirements(catalog);
9
+ const checks = await Promise.all(requirements.map(({ dependency, skillIds }) => checkLocalDependency(dependency, skillIds, runDependencyCheck)));
10
+ return {
11
+ ok: checks.every((check) => check.ok),
12
+ revision: catalog.revision,
13
+ source: catalog.source,
14
+ requiredCount: checks.length,
15
+ checks,
16
+ };
17
+ }
18
+ export function formatLocalDependencyReport(report) {
19
+ if (report.requiredCount === 0) {
20
+ return `No local runtime dependencies are required by released PostPlus skills (${report.revision}).`;
21
+ }
22
+ const missing = report.checks.filter((check) => !check.ok);
23
+ if (missing.length === 0) {
24
+ return `Ready (${report.requiredCount} local dependencies present; catalog ${report.revision})`;
25
+ }
26
+ return `Missing ${missing.length}/${report.requiredCount}: ${missing
27
+ .map((check) => `${check.dependency} for ${formatSkillList(check.skillIds)}`)
28
+ .join('; ')}`;
29
+ }
30
+ function collectLocalDependencyRequirements(catalog) {
31
+ const dependencyToSkills = new Map();
32
+ for (const skill of catalog.skills) {
33
+ for (const dependency of skill.localDependencies) {
34
+ if (!dependencyToSkills.has(dependency)) {
35
+ dependencyToSkills.set(dependency, new Set());
36
+ }
37
+ dependencyToSkills.get(dependency)?.add(skill.skillId);
38
+ }
39
+ }
40
+ return [...dependencyToSkills.entries()]
41
+ .map(([dependency, skillIds]) => ({
42
+ dependency,
43
+ skillIds: [...skillIds].sort((a, b) => a.localeCompare(b)),
44
+ }))
45
+ .sort((a, b) => a.dependency.localeCompare(b.dependency));
46
+ }
47
+ async function checkLocalDependency(dependency, skillIds, runDependencyCheck) {
48
+ try {
49
+ const command = buildLocalDependencyCommand(dependency);
50
+ await runDependencyCheck(command.command, command.args);
51
+ return {
52
+ dependency,
53
+ ok: true,
54
+ detail: 'available',
55
+ skillIds,
56
+ };
57
+ }
58
+ catch (error) {
59
+ return {
60
+ dependency,
61
+ ok: false,
62
+ detail: error instanceof Error
63
+ ? error.message
64
+ : 'Local dependency check failed.',
65
+ skillIds,
66
+ };
67
+ }
68
+ }
69
+ function buildLocalDependencyCommand(dependency) {
70
+ const parts = dependency.split(':');
71
+ if (parts.length === 1) {
72
+ return {
73
+ command: dependency,
74
+ args: ['--version'],
75
+ };
76
+ }
77
+ const [runtime, moduleName] = parts;
78
+ if (parts.length === 2 &&
79
+ runtime === 'python3' &&
80
+ /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(moduleName)) {
81
+ return {
82
+ command: 'python3',
83
+ args: [
84
+ '-c',
85
+ `import importlib; importlib.import_module(${JSON.stringify(moduleName)})`,
86
+ ],
87
+ };
88
+ }
89
+ throw new Error(`Unsupported local dependency requirement: ${dependency}`);
90
+ }
91
+ async function runLocalDependencyCommand(command, args) {
92
+ await runCommand(command, args, {
93
+ timeoutMs: LOCAL_DEPENDENCY_CHECK_TIMEOUT_MS,
94
+ });
95
+ }
96
+ function formatSkillList(skillIds) {
97
+ if (skillIds.length <= 3) {
98
+ return skillIds.join(', ');
99
+ }
100
+ return `${skillIds.slice(0, 3).join(', ')} +${skillIds.length - 3} more`;
101
+ }
@@ -103,6 +103,41 @@ export async function clearLocalAuthState() {
103
103
  return next;
104
104
  });
105
105
  }
106
+ export async function readManagedSkillBaseline() {
107
+ const config = await readLocalConfig();
108
+ const managedSkills = config?.managedSkills;
109
+ if (!managedSkills ||
110
+ typeof managedSkills.revision !== 'string' ||
111
+ !Array.isArray(managedSkills.skillNames)) {
112
+ return {
113
+ revision: null,
114
+ skillNames: [],
115
+ };
116
+ }
117
+ return {
118
+ revision: managedSkills.revision,
119
+ skillNames: normalizeSkillNames(managedSkills.skillNames),
120
+ };
121
+ }
122
+ export async function writeManagedSkillBaseline(input) {
123
+ return updateLocalConfig((current) => ({
124
+ ...(current ?? {}),
125
+ managedSkills: {
126
+ revision: input.revision,
127
+ skillNames: normalizeSkillNames(input.skillNames),
128
+ updatedAt: new Date().toISOString(),
129
+ },
130
+ }));
131
+ }
132
+ export async function clearManagedSkillBaseline() {
133
+ return updateLocalConfig((current) => {
134
+ const next = {
135
+ ...(current ?? {}),
136
+ };
137
+ delete next.managedSkills;
138
+ return next;
139
+ });
140
+ }
106
141
  export async function setLocalApiBaseUrl(apiBaseUrl) {
107
142
  const normalizedApiBaseUrl = apiBaseUrl.trim();
108
143
  if (normalizedApiBaseUrl.length === 0) {
@@ -226,3 +261,6 @@ function omitLegacyAuthFields(current) {
226
261
  const { apiKey: _apiKey, machineId: _machineId, ...rest } = (current ?? {});
227
262
  return rest;
228
263
  }
264
+ function normalizeSkillNames(values) {
265
+ return [...new Set(values.map((value) => value.trim()).filter(Boolean))].sort((left, right) => left.localeCompare(right));
266
+ }
@@ -12,64 +12,101 @@ const POSTPLUS_SKILLS_AGENT_ARGS = POSTPLUS_SKILLS_AGENT_TARGETS.join(' ');
12
12
  export const POSTPLUS_SKILLS_INSTALL_COMMAND = `npx -y skills add PostPlusAI/postplus-skills --global --full-depth --skill '*' --agent ${POSTPLUS_SKILLS_AGENT_ARGS} --yes`;
13
13
  export const POSTPLUS_SKILLS_LIST_COMMAND = 'npx -y skills add PostPlusAI/postplus-skills --list --full-depth';
14
14
  const POSTPLUS_SKILLS_INDEX_URL = 'https://raw.githubusercontent.com/PostPlusAI/postplus-skills/main/skills/INDEX.md';
15
- export async function loadPublicSkillCatalog() {
16
- const response = await fetch(POSTPLUS_SKILLS_INDEX_URL, {
15
+ const POSTPLUS_SKILLS_CATALOG_URL = 'https://raw.githubusercontent.com/PostPlusAI/postplus-skills/main/skills/catalog.json';
16
+ export async function loadPublicSkillCatalog(fetchFn = fetch) {
17
+ const response = await fetchFn(POSTPLUS_SKILLS_CATALOG_URL, {
17
18
  headers: {
18
- accept: 'text/markdown,text/plain',
19
+ accept: 'application/json',
19
20
  },
20
21
  signal: AbortSignal.timeout(15000),
21
22
  });
22
23
  if (!response.ok) {
23
24
  throw new Error(`Failed to load PostPlus skill catalog (${response.status}): ${response.statusText}`);
24
25
  }
25
- const indexText = await response.text();
26
- const skills = parseSkillIndex(indexText);
27
- if (skills.length === 0) {
28
- throw new Error('PostPlus public skill catalog is invalid: no released skills were found.');
29
- }
26
+ const raw = await response.text();
27
+ const payload = parseJsonResponse(raw, POSTPLUS_SKILLS_CATALOG_URL);
28
+ const catalog = parsePublicSkillCatalog(payload);
30
29
  return {
31
- source: POSTPLUS_SKILLS_REPO,
30
+ ...catalog,
31
+ catalogUrl: POSTPLUS_SKILLS_CATALOG_URL,
32
32
  indexUrl: POSTPLUS_SKILLS_INDEX_URL,
33
33
  installCommand: POSTPLUS_SKILLS_INSTALL_COMMAND,
34
34
  listCommand: POSTPLUS_SKILLS_LIST_COMMAND,
35
- skills,
36
35
  };
37
36
  }
38
- function parseSkillIndex(indexText) {
39
- const skills = [];
40
- let inReleasedSkills = false;
41
- let sawReleasedSkillsSection = false;
42
- let currentSkill = null;
43
- for (const line of indexText.split('\n')) {
44
- if (line.trim() === '## Released Skills') {
45
- inReleasedSkills = true;
46
- sawReleasedSkillsSection = true;
47
- continue;
48
- }
49
- if (!inReleasedSkills) {
50
- continue;
37
+ function parseJsonResponse(raw, url) {
38
+ try {
39
+ return JSON.parse(raw);
40
+ }
41
+ catch (error) {
42
+ const trimmed = raw.trimStart();
43
+ if (trimmed.startsWith('<')) {
44
+ throw new Error(`PostPlus public skill catalog returned HTML instead of JSON: ${url}`);
51
45
  }
52
- const skillMatch = line.match(/^- `([^`]+)`\s*$/);
53
- if (skillMatch) {
54
- currentSkill = skillMatch[1] ?? null;
55
- if (currentSkill) {
56
- skills.push({
57
- skillId: currentSkill,
58
- path: null,
59
- });
60
- }
61
- continue;
46
+ throw new Error(error instanceof Error
47
+ ? `PostPlus public skill catalog returned invalid JSON: ${error.message}`
48
+ : 'PostPlus public skill catalog returned invalid JSON.');
49
+ }
50
+ }
51
+ function parsePublicSkillCatalog(payload) {
52
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
53
+ throw new Error('PostPlus public skill catalog is invalid.');
54
+ }
55
+ const record = payload;
56
+ const revision = typeof record.revision === 'string' && record.revision.trim()
57
+ ? record.revision.trim()
58
+ : null;
59
+ const source = typeof record.source === 'string' && record.source.trim()
60
+ ? record.source.trim()
61
+ : null;
62
+ if (record.schemaVersion !== 1 ||
63
+ source !== POSTPLUS_SKILLS_REPO ||
64
+ !revision) {
65
+ throw new Error('PostPlus public skill catalog metadata is invalid.');
66
+ }
67
+ if (!Array.isArray(record.skills)) {
68
+ throw new Error('PostPlus public skill catalog has no skills array.');
69
+ }
70
+ const skills = record.skills.map((value) => {
71
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
72
+ throw new Error('PostPlus public skill catalog has an invalid skill.');
62
73
  }
63
- const pathMatch = line.match(/^\s+- Path: `([^`]+)`\s*$/);
64
- if (pathMatch && currentSkill) {
65
- const last = skills.at(-1);
66
- if (last?.skillId === currentSkill) {
67
- last.path = pathMatch[1] ?? null;
68
- }
74
+ const skill = value;
75
+ const skillId = typeof skill.name === 'string' && skill.name.trim()
76
+ ? skill.name.trim()
77
+ : null;
78
+ const path = typeof skill.path === 'string' && skill.path.trim()
79
+ ? skill.path.trim()
80
+ : null;
81
+ const requirements = skill.requirements &&
82
+ typeof skill.requirements === 'object' &&
83
+ !Array.isArray(skill.requirements)
84
+ ? skill.requirements
85
+ : {};
86
+ const localDependencies = Array.isArray(requirements.localDependencies)
87
+ ? requirements.localDependencies
88
+ .filter((value) => typeof value === 'string')
89
+ .map((value) => value.trim())
90
+ .filter(Boolean)
91
+ : [];
92
+ const status = typeof skill.status === 'string' ? skill.status.trim() : '';
93
+ if (!skillId ||
94
+ !path ||
95
+ !(status === 'released' || status.startsWith('released/'))) {
96
+ throw new Error('PostPlus public skill catalog has an invalid skill.');
69
97
  }
98
+ return {
99
+ localDependencies,
100
+ skillId,
101
+ path,
102
+ };
103
+ });
104
+ if (skills.length === 0) {
105
+ throw new Error('PostPlus public skill catalog is invalid: no released skills were found.');
70
106
  }
71
- if (!sawReleasedSkillsSection) {
72
- throw new Error('PostPlus public skill catalog is invalid: missing ## Released Skills section.');
73
- }
74
- return skills;
107
+ return {
108
+ revision,
109
+ skills,
110
+ source,
111
+ };
75
112
  }
@@ -1,27 +1,56 @@
1
- import { POSTPLUS_SKILLS_AGENT_TARGETS, POSTPLUS_SKILLS_INSTALL_COMMAND, POSTPLUS_SKILLS_REPO, loadPublicSkillCatalog, } from './skill-catalog.js';
2
1
  import { runCommand, runInteractiveCommand } from './command-runner.js';
2
+ import { clearManagedSkillBaseline, readManagedSkillBaseline, writeManagedSkillBaseline, } from './local-state.js';
3
+ import { POSTPLUS_SKILLS_AGENT_TARGETS, POSTPLUS_SKILLS_INSTALL_COMMAND, POSTPLUS_SKILLS_REPO, loadPublicSkillCatalog, } from './skill-catalog.js';
3
4
  const NPX_SKILLS = ['-y', 'skills'];
4
- export async function runPostPlusSkillUpdate() {
5
+ export async function runPostPlusSkillUpdate(dependencies = {
6
+ runInteractiveCommand,
7
+ }) {
5
8
  const catalog = await loadPublicSkillCatalog();
6
9
  const skillNames = catalog.skills.map((skill) => skill.skillId);
10
+ const baseline = await readManagedSkillBaseline();
11
+ const retiredSkillNames = baseline.skillNames.filter((skillName) => !skillNames.includes(skillName));
7
12
  if (skillNames.length === 0) {
8
13
  throw new Error('PostPlus public skill catalog has no released skills.');
9
14
  }
10
- return runInteractiveCommand('npx', buildPostPlusSkillUpdateArgs(skillNames));
15
+ const updateExitCode = await dependencies.runInteractiveCommand('npx', buildPostPlusSkillUpdateArgs(skillNames));
16
+ if (updateExitCode !== 0) {
17
+ return updateExitCode;
18
+ }
19
+ if (retiredSkillNames.length > 0) {
20
+ const removeExitCode = await dependencies.runInteractiveCommand('npx', buildPostPlusSkillUninstallArgs(retiredSkillNames));
21
+ if (removeExitCode !== 0) {
22
+ return removeExitCode;
23
+ }
24
+ }
25
+ await writeManagedSkillBaseline({
26
+ revision: catalog.revision,
27
+ skillNames,
28
+ });
29
+ return 0;
11
30
  }
12
- export async function runPostPlusSkillUninstall() {
31
+ export async function runPostPlusSkillUninstall(dependencies = {
32
+ runInteractiveCommand,
33
+ }) {
13
34
  const catalog = await loadPublicSkillCatalog();
14
35
  const skillNames = catalog.skills.map((skill) => skill.skillId);
15
- if (skillNames.length === 0) {
36
+ const baseline = await readManagedSkillBaseline();
37
+ const allKnownSkillNames = mergeSkillNames(skillNames, baseline.skillNames);
38
+ if (allKnownSkillNames.length === 0) {
16
39
  throw new Error('PostPlus public skill catalog has no released skills.');
17
40
  }
18
- return runInteractiveCommand('npx', buildPostPlusSkillUninstallArgs(skillNames));
41
+ const exitCode = await dependencies.runInteractiveCommand('npx', buildPostPlusSkillUninstallArgs(allKnownSkillNames));
42
+ if (exitCode === 0) {
43
+ await clearManagedSkillBaseline();
44
+ }
45
+ return exitCode;
19
46
  }
20
47
  export async function generateSkillInstallStatusReport(dependencies = {
21
48
  runCommand,
22
49
  }) {
23
50
  const catalog = await loadPublicSkillCatalog();
24
51
  const requiredSkills = new Set(catalog.skills.map((skill) => skill.skillId));
52
+ const baseline = await readManagedSkillBaseline();
53
+ const retiredManagedSkills = baseline.skillNames.filter((skillName) => !requiredSkills.has(skillName));
25
54
  try {
26
55
  const installed = await listInstalledSkills(dependencies);
27
56
  const postPlusInstalled = installed.filter((skill) => requiredSkills.has(skill.name));
@@ -37,8 +66,10 @@ export async function generateSkillInstallStatusReport(dependencies = {
37
66
  error: null,
38
67
  installCommand: POSTPLUS_SKILLS_INSTALL_COMMAND,
39
68
  installedCount: installedNames.size,
69
+ managedRevision: baseline.revision,
40
70
  missingSkills,
41
71
  requiredCount: requiredSkills.size,
72
+ retiredManagedSkills,
42
73
  scopes,
43
74
  source: POSTPLUS_SKILLS_REPO,
44
75
  updateCommand: formatPostPlusSkillUpdateCommand(),
@@ -53,8 +84,10 @@ export async function generateSkillInstallStatusReport(dependencies = {
53
84
  : 'Failed to inspect installed PostPlus skills.',
54
85
  installCommand: POSTPLUS_SKILLS_INSTALL_COMMAND,
55
86
  installedCount: 0,
87
+ managedRevision: baseline.revision,
56
88
  missingSkills: [...requiredSkills],
57
89
  requiredCount: requiredSkills.size,
90
+ retiredManagedSkills,
58
91
  scopes: [],
59
92
  source: POSTPLUS_SKILLS_REPO,
60
93
  updateCommand: formatPostPlusSkillUpdateCommand(),
@@ -74,7 +107,11 @@ export function formatSkillInstallStatusReport(report) {
74
107
  lines.push(`[FAIL] Installed released skills: ${report.installedCount}/${report.requiredCount}`);
75
108
  }
76
109
  lines.push(` Source: ${report.source}`);
110
+ lines.push(` Managed baseline: ${report.managedRevision ?? 'none'}`);
77
111
  lines.push(` Scope: ${report.scopes.length > 0 ? report.scopes.join(', ') : 'none detected'}`);
112
+ if (report.retiredManagedSkills.length > 0) {
113
+ lines.push(` Retired managed skills: ${formatSkillList(report.retiredManagedSkills, 8)}`, ` Cleanup: ${report.updateCommand}`);
114
+ }
78
115
  if (report.missingSkills.length > 0) {
79
116
  lines.push(` Missing: ${formatSkillList(report.missingSkills, 8)}`, ` Fix: ${report.installCommand}`);
80
117
  }
@@ -103,11 +140,12 @@ export function formatPostPlusSkillUpdateCommand() {
103
140
  export function formatPostPlusSkillUninstallCommand() {
104
141
  return 'postplus uninstall';
105
142
  }
143
+ function mergeSkillNames(left, right) {
144
+ return [...new Set([...left, ...right])].sort((a, b) => a.localeCompare(b));
145
+ }
106
146
  async function listInstalledSkills(dependencies) {
107
- const [project, global] = await Promise.all([
108
- listInstalledSkillsForScope(dependencies, []),
109
- listInstalledSkillsForScope(dependencies, ['--global']),
110
- ]);
147
+ const project = await listInstalledSkillsForScope(dependencies, []);
148
+ const global = await listInstalledSkillsForScope(dependencies, ['--global']);
111
149
  const byKey = new Map();
112
150
  for (const skill of [...project, ...global]) {
113
151
  byKey.set(`${skill.scope}:${skill.name}:${skill.path}`, skill);
@@ -151,5 +189,7 @@ function normalizeInstalledSkillEntry(value) {
151
189
  function formatSkillList(skills, limit) {
152
190
  const visible = skills.slice(0, limit);
153
191
  const rest = skills.length - visible.length;
154
- return rest > 0 ? `${visible.join(', ')} (+${rest} more)` : visible.join(', ');
192
+ return rest > 0
193
+ ? `${visible.join(', ')} (+${rest} more)`
194
+ : visible.join(', ');
155
195
  }
package/build/status.js CHANGED
@@ -17,7 +17,8 @@ export async function generateStatusReportWithDependencies(dependencies = {}) {
17
17
  generateUpdateStatus(),
18
18
  ]);
19
19
  return {
20
- ok: doctor.ok && auth.ok && skills.ok && updates.ok,
20
+ schemaVersion: 1,
21
+ ok: doctor.requiredOk && auth.ok && skills.ok && updates.ok,
21
22
  doctor,
22
23
  auth,
23
24
  skills,
@@ -25,10 +26,15 @@ export async function generateStatusReportWithDependencies(dependencies = {}) {
25
26
  };
26
27
  }
27
28
  export function formatStatusReport(report) {
29
+ const taskSpecificChecksNeedAttention = report.doctor.requiredOk && !report.doctor.ok;
28
30
  return [
29
31
  'PostPlus CLI status',
30
32
  '',
31
- `Overall: ${report.ok ? 'OK' : 'INCOMPLETE'}`,
33
+ `Overall: ${report.ok
34
+ ? taskSpecificChecksNeedAttention
35
+ ? 'OK (task-specific checks need attention)'
36
+ : 'OK'
37
+ : 'INCOMPLETE'}`,
32
38
  '',
33
39
  formatDoctorReport(report.doctor),
34
40
  '',
@@ -1,17 +1,16 @@
1
- import { createHash } from 'node:crypto';
2
1
  import { mkdir, readFile, writeFile } from 'node:fs/promises';
3
2
  import { dirname, join } from 'node:path';
4
- import { getPostPlusConfigDir } from './local-state.js';
5
- import { POSTPLUS_SKILLS_REPO } from './skill-catalog.js';
3
+ import { getPostPlusConfigDir, readManagedSkillBaseline, } from './local-state.js';
4
+ import { POSTPLUS_SKILLS_REPO, loadPublicSkillCatalog, } from './skill-catalog.js';
6
5
  const UPDATE_CHECK_TTL_MS = 24 * 60 * 60 * 1000;
7
6
  const UPDATE_CHECK_CACHE_FILE = 'update-check.json';
8
7
  const NPM_PACKAGE_NAME = '@postplus/cli';
9
8
  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
9
  export async function generateUpdateStatusReport(input = {}, dependencies = {
12
10
  fetchFn: fetch,
13
11
  }) {
14
12
  const currentVersion = await readCurrentCliVersion();
13
+ const managedSkillBaseline = await readManagedSkillBaseline();
15
14
  const cache = await readUpdateCheckCache();
16
15
  if (cache &&
17
16
  !input.force &&
@@ -20,7 +19,7 @@ export async function generateUpdateStatusReport(input = {}, dependencies = {
20
19
  return buildUpdateReport({
21
20
  cache,
22
21
  currentVersion,
23
- previousSkillRevision: cache.skills.latestRevision,
22
+ currentSkillRevision: managedSkillBaseline.revision,
24
23
  source: 'cache',
25
24
  });
26
25
  }
@@ -43,9 +42,7 @@ export async function generateUpdateStatusReport(input = {}, dependencies = {
43
42
  return buildUpdateReport({
44
43
  cache: nextCache,
45
44
  currentVersion,
46
- previousSkillRevision: input.resetSkillBaseline
47
- ? latestSkillRevision
48
- : cache?.skills.latestRevision ?? latestSkillRevision,
45
+ currentSkillRevision: managedSkillBaseline.revision,
49
46
  source: 'remote',
50
47
  });
51
48
  }
@@ -56,7 +53,7 @@ export async function generateUpdateStatusReport(input = {}, dependencies = {
56
53
  ...buildUpdateReport({
57
54
  cache,
58
55
  currentVersion,
59
- previousSkillRevision: cache.skills.latestRevision,
56
+ currentSkillRevision: managedSkillBaseline.revision,
60
57
  source: 'cache',
61
58
  }),
62
59
  warning,
@@ -73,7 +70,7 @@ export async function generateUpdateStatusReport(input = {}, dependencies = {
73
70
  updateCommand: 'npm install -g @postplus/cli',
74
71
  },
75
72
  skills: {
76
- currentRevision: null,
73
+ currentRevision: managedSkillBaseline.revision,
77
74
  latestRevision: null,
78
75
  updateAvailable: false,
79
76
  updateCommand: 'postplus update',
@@ -82,10 +79,9 @@ export async function generateUpdateStatusReport(input = {}, dependencies = {
82
79
  };
83
80
  }
84
81
  }
85
- export async function refreshUpdateCheckBaseline() {
82
+ export async function refreshUpdateCheckCache() {
86
83
  await generateUpdateStatusReport({
87
84
  force: true,
88
- resetSkillBaseline: true,
89
85
  });
90
86
  }
91
87
  export function formatUpdateStatusReport(report) {
@@ -116,19 +112,20 @@ function buildUpdateReport(input) {
116
112
  cli: {
117
113
  currentVersion: input.currentVersion,
118
114
  latestVersion: input.cache.cli.latestVersion,
119
- updateAvailable: compareVersions(input.cache.cli.latestVersion, input.currentVersion) > 0,
115
+ updateAvailable: compareVersions(input.cache.cli.latestVersion, input.currentVersion) >
116
+ 0,
120
117
  updateCommand: 'npm install -g @postplus/cli',
121
118
  },
122
119
  skills: {
123
- currentRevision: input.previousSkillRevision,
120
+ currentRevision: input.currentSkillRevision,
124
121
  latestRevision: input.cache.skills.latestRevision,
125
- updateAvailable: input.cache.skills.latestRevision !== input.previousSkillRevision,
122
+ updateAvailable: input.cache.skills.latestRevision !== input.currentSkillRevision,
126
123
  updateCommand: 'postplus update',
127
124
  },
128
125
  warning: null,
129
126
  };
130
127
  }
131
- async function readCurrentCliVersion() {
128
+ export async function readCurrentCliVersion() {
132
129
  const packageJsonPath = new URL('../package.json', import.meta.url);
133
130
  const raw = await readFile(packageJsonPath, 'utf8');
134
131
  const parsed = JSON.parse(raw);
@@ -155,23 +152,12 @@ async function fetchLatestCliVersion(fetchFn) {
155
152
  return payload.version.trim();
156
153
  }
157
154
  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}).`);
155
+ try {
156
+ return (await loadPublicSkillCatalog(fetchFn)).revision;
167
157
  }
168
- const payload = (await response.json());
169
- if (typeof payload.sha === 'string' && payload.sha.trim()) {
170
- return payload.sha.trim();
158
+ catch (error) {
159
+ throw new Error(`Failed to check latest ${POSTPLUS_SKILLS_REPO} revision: ${error instanceof Error ? error.message : String(error)}`);
171
160
  }
172
- return createHash('sha256')
173
- .update(JSON.stringify(payload))
174
- .digest('hex');
175
161
  }
176
162
  async function readUpdateCheckCache() {
177
163
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@postplus/cli",
3
- "version": "0.1.18",
3
+ "version": "0.1.20",
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.",
@@ -15,6 +15,7 @@
15
15
  "build/doctor.js",
16
16
  "build/hosted-release.js",
17
17
  "build/index.js",
18
+ "build/local-dependencies.js",
18
19
  "build/local-state.js",
19
20
  "build/skill-catalog.js",
20
21
  "build/skill-management.js",