@prave/cli 1.4.0 โ†’ 1.4.1

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.
@@ -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).
@@ -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> recursively.
9
- * No remote call: uninstalls are local-only; the install record stays
10
- * for analytics + history.
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
- * Slug is validated against [a-z0-9][a-z0-9-]{0,63} before any path math
13
- * so a hostile arg like `../../etc` cannot escape the skills directory.
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โ€™t remove ${slug}: ${err.message}`);
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.0",
3
+ "version": "1.4.1",
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.0"
57
+ "@prave/shared": "1.4.1"
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"
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()