@postplus/cli 0.1.37 → 0.1.39
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/build/hosted-domain-commands.js +13 -5
- package/build/hosted-schema-catalog.js +27 -47
- package/build/index.js +51 -3
- package/build/quote-confirmation.js +78 -1
- package/build/skill-management.js +131 -6
- package/package.json +1 -1
|
@@ -17,10 +17,7 @@ const HOSTED_DOMAIN_CAPABILITIES = {
|
|
|
17
17
|
media: new Set(['media-file', 'media-generation', 'video-analysis']),
|
|
18
18
|
mobile: new Set(['mobile-automation']),
|
|
19
19
|
publish: new Set(['social-publishing']),
|
|
20
|
-
research: new Set([
|
|
21
|
-
'public-content-collection',
|
|
22
|
-
'public-content-discovery',
|
|
23
|
-
]),
|
|
20
|
+
research: new Set(['public-content-collection', 'public-content-discovery']),
|
|
24
21
|
};
|
|
25
22
|
export async function runHostedDomainCommand(domain, args) {
|
|
26
23
|
const [subcommand, ...rest] = args;
|
|
@@ -69,6 +66,16 @@ async function runResearchCollect(args) {
|
|
|
69
66
|
`postplus-cli:research:${collectionKey}:${randomUUID()}`;
|
|
70
67
|
const quoteConfirmationToken = flags.values.get('quote-confirmation-token') ??
|
|
71
68
|
normalizeString(envelope.quoteConfirmationToken);
|
|
69
|
+
// Optional per-request cost ceiling (USD) overriding the hosted default.
|
|
70
|
+
const maxChargeFlag = flags.values.get('max-charge-usd');
|
|
71
|
+
let maxTotalChargeUsd;
|
|
72
|
+
if (maxChargeFlag !== undefined) {
|
|
73
|
+
const parsed = Number(maxChargeFlag);
|
|
74
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
75
|
+
throw new Error('--max-charge-usd must be a positive number of USD.');
|
|
76
|
+
}
|
|
77
|
+
maxTotalChargeUsd = parsed;
|
|
78
|
+
}
|
|
72
79
|
const payload = await postHostedJson({
|
|
73
80
|
body: {
|
|
74
81
|
collectionKey,
|
|
@@ -76,6 +83,7 @@ async function runResearchCollect(args) {
|
|
|
76
83
|
operationId,
|
|
77
84
|
quoteConfirmationToken: quoteConfirmationToken ?? undefined,
|
|
78
85
|
skillName,
|
|
86
|
+
maxTotalChargeUsd,
|
|
79
87
|
},
|
|
80
88
|
pathName: '/api/postplus-cli/hosted/collection',
|
|
81
89
|
skillName,
|
|
@@ -348,7 +356,7 @@ function printResearchHelp() {
|
|
|
348
356
|
|
|
349
357
|
Usage:
|
|
350
358
|
postplus research schema [--collection-key <key>] [--json]
|
|
351
|
-
postplus research collect --skill <skill-id> --collection-key <key> --input <hosted-envelope.json> [--output <result.json>]
|
|
359
|
+
postplus research collect --skill <skill-id> --collection-key <key> --input <hosted-envelope.json> [--max-charge-usd <usd>] [--output <result.json>]
|
|
352
360
|
postplus research collect --run-handle <runHandle> [--output <result.json>]
|
|
353
361
|
postplus research capability --request <hosted-capability-request.json> [--output <result.json>]
|
|
354
362
|
`);
|
|
@@ -1,87 +1,72 @@
|
|
|
1
1
|
// Generated from the PostPlus Cloud hosted catalog release gate.
|
|
2
2
|
// Keep keys in sync with apps/web hosted capability and collection catalogs.
|
|
3
|
+
//
|
|
4
|
+
// The result-count field in each hint is a fetch-volume example using the actor's
|
|
5
|
+
// REAL input field (it shapes how much the actor fetches, not the cost ceiling).
|
|
6
|
+
// Total spend is bounded server-side by a per-request USD budget — pay-per-event
|
|
7
|
+
// actors via Apify maxTotalChargeUsd, Bright Data Facebook via
|
|
8
|
+
// limit_multiple_results — so these values are starting points, not the cap.
|
|
9
|
+
// Field names matter: clockworks TikTok actors fetch per resultsPerPage /
|
|
10
|
+
// maxProfilesPerQuery / commentsPerPost (maxItems is a run option they ignore as
|
|
11
|
+
// input) and apidojo youtube-comments uses maxItems (not maxComments).
|
|
3
12
|
export const RESEARCH_COLLECTION_HINTS = {
|
|
4
|
-
'amazon-asins': {
|
|
5
|
-
asins: ['B0C1234567'],
|
|
6
|
-
country: 'US',
|
|
7
|
-
},
|
|
8
|
-
'amazon-bestsellers': {
|
|
9
|
-
categoryUrl: 'https://www.amazon.com/Best-Sellers/zgbs',
|
|
10
|
-
maxItems: 5,
|
|
11
|
-
},
|
|
12
|
-
'amazon-free-products': {
|
|
13
|
-
keyword: 'portable blender',
|
|
14
|
-
maxItems: 5,
|
|
15
|
-
},
|
|
16
|
-
'amazon-products': {
|
|
17
|
-
country: 'US',
|
|
18
|
-
keyword: 'portable blender',
|
|
19
|
-
maxItems: 5,
|
|
20
|
-
},
|
|
21
|
-
'amazon-reviews': {
|
|
22
|
-
asin: 'B0C1234567',
|
|
23
|
-
country: 'US',
|
|
24
|
-
maxReviews: 10,
|
|
25
|
-
},
|
|
26
|
-
'amazon-reviews-v2': {
|
|
27
|
-
asin: 'B0C1234567',
|
|
28
|
-
domainCode: 'com',
|
|
29
|
-
maxReviews: 10,
|
|
30
|
-
},
|
|
31
13
|
'google-trends-fast': {
|
|
32
|
-
|
|
14
|
+
enableTrendingSearches: false,
|
|
15
|
+
geo: 'US',
|
|
16
|
+
keyword: 'portable blender',
|
|
17
|
+
timeframe: 'today 12-m',
|
|
33
18
|
},
|
|
34
19
|
'instagram-comments': {
|
|
35
20
|
directUrls: ['https://www.instagram.com/p/example/'],
|
|
36
|
-
resultsLimit:
|
|
21
|
+
resultsLimit: 20,
|
|
37
22
|
},
|
|
38
23
|
'instagram-email-search': {
|
|
39
24
|
Country: 'www',
|
|
40
25
|
Email_Type: '0',
|
|
41
26
|
Keyword: 'skincare creator',
|
|
42
|
-
Limit: '
|
|
27
|
+
Limit: '25',
|
|
43
28
|
social_network: 'instagram.com/',
|
|
44
29
|
},
|
|
45
30
|
'instagram-hashtags': {
|
|
46
31
|
hashtags: ['desksetup'],
|
|
47
|
-
resultsLimit:
|
|
32
|
+
resultsLimit: 10,
|
|
48
33
|
},
|
|
49
34
|
'instagram-posts': {
|
|
50
|
-
resultsLimit:
|
|
35
|
+
resultsLimit: 12,
|
|
51
36
|
username: ['openai'],
|
|
52
37
|
},
|
|
53
38
|
'instagram-profiles': {
|
|
54
|
-
resultsLimit: 3,
|
|
55
39
|
usernames: ['instagram'],
|
|
56
40
|
},
|
|
57
41
|
'instagram-search': {
|
|
58
|
-
searchLimit:
|
|
42
|
+
searchLimit: 10,
|
|
59
43
|
searchTerms: ['skincare routine'],
|
|
60
44
|
searchType: 'user',
|
|
61
45
|
},
|
|
62
46
|
'tiktok-ads-top': {
|
|
63
47
|
include_analytics: true,
|
|
64
|
-
limit:
|
|
48
|
+
limit: 20,
|
|
65
49
|
},
|
|
66
50
|
'tiktok-comments': {
|
|
51
|
+
commentsPerPost: 20,
|
|
67
52
|
postURLs: ['https://www.tiktok.com/@example/video/1234567890'],
|
|
68
|
-
resultsPerPage: 5,
|
|
69
53
|
},
|
|
70
54
|
'tiktok-profiles': {
|
|
55
|
+
resultsPerPage: 12,
|
|
71
56
|
usernames: ['tiktok'],
|
|
72
57
|
},
|
|
73
58
|
'tiktok-related-videos': {
|
|
74
|
-
maxItems: 3,
|
|
75
59
|
postURLs: ['https://www.tiktok.com/@example/video/1234567890'],
|
|
60
|
+
resultsPerPage: 10,
|
|
76
61
|
},
|
|
77
62
|
'tiktok-users': {
|
|
78
|
-
|
|
63
|
+
maxProfilesPerQuery: 10,
|
|
79
64
|
searchQueries: ['skincare creator'],
|
|
80
65
|
},
|
|
81
66
|
'tiktok-videos': {
|
|
82
|
-
maxItems: 3,
|
|
83
67
|
proxyCountryCode: 'US',
|
|
84
68
|
queries: ['portable blender'],
|
|
69
|
+
resultsPerPage: 10,
|
|
85
70
|
searchSection: '/video',
|
|
86
71
|
},
|
|
87
72
|
'youtube-channel-summary': {
|
|
@@ -91,23 +76,17 @@ export const RESEARCH_COLLECTION_HINTS = {
|
|
|
91
76
|
maxVideosPerChannel: 0,
|
|
92
77
|
},
|
|
93
78
|
'youtube-comments': {
|
|
94
|
-
|
|
79
|
+
maxItems: 50,
|
|
95
80
|
startUrls: ['https://www.youtube.com/watch?v=dQw4w9WgXcQ'],
|
|
96
81
|
},
|
|
97
82
|
'youtube-video-download': {
|
|
98
83
|
urls: ['https://www.youtube.com/watch?v=dQw4w9WgXcQ'],
|
|
99
84
|
},
|
|
100
|
-
'x-posts': {
|
|
101
|
-
maxItems: 5,
|
|
102
|
-
searchTerms: ['product launch'],
|
|
103
|
-
},
|
|
104
|
-
'x-profiles': {
|
|
105
|
-
handles: ['OpenAI'],
|
|
106
|
-
},
|
|
107
85
|
};
|
|
108
86
|
export const PUBLIC_CONTENT_SOURCE_HINTS = {
|
|
109
87
|
'facebook-group-posts': [
|
|
110
88
|
{
|
|
89
|
+
num_of_posts: 25,
|
|
111
90
|
url: 'https://www.facebook.com/groups/example',
|
|
112
91
|
},
|
|
113
92
|
],
|
|
@@ -118,6 +97,7 @@ export const PUBLIC_CONTENT_SOURCE_HINTS = {
|
|
|
118
97
|
],
|
|
119
98
|
'facebook-profile-posts': [
|
|
120
99
|
{
|
|
100
|
+
num_of_posts: 25,
|
|
121
101
|
url: 'https://www.facebook.com/openai',
|
|
122
102
|
},
|
|
123
103
|
],
|
package/build/index.js
CHANGED
|
@@ -9,7 +9,7 @@ import { readCurrentCliVersion } from './client-compatibility.js';
|
|
|
9
9
|
import { formatDoctorReport, generateDoctorReport } from './doctor.js';
|
|
10
10
|
import { runHostedDomainCommand } from './hosted-domain-commands.js';
|
|
11
11
|
import { assertConfigFilePermissions } from './local-state.js';
|
|
12
|
-
import { readLargeCreditQuoteConfirmationChallenge, resolveLargeCreditQuoteConfirmation, } from './quote-confirmation.js';
|
|
12
|
+
import { QUOTE_AUTO_CONFIRM_UNDER_ENV, QuoteAutoConfirmCeilingExceededError, QuoteConfirmationNonInteractiveError, confirmLargeCreditQuote, readLargeCreditQuoteConfirmationChallenge, resolveLargeCreditQuoteConfirmation, } from './quote-confirmation.js';
|
|
13
13
|
import { POSTPLUS_SKILLS_CURRENT_DIRECTORY_INSTALL_COMMAND, POSTPLUS_SKILLS_INSTALL_COMMAND, formatPostPlusSkillsInstallCommand, loadPublicSkillCatalog, } from './skill-catalog.js';
|
|
14
14
|
import { formatSkillBaselineVerifyReport, runPostPlusSkillUninstall, runPostPlusSkillUpdate, runPostPlusSkillVerify, } from './skill-management.js';
|
|
15
15
|
import { formatStatusReport, generateStatusReport } from './status.js';
|
|
@@ -51,7 +51,7 @@ Usage:
|
|
|
51
51
|
postplus publish capability --request <hosted-capability-request.json> [--output <result.json>]
|
|
52
52
|
postplus mobile schema [--json]
|
|
53
53
|
postplus mobile capability --request <hosted-capability-request.json> [--output <result.json>]
|
|
54
|
-
postplus quote confirm --json --challenge-file <path>
|
|
54
|
+
postplus quote confirm --json --challenge-file <path> [--auto-confirm-under <millicredits>]
|
|
55
55
|
postplus skills verify [--json]
|
|
56
56
|
postplus studio init|open|status Open bundled Local Studio
|
|
57
57
|
postplus update [--current-directory]
|
|
@@ -208,11 +208,46 @@ async function runQuoteCommand(rest) {
|
|
|
208
208
|
process.stderr.write('Invalid large credit quote confirmation challenge.\n');
|
|
209
209
|
return 1;
|
|
210
210
|
}
|
|
211
|
-
|
|
211
|
+
const ceilingMillicredits = resolveQuoteAutoConfirmCeiling(parsed.autoConfirmUnder);
|
|
212
|
+
try {
|
|
213
|
+
writeJson(await resolveLargeCreditQuoteConfirmation(challenge, {
|
|
214
|
+
confirm: confirmLargeCreditQuote,
|
|
215
|
+
ceilingMillicredits,
|
|
216
|
+
}));
|
|
217
|
+
}
|
|
218
|
+
catch (error) {
|
|
219
|
+
if (error instanceof QuoteAutoConfirmCeilingExceededError ||
|
|
220
|
+
error instanceof QuoteConfirmationNonInteractiveError) {
|
|
221
|
+
process.stderr.write(`${error.message}\n`);
|
|
222
|
+
return 1;
|
|
223
|
+
}
|
|
224
|
+
throw error;
|
|
225
|
+
}
|
|
212
226
|
return 0;
|
|
213
227
|
}
|
|
228
|
+
/**
|
|
229
|
+
* Resolves the bounded auto-confirm ceiling in millicredits. Precedence:
|
|
230
|
+
* explicit --auto-confirm-under flag, then the
|
|
231
|
+
* POSTPLUS_QUOTE_AUTO_CONFIRM_UNDER_MILLICREDITS env var. Returns null when
|
|
232
|
+
* neither is set, leaving today's interactive behavior unchanged.
|
|
233
|
+
*/
|
|
234
|
+
function resolveQuoteAutoConfirmCeiling(flagValue) {
|
|
235
|
+
if (flagValue !== null) {
|
|
236
|
+
return flagValue;
|
|
237
|
+
}
|
|
238
|
+
const envValue = process.env[QUOTE_AUTO_CONFIRM_UNDER_ENV];
|
|
239
|
+
if (envValue === undefined || envValue.trim() === '') {
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
const parsed = Number(envValue);
|
|
243
|
+
if (!Number.isFinite(parsed) || parsed < 0) {
|
|
244
|
+
throw new Error(`Invalid ${QUOTE_AUTO_CONFIRM_UNDER_ENV}: expected a non-negative number of millicredits.`);
|
|
245
|
+
}
|
|
246
|
+
return parsed;
|
|
247
|
+
}
|
|
214
248
|
function parseQuoteConfirmOptions(args) {
|
|
215
249
|
const options = {
|
|
250
|
+
autoConfirmUnder: null,
|
|
216
251
|
challengeFile: null,
|
|
217
252
|
json: false,
|
|
218
253
|
};
|
|
@@ -231,6 +266,19 @@ function parseQuoteConfirmOptions(args) {
|
|
|
231
266
|
index += 1;
|
|
232
267
|
continue;
|
|
233
268
|
}
|
|
269
|
+
if (arg === '--auto-confirm-under') {
|
|
270
|
+
const rawValue = args[index + 1];
|
|
271
|
+
if (!rawValue || rawValue.startsWith('--')) {
|
|
272
|
+
throw new Error('Missing value for --auto-confirm-under.');
|
|
273
|
+
}
|
|
274
|
+
const value = Number(rawValue);
|
|
275
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
276
|
+
throw new Error('Invalid value for --auto-confirm-under: expected a non-negative number of millicredits.');
|
|
277
|
+
}
|
|
278
|
+
options.autoConfirmUnder = value;
|
|
279
|
+
index += 1;
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
234
282
|
throw new Error(`Unknown option for quote confirm: ${arg}`);
|
|
235
283
|
}
|
|
236
284
|
return options;
|
|
@@ -1,6 +1,50 @@
|
|
|
1
1
|
import readline from 'node:readline/promises';
|
|
2
2
|
import { readLocalConfig, updateLocalConfig } from './local-state.js';
|
|
3
3
|
const PRODUCT_ERROR_CODE = 'postplus_cli_quote_confirmation_required';
|
|
4
|
+
export const QUOTE_AUTO_CONFIRM_CEILING_EXCEEDED_CODE = 'postplus_cli_quote_auto_confirm_ceiling_exceeded';
|
|
5
|
+
export const QUOTE_AUTO_CONFIRM_UNDER_ENV = 'POSTPLUS_QUOTE_AUTO_CONFIRM_UNDER_MILLICREDITS';
|
|
6
|
+
/**
|
|
7
|
+
* Thrown when a bounded auto-confirm ceiling is configured but the challenge
|
|
8
|
+
* cost exceeds it. Carries the original challenge so an orchestrator or human
|
|
9
|
+
* can confirm explicitly instead of the CLI hanging on a readline prompt.
|
|
10
|
+
*/
|
|
11
|
+
export class QuoteAutoConfirmCeilingExceededError extends Error {
|
|
12
|
+
code = QUOTE_AUTO_CONFIRM_CEILING_EXCEEDED_CODE;
|
|
13
|
+
challenge;
|
|
14
|
+
ceilingMillicredits;
|
|
15
|
+
costMillicredits;
|
|
16
|
+
constructor(input) {
|
|
17
|
+
super(`Quote cost ${input.costMillicredits} millicredits exceeds the ` +
|
|
18
|
+
`auto-confirm ceiling of ${input.ceilingMillicredits} millicredits. ` +
|
|
19
|
+
'Confirm explicitly or raise --auto-confirm-under / ' +
|
|
20
|
+
`${QUOTE_AUTO_CONFIRM_UNDER_ENV}.`);
|
|
21
|
+
this.name = 'QuoteAutoConfirmCeilingExceededError';
|
|
22
|
+
this.challenge = input.challenge;
|
|
23
|
+
this.ceilingMillicredits = input.ceilingMillicredits;
|
|
24
|
+
this.costMillicredits = input.costMillicredits;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Thrown when no auto-confirm ceiling is configured and stdin is not a TTY, so
|
|
29
|
+
* the interactive readline prompt would hang. Fails fast with an actionable
|
|
30
|
+
* message instead.
|
|
31
|
+
*/
|
|
32
|
+
export class QuoteConfirmationNonInteractiveError extends Error {
|
|
33
|
+
code = 'postplus_cli_quote_confirmation_non_interactive';
|
|
34
|
+
challenge;
|
|
35
|
+
constructor(challenge) {
|
|
36
|
+
super('Quote confirmation required but stdin is not a TTY and no auto-confirm ' +
|
|
37
|
+
'ceiling is configured. Pass --auto-confirm-under <millicredits>, set ' +
|
|
38
|
+
`${QUOTE_AUTO_CONFIRM_UNDER_ENV}, or run interactively.`);
|
|
39
|
+
this.name = 'QuoteConfirmationNonInteractiveError';
|
|
40
|
+
this.challenge = challenge;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function resolveChallengeCostMillicredits(challenge) {
|
|
44
|
+
return typeof challenge.estimatedMillicredits === 'number'
|
|
45
|
+
? challenge.estimatedMillicredits
|
|
46
|
+
: challenge.requiredTierMillicredits;
|
|
47
|
+
}
|
|
4
48
|
export function readLargeCreditQuoteConfirmationChallenge(value) {
|
|
5
49
|
if (!value || typeof value !== 'object') {
|
|
6
50
|
return null;
|
|
@@ -55,7 +99,7 @@ export async function resolveLargeCreditQuoteConfirmation(challenge, dependencie
|
|
|
55
99
|
}) {
|
|
56
100
|
const acknowledgedTierMillicredits = await readAcknowledgedTierMillicredits(challenge);
|
|
57
101
|
if (acknowledgedTierMillicredits < challenge.requiredTierMillicredits) {
|
|
58
|
-
await
|
|
102
|
+
await runQuoteConfirmation(challenge, dependencies);
|
|
59
103
|
await writeAcknowledgedTierMillicredits(challenge);
|
|
60
104
|
}
|
|
61
105
|
return {
|
|
@@ -63,6 +107,39 @@ export async function resolveLargeCreditQuoteConfirmation(challenge, dependencie
|
|
|
63
107
|
token: challenge.token,
|
|
64
108
|
};
|
|
65
109
|
}
|
|
110
|
+
async function runQuoteConfirmation(challenge, dependencies) {
|
|
111
|
+
const ceiling = dependencies.ceilingMillicredits;
|
|
112
|
+
if (typeof ceiling === 'number' && Number.isFinite(ceiling)) {
|
|
113
|
+
const cost = resolveChallengeCostMillicredits(challenge);
|
|
114
|
+
if (cost > ceiling) {
|
|
115
|
+
throw new QuoteAutoConfirmCeilingExceededError({
|
|
116
|
+
challenge,
|
|
117
|
+
ceilingMillicredits: ceiling,
|
|
118
|
+
costMillicredits: cost,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
const log = dependencies.logNotice ?? defaultLogNotice;
|
|
122
|
+
const now = (dependencies.now ?? (() => new Date()))();
|
|
123
|
+
log(JSON.stringify({
|
|
124
|
+
event: 'quote_auto_confirm',
|
|
125
|
+
timestamp: now.toISOString(),
|
|
126
|
+
accountId: challenge.accountId,
|
|
127
|
+
operationId: challenge.operationId,
|
|
128
|
+
costMillicredits: cost,
|
|
129
|
+
ceilingMillicredits: ceiling,
|
|
130
|
+
requiredTierMillicredits: challenge.requiredTierMillicredits,
|
|
131
|
+
}));
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
const isTty = dependencies.isTty ?? (() => Boolean(process.stdin.isTTY));
|
|
135
|
+
if (!isTty()) {
|
|
136
|
+
throw new QuoteConfirmationNonInteractiveError(challenge);
|
|
137
|
+
}
|
|
138
|
+
await dependencies.confirm(challenge);
|
|
139
|
+
}
|
|
140
|
+
function defaultLogNotice(line) {
|
|
141
|
+
process.stderr.write(`${line}\n`);
|
|
142
|
+
}
|
|
66
143
|
export async function confirmLargeCreditQuote(challenge) {
|
|
67
144
|
const terminal = readline.createInterface({
|
|
68
145
|
input: process.stdin,
|
|
@@ -1,9 +1,15 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
1
4
|
import { writeCurrentCliVersionToLocalConfig } from './client-compatibility.js';
|
|
2
5
|
import { runCommand, runInteractiveCommand } from './command-runner.js';
|
|
3
6
|
import { clearManagedSkillBaseline, readManagedSkillBaseline, writeManagedSkillBaseline, } from './local-state.js';
|
|
4
7
|
import { POSTPLUS_SKILLS_AGENT_TARGETS, formatPostPlusSkillsInstallCommand, resolvePostPlusSkillsSource, loadPublicSkillCatalog, } from './skill-catalog.js';
|
|
5
8
|
import { clearUpdateCheckCache } from './update-check.js';
|
|
6
9
|
const NPX_SKILLS = ['-y', 'skills'];
|
|
10
|
+
const SKILLS_INSTALLER_GLOBAL_LOCK_PATH = ['.agents', '.skill-lock.json'];
|
|
11
|
+
const SKILLS_INSTALLER_PROJECT_LOCK_PATH = 'skills-lock.json';
|
|
12
|
+
const SKILLS_INSTALLER_POSTPLUS_SOURCE = 'postplusai/postplus-skills';
|
|
7
13
|
const DEFAULT_SKILL_MUTATION_OPTIONS = {
|
|
8
14
|
scope: 'global',
|
|
9
15
|
};
|
|
@@ -12,8 +18,10 @@ export async function runPostPlusSkillUpdate(dependencies = {
|
|
|
12
18
|
}, options = DEFAULT_SKILL_MUTATION_OPTIONS) {
|
|
13
19
|
const catalog = await loadPublicSkillCatalog();
|
|
14
20
|
const skillNames = catalog.skills.map((skill) => skill.skillId);
|
|
21
|
+
const releasedSkills = new Set(skillNames);
|
|
15
22
|
const baseline = await readManagedSkillBaseline();
|
|
16
|
-
const
|
|
23
|
+
const lockedSkillNames = await readPostPlusInstallerLockedSkillEntries(options.scope).then((entries) => entries.map((entry) => entry.name));
|
|
24
|
+
const retiredSkillNames = mergeSkillNames(baseline.skillNames, lockedSkillNames).filter((skillName) => !releasedSkills.has(skillName));
|
|
17
25
|
if (skillNames.length === 0) {
|
|
18
26
|
throw new Error('PostPlus public skill catalog has no released skills.');
|
|
19
27
|
}
|
|
@@ -45,7 +53,8 @@ export async function runPostPlusSkillUninstall(dependencies = {
|
|
|
45
53
|
const catalog = await loadPublicSkillCatalog();
|
|
46
54
|
const skillNames = catalog.skills.map((skill) => skill.skillId);
|
|
47
55
|
const baseline = await readManagedSkillBaseline();
|
|
48
|
-
const
|
|
56
|
+
const lockedSkillNames = await readPostPlusInstallerLockedSkillEntries(options.scope).then((entries) => entries.map((entry) => entry.name));
|
|
57
|
+
const allKnownSkillNames = mergeSkillNames(mergeSkillNames(skillNames, baseline.skillNames), lockedSkillNames);
|
|
49
58
|
if (allKnownSkillNames.length === 0) {
|
|
50
59
|
throw new Error('PostPlus public skill catalog has no released skills.');
|
|
51
60
|
}
|
|
@@ -96,9 +105,20 @@ async function inspectPostPlusSkillInstall(dependencies, options = {}) {
|
|
|
96
105
|
const requiredSkillNames = catalog.skills.map((skill) => skill.skillId);
|
|
97
106
|
const requiredSkills = new Set(requiredSkillNames);
|
|
98
107
|
const baseline = await readManagedSkillBaseline();
|
|
99
|
-
const
|
|
108
|
+
const baselineRetiredManagedSkills = baseline.skillNames.filter((skillName) => !requiredSkills.has(skillName));
|
|
100
109
|
try {
|
|
101
110
|
const installed = await listInstalledSkills(dependencies);
|
|
111
|
+
const baselineRetiredSkills = new Set(baselineRetiredManagedSkills);
|
|
112
|
+
const lockedSkills = new Set((await readPostPlusInstallerLockedSkillEntries()).map((entry) => `${entry.scope}:${entry.name}`));
|
|
113
|
+
const installedRetiredManagedSkills = [
|
|
114
|
+
...new Set(installed
|
|
115
|
+
.filter((skill) => baselineRetiredSkills.has(skill.name) ||
|
|
116
|
+
lockedSkills.has(`${skill.scope}:${skill.name}`))
|
|
117
|
+
.map((skill) => skill.name)),
|
|
118
|
+
]
|
|
119
|
+
.filter((skillName) => !requiredSkills.has(skillName))
|
|
120
|
+
.sort((a, b) => a.localeCompare(b));
|
|
121
|
+
const retiredManagedSkills = mergeSkillNames(baselineRetiredManagedSkills, installedRetiredManagedSkills);
|
|
102
122
|
const postPlusInstalled = installed.filter((skill) => requiredSkills.has(skill.name));
|
|
103
123
|
const installedNames = new Set(postPlusInstalled.map((skill) => skill.name));
|
|
104
124
|
const missingSkills = [...requiredSkills].filter((skill) => !installedNames.has(skill));
|
|
@@ -110,7 +130,8 @@ async function inspectPostPlusSkillInstall(dependencies, options = {}) {
|
|
|
110
130
|
baseline,
|
|
111
131
|
releaseId: catalog.releaseId,
|
|
112
132
|
skillNames: requiredSkillNames,
|
|
113
|
-
})
|
|
133
|
+
}) &&
|
|
134
|
+
installedRetiredManagedSkills.length === 0) {
|
|
114
135
|
await writeManagedSkillBaseline({
|
|
115
136
|
releaseId: catalog.releaseId,
|
|
116
137
|
skillNames: requiredSkillNames,
|
|
@@ -128,7 +149,8 @@ async function inspectPostPlusSkillInstall(dependencies, options = {}) {
|
|
|
128
149
|
return {
|
|
129
150
|
catalog,
|
|
130
151
|
report: {
|
|
131
|
-
ok: missingSkills.length === 0
|
|
152
|
+
ok: missingSkills.length === 0 &&
|
|
153
|
+
installedRetiredManagedSkills.length === 0,
|
|
132
154
|
error: null,
|
|
133
155
|
installCommand: formatPostPlusSkillsInstallCommand(catalog.source),
|
|
134
156
|
installedCount: installedNames.size,
|
|
@@ -157,7 +179,7 @@ async function inspectPostPlusSkillInstall(dependencies, options = {}) {
|
|
|
157
179
|
managedSkillsReleaseId: baseline.releaseId,
|
|
158
180
|
missingSkills: [...requiredSkills],
|
|
159
181
|
requiredCount: requiredSkills.size,
|
|
160
|
-
retiredManagedSkills,
|
|
182
|
+
retiredManagedSkills: baselineRetiredManagedSkills,
|
|
161
183
|
scopes: [],
|
|
162
184
|
source: catalog.source,
|
|
163
185
|
updateCommand: formatPostPlusSkillUpdateCommand(),
|
|
@@ -212,6 +234,9 @@ export function formatSkillBaselineVerifyReport(report) {
|
|
|
212
234
|
else {
|
|
213
235
|
lines.push(' Verified baseline: unchanged');
|
|
214
236
|
}
|
|
237
|
+
if (report.retiredManagedSkills.length > 0) {
|
|
238
|
+
lines.push(` Retired managed skills: ${formatSkillList(report.retiredManagedSkills, 8)}`, ` Cleanup (global): ${report.updateCommand}`, ` Cleanup (current directory): ${formatPostPlusSkillUpdateCommand('current-directory')}`);
|
|
239
|
+
}
|
|
215
240
|
if (report.missingSkills.length > 0) {
|
|
216
241
|
lines.push(` Missing: ${formatSkillList(report.missingSkills, 8)}`, ` Fix (global): ${report.installCommand}`, ` Fix (current directory): ${formatPostPlusSkillsInstallCommand(report.source, 'current-directory')}`);
|
|
217
242
|
}
|
|
@@ -274,6 +299,106 @@ function haveSameSkillNames(left, right) {
|
|
|
274
299
|
return (normalizedLeft.length === normalizedRight.length &&
|
|
275
300
|
normalizedLeft.every((value, index) => value === normalizedRight[index]));
|
|
276
301
|
}
|
|
302
|
+
async function readPostPlusInstallerLockedSkillEntries(scope) {
|
|
303
|
+
const lockPaths = scope === 'global'
|
|
304
|
+
? [{ path: getSkillsInstallerGlobalLockPath(), scope: 'global' }]
|
|
305
|
+
: scope === 'current-directory'
|
|
306
|
+
? [
|
|
307
|
+
{
|
|
308
|
+
path: getSkillsInstallerProjectLockPath(),
|
|
309
|
+
scope: 'project',
|
|
310
|
+
},
|
|
311
|
+
]
|
|
312
|
+
: [
|
|
313
|
+
{
|
|
314
|
+
path: getSkillsInstallerProjectLockPath(),
|
|
315
|
+
scope: 'project',
|
|
316
|
+
},
|
|
317
|
+
{
|
|
318
|
+
path: getSkillsInstallerGlobalLockPath(),
|
|
319
|
+
scope: 'global',
|
|
320
|
+
},
|
|
321
|
+
];
|
|
322
|
+
const entries = await Promise.all(lockPaths.map((lock) => readPostPlusInstallerLockedSkillNamesFromPath(lock.path).then((skillNames) => skillNames.map((name) => ({
|
|
323
|
+
name,
|
|
324
|
+
scope: lock.scope,
|
|
325
|
+
})))));
|
|
326
|
+
return entries
|
|
327
|
+
.flat()
|
|
328
|
+
.sort((left, right) => left.scope.localeCompare(right.scope) ||
|
|
329
|
+
left.name.localeCompare(right.name));
|
|
330
|
+
}
|
|
331
|
+
async function readPostPlusInstallerLockedSkillNamesFromPath(lockPath) {
|
|
332
|
+
try {
|
|
333
|
+
const raw = await readFile(lockPath, 'utf8');
|
|
334
|
+
const payload = JSON.parse(raw);
|
|
335
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
|
|
336
|
+
return [];
|
|
337
|
+
}
|
|
338
|
+
const record = payload;
|
|
339
|
+
if (typeof record.version !== 'number') {
|
|
340
|
+
return [];
|
|
341
|
+
}
|
|
342
|
+
if (!record.skills || typeof record.skills !== 'object') {
|
|
343
|
+
return [];
|
|
344
|
+
}
|
|
345
|
+
return Object.entries(record.skills)
|
|
346
|
+
.filter(([, entry]) => isPostPlusSkillsInstallerLockEntry(entry))
|
|
347
|
+
.map(([skillName]) => skillName.trim())
|
|
348
|
+
.filter(Boolean)
|
|
349
|
+
.sort((a, b) => a.localeCompare(b));
|
|
350
|
+
}
|
|
351
|
+
catch (error) {
|
|
352
|
+
const nodeError = error;
|
|
353
|
+
if (nodeError.code === 'ENOENT') {
|
|
354
|
+
return [];
|
|
355
|
+
}
|
|
356
|
+
throw error;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
function isPostPlusSkillsInstallerLockEntry(entry) {
|
|
360
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
|
|
361
|
+
return false;
|
|
362
|
+
}
|
|
363
|
+
const record = entry;
|
|
364
|
+
const source = typeof record.source === 'string' ? record.source.trim() : '';
|
|
365
|
+
const sourceUrl = typeof record.sourceUrl === 'string' ? record.sourceUrl.trim() : '';
|
|
366
|
+
return (normalizeSkillsInstallerSource(source) ===
|
|
367
|
+
SKILLS_INSTALLER_POSTPLUS_SOURCE ||
|
|
368
|
+
normalizeSkillsInstallerSource(sourceUrl) ===
|
|
369
|
+
SKILLS_INSTALLER_POSTPLUS_SOURCE);
|
|
370
|
+
}
|
|
371
|
+
function normalizeSkillsInstallerSource(value) {
|
|
372
|
+
let normalized = value.trim().replace(/\\/g, '/');
|
|
373
|
+
if (normalized.length === 0) {
|
|
374
|
+
return '';
|
|
375
|
+
}
|
|
376
|
+
const sshMatch = normalized.match(/^git@[^:]+:(.+)$/);
|
|
377
|
+
if (sshMatch) {
|
|
378
|
+
normalized = sshMatch[1] ?? '';
|
|
379
|
+
}
|
|
380
|
+
else if (/^https?:\/\//i.test(normalized) || /^ssh:\/\//i.test(normalized)) {
|
|
381
|
+
try {
|
|
382
|
+
normalized = new URL(normalized).pathname.replace(/^\/+/, '');
|
|
383
|
+
}
|
|
384
|
+
catch {
|
|
385
|
+
return normalized.toLowerCase();
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
return normalized
|
|
389
|
+
.replace(/\.git$/i, '')
|
|
390
|
+
.replace(/\/+$/, '')
|
|
391
|
+
.toLowerCase();
|
|
392
|
+
}
|
|
393
|
+
function getSkillsInstallerGlobalLockPath() {
|
|
394
|
+
const xdgStateHome = process.env.XDG_STATE_HOME?.trim();
|
|
395
|
+
return xdgStateHome
|
|
396
|
+
? join(xdgStateHome, 'skills', '.skill-lock.json')
|
|
397
|
+
: join(homedir(), ...SKILLS_INSTALLER_GLOBAL_LOCK_PATH);
|
|
398
|
+
}
|
|
399
|
+
function getSkillsInstallerProjectLockPath() {
|
|
400
|
+
return join(process.cwd(), SKILLS_INSTALLER_PROJECT_LOCK_PATH);
|
|
401
|
+
}
|
|
277
402
|
async function listInstalledSkills(dependencies) {
|
|
278
403
|
const project = await listInstalledSkillsForScope(dependencies, []);
|
|
279
404
|
const global = await listInstalledSkillsForScope(dependencies, ['--global']);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@postplus/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.39",
|
|
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.",
|