@prave/cli 1.4.0 β 1.4.2
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/dist/commands/list.js +2 -0
- package/dist/commands/run.js +145 -9
- package/dist/commands/search.js +2 -0
- package/dist/commands/uninstall.js +38 -7
- package/dist/commands/whatdoes.js +2 -0
- package/dist/lib/nudge.js +61 -0
- package/package.json +6 -4
- package/scripts/postinstall.mjs +31 -0
package/dist/commands/list.js
CHANGED
|
@@ -5,6 +5,7 @@ import { tokenTier } from '@prave/shared';
|
|
|
5
5
|
import { track } from '../lib/analytics.js';
|
|
6
6
|
import { api } from '../lib/api.js';
|
|
7
7
|
import { CONFIG } from '../lib/config.js';
|
|
8
|
+
import { nudgeIfAnonymous } from '../lib/nudge.js';
|
|
8
9
|
import { log } from '../utils/logger.js';
|
|
9
10
|
const TIER_EMOJI = {
|
|
10
11
|
lean: 'π’',
|
|
@@ -68,6 +69,7 @@ export async function listCommand(opts = {}) {
|
|
|
68
69
|
console.log(` ${chalk.cyan('β’')} ${name}`);
|
|
69
70
|
}
|
|
70
71
|
log.dim(`\n${localSlugs.length} local skill${localSlugs.length === 1 ? '' : 's'} in ${CONFIG.skillsDir}`);
|
|
72
|
+
await nudgeIfAnonymous('list');
|
|
71
73
|
return;
|
|
72
74
|
}
|
|
73
75
|
// Enriched path β pull intelligence and merge by slug (best-effort).
|
package/dist/commands/run.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { readdir, readFile, stat } from 'node:fs/promises';
|
|
2
2
|
import { resolve, relative, basename, sep } from 'node:path';
|
|
3
|
+
import { createInterface } from 'node:readline/promises';
|
|
3
4
|
import { Buffer } from 'node:buffer';
|
|
4
5
|
import chalk from 'chalk';
|
|
5
6
|
import open from 'open';
|
|
@@ -62,11 +63,40 @@ export async function runDeployCommand(pathArg) {
|
|
|
62
63
|
const creds0 = await requireAuth('prave run');
|
|
63
64
|
if (!creds0)
|
|
64
65
|
return;
|
|
65
|
-
//
|
|
66
|
+
// 1a. Look for .env / .env.production / .env.local etc. The scanner
|
|
67
|
+
// would otherwise hard-reject them on the upload. Offer to lift the
|
|
68
|
+
// values out of the bundle and pass them to the wizard so they end
|
|
69
|
+
// up encrypted on the run row instead of in the tarball.
|
|
70
|
+
const envFiles = await findEnvFiles(root);
|
|
71
|
+
let envVars = {};
|
|
72
|
+
const stripPaths = new Set();
|
|
73
|
+
if (envFiles.length > 0) {
|
|
74
|
+
const totalKeys = envFiles.reduce((n, f) => n + Object.keys(f.values).length, 0);
|
|
75
|
+
console.log();
|
|
76
|
+
console.log(chalk.bold(`Found ${envFiles.length} env file${envFiles.length === 1 ? '' : 's'} with ${totalKeys} variable${totalKeys === 1 ? '' : 's'}:`));
|
|
77
|
+
for (const f of envFiles) {
|
|
78
|
+
console.log(` ${chalk.cyan('β’')} ${f.path} ${chalk.dim(`(${Object.keys(f.values).length} keys)`)}`);
|
|
79
|
+
}
|
|
80
|
+
console.log(chalk.dim('\nIf you continue, Prave strips these from the upload and passes the\n' +
|
|
81
|
+
'values to the browser wizard. They get AES-256-GCM-encrypted onto\n' +
|
|
82
|
+
'the run, decrypted only inside the sandbox at run-time.\n' +
|
|
83
|
+
'Decline to abort so you can clean up first.'));
|
|
84
|
+
const yes = await confirmYesNo('Strip & pass env vars to the wizard?');
|
|
85
|
+
if (!yes) {
|
|
86
|
+
log.error('Aborted. Remove the .env file(s) (or rename to .env.example) and re-run.');
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
for (const f of envFiles) {
|
|
90
|
+
stripPaths.add(f.path);
|
|
91
|
+
Object.assign(envVars, f.values);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
// 1b. Local secret-scan BEFORE we ship anything. Cheap defence in
|
|
66
95
|
// depth β even though the API scans again, surfacing the finding
|
|
67
96
|
// pre-upload saves the user a round-trip and avoids briefly storing
|
|
68
|
-
// a secret-bearing tarball in our bucket.
|
|
69
|
-
|
|
97
|
+
// a secret-bearing tarball in our bucket. The env files we just
|
|
98
|
+
// accepted to lift out are excluded from the scan.
|
|
99
|
+
const localFindings = await preflightScan(root, stripPaths);
|
|
70
100
|
if (localFindings.length > 0) {
|
|
71
101
|
log.error('Bundle contains files that look like secrets:');
|
|
72
102
|
for (const f of localFindings.slice(0, 10)) {
|
|
@@ -89,13 +119,28 @@ export async function runDeployCommand(pathArg) {
|
|
|
89
119
|
initSpinner.fail(`Could not open deploy session: ${err.message}`);
|
|
90
120
|
process.exit(1);
|
|
91
121
|
}
|
|
122
|
+
// 2b. Ship env vars to the wizard before the upload so they're
|
|
123
|
+
// waiting when the browser opens. The wizard reads them once via
|
|
124
|
+
// /deploy/status and pre-fills its env-vars step.
|
|
125
|
+
if (Object.keys(envVars).length > 0) {
|
|
126
|
+
const envSpinner = ora('Sending env vars to the wizardβ¦').start();
|
|
127
|
+
try {
|
|
128
|
+
await api.post(`/api/v1/deploy/env?session=${encodeURIComponent(session.session_id)}`, { env_vars: envVars }, true);
|
|
129
|
+
envSpinner.succeed(`Passed ${Object.keys(envVars).length} env var(s)`);
|
|
130
|
+
}
|
|
131
|
+
catch (err) {
|
|
132
|
+
envSpinner.fail(`Could not ship env vars: ${err.message}`);
|
|
133
|
+
process.exit(1);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
92
136
|
// 3. Pack the directory into a gzipped tar in memory. tar.create's
|
|
93
137
|
// `cwd` option is critical β paths inside the archive must be
|
|
94
|
-
// RELATIVE to the project root, not absolute.
|
|
138
|
+
// RELATIVE to the project root, not absolute. Stripped paths are
|
|
139
|
+
// env files we already shipped via /deploy/env above.
|
|
95
140
|
const packSpinner = ora('Bundling projectβ¦').start();
|
|
96
141
|
let tarball;
|
|
97
142
|
try {
|
|
98
|
-
tarball = await packDirectory(root);
|
|
143
|
+
tarball = await packDirectory(root, stripPaths);
|
|
99
144
|
packSpinner.succeed(`Bundled ${formatBytes(tarball.length)}`);
|
|
100
145
|
}
|
|
101
146
|
catch (err) {
|
|
@@ -221,7 +266,7 @@ async function findSkillMd(root) {
|
|
|
221
266
|
return null;
|
|
222
267
|
}
|
|
223
268
|
}
|
|
224
|
-
async function preflightScan(root) {
|
|
269
|
+
async function preflightScan(root, stripPaths = new Set()) {
|
|
225
270
|
const inputs = [];
|
|
226
271
|
let files = 0;
|
|
227
272
|
let bytes = 0;
|
|
@@ -243,8 +288,13 @@ async function preflightScan(root) {
|
|
|
243
288
|
}
|
|
244
289
|
if (!entry.isFile())
|
|
245
290
|
continue;
|
|
246
|
-
files++;
|
|
247
291
|
const rel = relative(root, abs).split(sep).join('/');
|
|
292
|
+
// The user already accepted to lift this env file out of the
|
|
293
|
+
// bundle in the previous step. Don't scan it (we know it has
|
|
294
|
+
// secrets β that's why we're lifting it).
|
|
295
|
+
if (stripPaths.has(rel))
|
|
296
|
+
continue;
|
|
297
|
+
files++;
|
|
248
298
|
if (!isLikelyTextPath(rel)) {
|
|
249
299
|
inputs.push({ path: rel });
|
|
250
300
|
continue;
|
|
@@ -262,12 +312,17 @@ async function preflightScan(root) {
|
|
|
262
312
|
await visit(root);
|
|
263
313
|
return scanForSecrets(inputs).findings;
|
|
264
314
|
}
|
|
265
|
-
async function packDirectory(root) {
|
|
315
|
+
async function packDirectory(root, stripPaths = new Set()) {
|
|
266
316
|
// Top-level entries only β tar.create resolves them against `cwd`.
|
|
267
317
|
const entries = (await readdir(root)).filter((n) => !TAR_IGNORE.has(n));
|
|
268
318
|
if (entries.length === 0) {
|
|
269
319
|
throw new Error('Project directory is empty.');
|
|
270
320
|
}
|
|
321
|
+
// Normalise the strip-set against tar.create's relative-path format
|
|
322
|
+
// (POSIX forward slashes, no leading "./"). Paths sit one level
|
|
323
|
+
// beneath the archive's basename prefix, so we compare on the
|
|
324
|
+
// input-side relative path tar.create hands us.
|
|
325
|
+
const stripNormalised = new Set([...stripPaths].map((p) => p.replace(/\\/g, '/').replace(/^\.\//, '')));
|
|
271
326
|
const stream = tar.create({
|
|
272
327
|
gzip: true,
|
|
273
328
|
cwd: root,
|
|
@@ -276,7 +331,10 @@ async function packDirectory(root) {
|
|
|
276
331
|
// gets a `<project>/SKILL.md` shape, not a flat dump at the
|
|
277
332
|
// archive root.
|
|
278
333
|
prefix: basename(root),
|
|
279
|
-
filter: (
|
|
334
|
+
filter: (path) => {
|
|
335
|
+
const norm = path.replace(/\\/g, '/').replace(/^\.\//, '');
|
|
336
|
+
return !stripNormalised.has(norm);
|
|
337
|
+
},
|
|
280
338
|
}, entries);
|
|
281
339
|
const chunks = [];
|
|
282
340
|
for await (const chunk of stream) {
|
|
@@ -284,6 +342,84 @@ async function packDirectory(root) {
|
|
|
284
342
|
}
|
|
285
343
|
return Buffer.concat(chunks);
|
|
286
344
|
}
|
|
345
|
+
/**
|
|
346
|
+
* Look for common `.env*` files at the project root. We don't scan
|
|
347
|
+
* deeper than top-level β a nested `.env` is almost certainly a
|
|
348
|
+
* shipping example, not the real secrets file. Returns an empty list
|
|
349
|
+
* when nothing's found.
|
|
350
|
+
*/
|
|
351
|
+
async function findEnvFiles(root) {
|
|
352
|
+
const entries = await readdir(root, { withFileTypes: true }).catch(() => []);
|
|
353
|
+
const findings = [];
|
|
354
|
+
for (const entry of entries) {
|
|
355
|
+
if (!entry.isFile())
|
|
356
|
+
continue;
|
|
357
|
+
const name = entry.name;
|
|
358
|
+
if (!isRealEnvFile(name))
|
|
359
|
+
continue;
|
|
360
|
+
try {
|
|
361
|
+
const raw = await readFile(`${root}${sep}${name}`, 'utf8');
|
|
362
|
+
const values = parseDotenv(raw);
|
|
363
|
+
if (Object.keys(values).length === 0)
|
|
364
|
+
continue;
|
|
365
|
+
findings.push({ path: name, values });
|
|
366
|
+
}
|
|
367
|
+
catch {
|
|
368
|
+
/* unreadable β skip */
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
return findings;
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* `.env`, `.env.local`, `.env.production`, `.env.development`, `.envrc`
|
|
375
|
+
* count as "real env files". Templates (`.env.example`, `.env.sample`,
|
|
376
|
+
* `.env.template`) are explicitly NOT lifted β those are meant to be
|
|
377
|
+
* shipped.
|
|
378
|
+
*/
|
|
379
|
+
function isRealEnvFile(name) {
|
|
380
|
+
if (name === '.env' || name === '.envrc')
|
|
381
|
+
return true;
|
|
382
|
+
if (!name.startsWith('.env.'))
|
|
383
|
+
return false;
|
|
384
|
+
const suffix = name.slice('.env.'.length);
|
|
385
|
+
if (/^(example|sample|template)$/i.test(suffix))
|
|
386
|
+
return false;
|
|
387
|
+
return true;
|
|
388
|
+
}
|
|
389
|
+
function parseDotenv(text) {
|
|
390
|
+
const out = {};
|
|
391
|
+
for (const raw of text.split('\n')) {
|
|
392
|
+
const line = raw.trim();
|
|
393
|
+
if (!line || line.startsWith('#'))
|
|
394
|
+
continue;
|
|
395
|
+
const eq = line.indexOf('=');
|
|
396
|
+
if (eq <= 0)
|
|
397
|
+
continue;
|
|
398
|
+
const key = line.slice(0, eq).trim();
|
|
399
|
+
let value = line.slice(eq + 1).trim();
|
|
400
|
+
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
401
|
+
(value.startsWith("'") && value.endsWith("'"))) {
|
|
402
|
+
value = value.slice(1, -1);
|
|
403
|
+
}
|
|
404
|
+
if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(key))
|
|
405
|
+
out[key] = value;
|
|
406
|
+
}
|
|
407
|
+
return out;
|
|
408
|
+
}
|
|
409
|
+
async function confirmYesNo(question) {
|
|
410
|
+
// Non-interactive shells (pipes, CI) can't prompt β default to "no"
|
|
411
|
+
// so we never silently ship secrets in a script.
|
|
412
|
+
if (!process.stdout.isTTY || !process.stdin.isTTY)
|
|
413
|
+
return false;
|
|
414
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
415
|
+
try {
|
|
416
|
+
const answer = (await rl.question(`${question} [Y/n] `)).trim().toLowerCase();
|
|
417
|
+
return answer === '' || answer === 'y' || answer === 'yes';
|
|
418
|
+
}
|
|
419
|
+
finally {
|
|
420
|
+
rl.close();
|
|
421
|
+
}
|
|
422
|
+
}
|
|
287
423
|
function formatBytes(n) {
|
|
288
424
|
if (n < 1024)
|
|
289
425
|
return `${n}B`;
|
package/dist/commands/search.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import { track } from '../lib/analytics.js';
|
|
3
3
|
import { api } from '../lib/api.js';
|
|
4
|
+
import { nudgeIfAnonymous } from '../lib/nudge.js';
|
|
4
5
|
import { log } from '../utils/logger.js';
|
|
5
6
|
const SLUG_COL = 32;
|
|
6
7
|
function formatInstalls(n) {
|
|
@@ -41,4 +42,5 @@ export async function searchCommand(query) {
|
|
|
41
42
|
if (skills.length > 0) {
|
|
42
43
|
console.log(chalk.dim(' β prave install <slug>'));
|
|
43
44
|
}
|
|
45
|
+
await nudgeIfAnonymous('search');
|
|
44
46
|
}
|
|
@@ -2,15 +2,23 @@ import { rm } from 'node:fs/promises';
|
|
|
2
2
|
import { join, resolve, sep } from 'node:path';
|
|
3
3
|
import ora from 'ora';
|
|
4
4
|
import { track } from '../lib/analytics.js';
|
|
5
|
+
import { api, ApiError } from '../lib/api.js';
|
|
5
6
|
import { CONFIG } from '../lib/config.js';
|
|
7
|
+
import { loadCredentials } from '../lib/credentials.js';
|
|
6
8
|
import { assertSlug, InvalidSlugError } from '../lib/slug.js';
|
|
7
9
|
/**
|
|
8
|
-
* `prave uninstall <slug>` β removes ~/.claude/skills/<slug>
|
|
9
|
-
*
|
|
10
|
-
*
|
|
10
|
+
* `prave uninstall <slug>` β removes ~/.claude/skills/<slug> AND tells
|
|
11
|
+
* the server to drop the corresponding install record so the
|
|
12
|
+
* dashboard counter goes down.
|
|
11
13
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
+
* The remote DELETE is best-effort: a network failure or "not logged
|
|
15
|
+
* in" state still lets the local rm succeed (so the user isn't stuck
|
|
16
|
+
* with dead files when offline). The mismatch then heals on the next
|
|
17
|
+
* `prave sync` or manual dashboard cleanup.
|
|
18
|
+
*
|
|
19
|
+
* Slug is validated against [a-z0-9][a-z0-9-]{0,63} before any path
|
|
20
|
+
* math so a hostile arg like `../../etc` cannot escape the skills
|
|
21
|
+
* directory.
|
|
14
22
|
*/
|
|
15
23
|
export async function uninstallCommand(slug) {
|
|
16
24
|
track('cli_uninstall', { slug });
|
|
@@ -37,10 +45,33 @@ export async function uninstallCommand(slug) {
|
|
|
37
45
|
const spinner = ora(`Removing ${slug}β¦`).start();
|
|
38
46
|
try {
|
|
39
47
|
await rm(dir, { recursive: true, force: true });
|
|
40
|
-
spinner.succeed(`Removed ${dir}`);
|
|
41
48
|
}
|
|
42
49
|
catch (err) {
|
|
43
|
-
spinner.fail(`Couldn
|
|
50
|
+
spinner.fail(`Couldn't remove ${slug}: ${err.message}`);
|
|
44
51
|
process.exitCode = 1;
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
// Best-effort server-side ledger sweep. We only attempt it when the
|
|
55
|
+
// user is logged in β anonymous CLI usage (running before
|
|
56
|
+
// `prave login`) still does the local rm and exits cleanly.
|
|
57
|
+
const creds = await loadCredentials();
|
|
58
|
+
if (creds) {
|
|
59
|
+
try {
|
|
60
|
+
await api.del(`/api/v1/skills/${encodeURIComponent(slug)}/install`, true);
|
|
61
|
+
spinner.succeed(`Removed ${slug} (local + server install record)`);
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
// 404 = install record already absent (matches our idempotent
|
|
65
|
+
// server behaviour). Anything else is a soft warning.
|
|
66
|
+
if (err instanceof ApiError && err.status === 404) {
|
|
67
|
+
spinner.succeed(`Removed ${slug} (local; server had no install record)`);
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
spinner.warn(`Removed ${slug} locally β server ledger update failed (${err.message}). Run \`prave sync\` later to reconcile.`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
spinner.succeed(`Removed ${slug} (local only β not signed in)`);
|
|
45
76
|
}
|
|
46
77
|
}
|
|
@@ -3,6 +3,7 @@ import ora from 'ora';
|
|
|
3
3
|
import { tokenTier } from '@prave/shared';
|
|
4
4
|
import { track } from '../lib/analytics.js';
|
|
5
5
|
import { api, ApiError } from '../lib/api.js';
|
|
6
|
+
import { nudgeIfAnonymous } from '../lib/nudge.js';
|
|
6
7
|
import { log } from '../utils/logger.js';
|
|
7
8
|
const TIER_BADGE = {
|
|
8
9
|
lean: chalk.green('π’ Lean'),
|
|
@@ -71,6 +72,7 @@ export async function whatdoesCommand(skillName) {
|
|
|
71
72
|
console.log(`π Requires: ${requires}`);
|
|
72
73
|
console.log(`${data.conflicts.length > 0 ? chalk.yellow('β οΈ ') : 'β οΈ '}Conflicts: ${conflicts}`);
|
|
73
74
|
console.log(chalk.dim(RULE));
|
|
75
|
+
await nudgeIfAnonymous('whatdoes');
|
|
74
76
|
}
|
|
75
77
|
catch (err) {
|
|
76
78
|
spinner.stop();
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { loadCredentials } from './credentials.js';
|
|
3
|
+
let alreadyNudged = false;
|
|
4
|
+
export async function nudgeIfAnonymous(context = 'generic') {
|
|
5
|
+
if (alreadyNudged)
|
|
6
|
+
return;
|
|
7
|
+
if (process.env.PRAVE_QUIET === '1')
|
|
8
|
+
return;
|
|
9
|
+
if (process.env.PRAVE_TELEMETRY === '0')
|
|
10
|
+
return; // user opted out of analytics
|
|
11
|
+
// CI / non-interactive shells are unlikely to act on a nudge β and
|
|
12
|
+
// the noise breaks pipe-parsing scripts.
|
|
13
|
+
if (!process.stdout.isTTY)
|
|
14
|
+
return;
|
|
15
|
+
const creds = await loadCredentials();
|
|
16
|
+
if (creds)
|
|
17
|
+
return; // logged-in β no nudge
|
|
18
|
+
alreadyNudged = true;
|
|
19
|
+
const cta = chalk.cyan('prave login');
|
|
20
|
+
const url = chalk.dim('β takes 10 seconds, free forever');
|
|
21
|
+
const message = (() => {
|
|
22
|
+
switch (context) {
|
|
23
|
+
case 'search':
|
|
24
|
+
return `${chalk.dim('Save these to your library, get token costs + conflicts.')}\n${chalk.dim('β')} ${cta} ${url}`;
|
|
25
|
+
case 'whatdoes':
|
|
26
|
+
return `${chalk.dim('Want the full audit β triggers, conflicts, token cost?')}\n${chalk.dim('β')} ${cta} ${url}`;
|
|
27
|
+
case 'list':
|
|
28
|
+
return `${chalk.dim('Sign in to see AI descriptions, conflicts and token cost.')}\n${chalk.dim('β')} ${cta} ${url}`;
|
|
29
|
+
case 'overview':
|
|
30
|
+
return `${chalk.dim('Track this over time on prave.app.')}\n${chalk.dim('β')} ${cta} ${url}`;
|
|
31
|
+
case 'generic':
|
|
32
|
+
default:
|
|
33
|
+
return `${chalk.dim('Get the full Skill Intelligence on prave.app.')}\n${chalk.dim('β')} ${cta} ${url}`;
|
|
34
|
+
}
|
|
35
|
+
})();
|
|
36
|
+
// One blank line so the nudge isn't glued to the command's main
|
|
37
|
+
// output. Two `console.log` calls so the chalk styles survive the
|
|
38
|
+
// pipeline cleanly.
|
|
39
|
+
console.log();
|
|
40
|
+
console.log(message);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Post-install banner used by package.json's `postinstall` script.
|
|
44
|
+
* Runs once after `npm i -g @prave/cli`. Stays short β npm's install
|
|
45
|
+
* log is already crowded.
|
|
46
|
+
*/
|
|
47
|
+
export function printPostInstallBanner() {
|
|
48
|
+
if (process.env.CI)
|
|
49
|
+
return;
|
|
50
|
+
if (process.env.PRAVE_QUIET === '1')
|
|
51
|
+
return;
|
|
52
|
+
console.log();
|
|
53
|
+
console.log(chalk.bold(` Prave CLI installed.`));
|
|
54
|
+
console.log();
|
|
55
|
+
console.log(` ${chalk.cyan('prave login')} ${chalk.dim('β create your free account (browser)')}`);
|
|
56
|
+
console.log(` ${chalk.cyan('prave search <q>')} ${chalk.dim('β find any Claude Skill')}`);
|
|
57
|
+
console.log(` ${chalk.cyan('prave docs')} ${chalk.dim('β open the docs')}`);
|
|
58
|
+
console.log();
|
|
59
|
+
console.log(chalk.dim(' Docs: https://prave.app/docs Β· Issues: github.com/eppstudio/prave'));
|
|
60
|
+
console.log();
|
|
61
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prave/cli",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.2",
|
|
4
4
|
"description": "Prave CLI β discover, install, version, test, and ship Claude Skills. The developer platform for the complete Skill lifecycle.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
|
@@ -43,7 +43,8 @@
|
|
|
43
43
|
},
|
|
44
44
|
"main": "dist/index.js",
|
|
45
45
|
"files": [
|
|
46
|
-
"dist"
|
|
46
|
+
"dist",
|
|
47
|
+
"scripts/postinstall.mjs"
|
|
47
48
|
],
|
|
48
49
|
"dependencies": {
|
|
49
50
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
@@ -53,7 +54,7 @@
|
|
|
53
54
|
"ora": "^8.0.1",
|
|
54
55
|
"tar": "^7.4.3",
|
|
55
56
|
"undici": "^6.18.0",
|
|
56
|
-
"@prave/shared": "1.4.
|
|
57
|
+
"@prave/shared": "1.4.2"
|
|
57
58
|
},
|
|
58
59
|
"devDependencies": {
|
|
59
60
|
"@types/node": "^20.12.7",
|
|
@@ -69,6 +70,7 @@
|
|
|
69
70
|
"cli": "tsx src/index.ts",
|
|
70
71
|
"build": "tsc -p tsconfig.json && node scripts/inject-config.mjs",
|
|
71
72
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
72
|
-
"lint": "tsc -p tsconfig.json --noEmit"
|
|
73
|
+
"lint": "tsc -p tsconfig.json --noEmit",
|
|
74
|
+
"postinstall": "node scripts/postinstall.mjs || true"
|
|
73
75
|
}
|
|
74
76
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* npm postinstall hook. Runs once after `npm i -g @prave/cli` (and on
|
|
4
|
+
* upgrades). Prints a small "you're set β what now?" banner so the
|
|
5
|
+
* thousands of CLI installs we're seeing actually find the dashboard
|
|
6
|
+
* + the docs.
|
|
7
|
+
*
|
|
8
|
+
* Silent in CI / non-interactive shells / when PRAVE_QUIET=1 is set.
|
|
9
|
+
* Also silent on dependent installs β npm sets `npm_config_global` to
|
|
10
|
+
* 'false' or unset when we're being installed as a dependency, and
|
|
11
|
+
* the banner is only useful for the global-install user.
|
|
12
|
+
*/
|
|
13
|
+
if (process.env.CI) process.exit(0)
|
|
14
|
+
if (process.env.PRAVE_QUIET === '1') process.exit(0)
|
|
15
|
+
// `npm_config_global` is `'true'` only for `npm i -g`. Skip for local
|
|
16
|
+
// dependency installs (e.g. when our worker container builds).
|
|
17
|
+
if (process.env.npm_config_global !== 'true') process.exit(0)
|
|
18
|
+
|
|
19
|
+
const dim = (s) => `\x1b[2m${s}\x1b[0m`
|
|
20
|
+
const bold = (s) => `\x1b[1m${s}\x1b[0m`
|
|
21
|
+
const cyan = (s) => `\x1b[36m${s}\x1b[0m`
|
|
22
|
+
|
|
23
|
+
console.log()
|
|
24
|
+
console.log(` ${bold('Prave CLI installed.')}`)
|
|
25
|
+
console.log()
|
|
26
|
+
console.log(` ${cyan('prave login')} ${dim('β create your free account (browser)')}`)
|
|
27
|
+
console.log(` ${cyan('prave search <q>')} ${dim('β find any Claude Skill')}`)
|
|
28
|
+
console.log(` ${cyan('prave docs')} ${dim('β open the docs')}`)
|
|
29
|
+
console.log()
|
|
30
|
+
console.log(dim(' Docs: https://prave.app/docs Β· Issues: github.com/eppstudio/prave'))
|
|
31
|
+
console.log()
|