wensity 0.1.14 → 0.1.15

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.
Files changed (3) hide show
  1. package/README.md +32 -0
  2. package/dist/index.js +968 -24
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -92,6 +92,10 @@ with their icons as authored.
92
92
  | `wensity login` | Save a Wensity API token for Pro component access |
93
93
  | `wensity logout` | Remove saved credentials |
94
94
  | `wensity whoami` | Verify the saved token and print the account and plan |
95
+ | `wensity skills add <name...>` | Install Wensity Design Skills for your AI agents (`--all` for every skill your plan includes) |
96
+ | `wensity skills list` | Every skill, Free or Pro, with the installed and latest version |
97
+ | `wensity skills update [name...]` | Get the latest version, leaving files you edited alone unless `--force` |
98
+ | `wensity skills remove <name...>` | Delete the skill files the CLI wrote, keeping any you edited |
95
99
  | `wensity version` | Print the CLI version |
96
100
 
97
101
  Useful flags:
@@ -104,6 +108,34 @@ Useful flags:
104
108
  | `--force` | Allow `init` to overwrite an existing `wensity.json` |
105
109
  | `--json` | Machine readable output where supported |
106
110
 
111
+ ## Design Skills
112
+
113
+ Agent skills that teach Claude Code, Codex, Cursor, Copilot, Windsurf, Gemini
114
+ CLI and OpenCode how to design interfaces. Free skills need no account; Pro
115
+ skills come with Wensity Pro and use the saved login.
116
+
117
+ ```bash
118
+ npx wensity@latest skills add typography layout
119
+ npx wensity@latest login
120
+ npx wensity@latest skills add motion
121
+ ```
122
+
123
+ Names work in full (`wensity-motion`), short (`motion`), or as the slug on the
124
+ site (`ux-writing`). Skills go where each agent reads them: `.claude/skills/`
125
+ for Claude Code and `.agents/skills/` for the others. The CLI picks the ones
126
+ your project already uses, asks once when there are none, or takes
127
+ `--agent claude` / `--agent agents`. Add `--global` to install in your home
128
+ folder for every project, and `--dry-run` to see the changes first.
129
+
130
+ Each install is recorded in `wensity-skills.json` (with `--global`,
131
+ `~/.config/wensity/skills.json`) with the hash of every file written, so
132
+ `update` and `remove` never touch a file you changed. Every file is checked
133
+ against its hash before anything is written, and a failed install changes
134
+ nothing.
135
+
136
+ Pro skill files carry one license line for your account. Keep them out of
137
+ public repositories.
138
+
107
139
  ## Authentication
108
140
 
109
141
  Free components need no account. Wensity Pro components and blocks require an
package/dist/index.js CHANGED
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/commands.ts
4
- import { randomBytes } from "crypto";
5
- import { existsSync as existsSync5 } from "fs";
6
- import { mkdir as mkdir7, readFile as readFile8, stat, writeFile as writeFile6 } from "fs/promises";
7
- import { dirname as dirname8, join as join11, relative as relative8 } from "path";
4
+ import { randomBytes as randomBytes3 } from "crypto";
5
+ import { existsSync as existsSync6 } from "fs";
6
+ import { mkdir as mkdir8, readFile as readFile9, stat, writeFile as writeFile7 } from "fs/promises";
7
+ import { dirname as dirname9, join as join13, relative as relative8 } from "path";
8
8
 
9
9
  // ../install-core/src/constants.ts
10
10
  var CONFIG_FILE_NAME = "wensity.json";
@@ -17,7 +17,7 @@ var DEFAULT_UTILS_ALIAS = "@/lib/utils";
17
17
 
18
18
  // src/constants.ts
19
19
  var CLI_NAME = "wensity";
20
- var CLI_VERSION = "0.1.14";
20
+ var CLI_VERSION = "0.1.15";
21
21
  var INIT_PRIMITIVE_CSS_SLUG = "button";
22
22
  var HELP_TEXT = `Wensity CLI ${CLI_VERSION}
23
23
 
@@ -31,6 +31,10 @@ Usage:
31
31
  wensity list [--type component|block] [options]
32
32
  wensity add <slug...> [--type component|block] [options]
33
33
  wensity update <slug...> [--type component|block] [options]
34
+ wensity skills add <name...> [--all] [--agent claude|agents] [--global] [--dry-run] [--yes] [--force]
35
+ wensity skills list [--global] [--json]
36
+ wensity skills update [name...] [--global] [--dry-run] [--force]
37
+ wensity skills remove <name...> [--global] [--dry-run]
34
38
  wensity version
35
39
 
36
40
  Commands:
@@ -43,6 +47,7 @@ Commands:
43
47
  list List registry components or blocks
44
48
  add Download one or more component/block source files into this project
45
49
  update Re-download one or more components/blocks and overwrite the local copies
50
+ skills Add, list, update or remove Wensity Design Skills for your AI agents
46
51
  version Print the CLI version
47
52
 
48
53
  Options:
@@ -58,6 +63,10 @@ Options:
58
63
  --no-install Write files without running the package manager
59
64
  --dry-run Print planned writes and installs without changing files
60
65
  --json Print machine-readable output where supported
66
+ --agent <agent> skills: claude (.claude/skills) or agents (.agents/skills); repeat for both
67
+ --global skills: install in your home folder for every project
68
+ --all skills add: every skill your plan includes
69
+ --yes skills: no questions; installs for both agents when none is found
61
70
  --help Show help
62
71
  --version Print the CLI version
63
72
 
@@ -70,6 +79,12 @@ Preset notes:
70
79
  init --template writes a minimal app scaffold in the current directory before Wensity files
71
80
  add/update accept several slugs at once and read style.iconLibrary for UI primitives only (Tabler|Lucide)
72
81
  Invalid/expired/unsupported codes exit non-zero with an actionable message
82
+
83
+ Skills notes:
84
+ Names work in full (wensity-motion), short (motion), or as the site slug (ux-writing)
85
+ Free skills need no login; Pro skills use the token saved by wensity login
86
+ Installs are recorded in wensity-skills.json (with --global: ~/.config/wensity/skills.json)
87
+ update and remove never touch a file you edited unless you pass --force (update only)
73
88
  `;
74
89
 
75
90
  // ../install-core/src/errors.ts
@@ -246,7 +261,7 @@ function originOf(url) {
246
261
  }
247
262
  }
248
263
  function defaultSleep(ms) {
249
- return new Promise((resolve3) => setTimeout(resolve3, ms));
264
+ return new Promise((resolve4) => setTimeout(resolve4, ms));
250
265
  }
251
266
  function parseJsonOrNull(text) {
252
267
  try {
@@ -293,7 +308,12 @@ var UPSTREAM_ERROR_CODES = {
293
308
  component_source_unreadable: "source_unavailable",
294
309
  block_source_unreadable: "source_unavailable",
295
310
  component_source_graph_unreadable: "source_unavailable",
296
- cli_auth_failed: "server_error"
311
+ cli_auth_failed: "server_error",
312
+ pro_required: "entitlement_required",
313
+ license_inactive: "entitlement_required",
314
+ skill_not_found: "item_not_found",
315
+ skill_version_not_found: "item_not_found",
316
+ skill_unavailable: "source_unavailable"
297
317
  };
298
318
  function errorCodeFor(upstream) {
299
319
  const known = upstream.error ? UPSTREAM_ERROR_CODES[upstream.error] : void 0;
@@ -503,6 +523,15 @@ function createWensityClient(options) {
503
523
  }
504
524
  return await fetchComponentSource(slug, opts);
505
525
  },
526
+ fetchSkillIndex: () => get(joinUrl(registryOrigin(registryUrl), "/api/skills")),
527
+ async fetchSkillPayload(name, tier) {
528
+ const path = `/api/cli/skills/${encodeURIComponent(name)}`;
529
+ if (tier === "pro") {
530
+ if (!token) throw proRequired(name, name, "skill");
531
+ return get(joinUrl(apiBaseUrl, path), true);
532
+ }
533
+ return get(joinUrl(registryOrigin(registryUrl), path));
534
+ },
506
535
  async whoami() {
507
536
  if (!token) {
508
537
  throw new WensityError("auth_required", "No Wensity API token is configured.");
@@ -718,7 +747,7 @@ async function apply(options) {
718
747
  details: { reason: "precondition_mismatch", path: file.relativePath, expected: "present", found: "absent" }
719
748
  });
720
749
  }
721
- return { ...file, absolutePath, existed: Boolean(stat2), mode: stat2 ? stat2.mode & 511 : void 0 };
750
+ return { ...file, absolutePath, existed: Boolean(stat2), requestedMode: file.mode, mode: stat2 ? stat2.mode & 511 : void 0 };
722
751
  });
723
752
  const workDir = ensureWorkDir(projectRoot);
724
753
  const journalDir = join3(workDir, planId);
@@ -769,7 +798,7 @@ async function apply(options) {
769
798
  await writeJournal("in_progress", "staging");
770
799
  for (const [index, target] of targets.entries()) {
771
800
  failedPath = target.relativePath;
772
- await writeSynced(join3(journalDir, "staged", String(index)), target.content, target.mode ?? 420);
801
+ await writeSynced(join3(journalDir, "staged", String(index)), target.content, target.requestedMode ?? target.mode ?? 420);
773
802
  emit({ type: "stage", path: target.relativePath });
774
803
  fault("stage", index);
775
804
  }
@@ -947,7 +976,7 @@ async function place(staged, target, platform, hooks, emit, projectRoot) {
947
976
  } catch (error) {
948
977
  const retryable = isNodeError(error) && WINDOWS_RETRY_CODES.has(error.code ?? "");
949
978
  if (!retryable || attempt >= attempts) throw error;
950
- await new Promise((resolve3) => setTimeout(resolve3, 25 * 2 ** (attempt - 1)));
979
+ await new Promise((resolve4) => setTimeout(resolve4, 25 * 2 ** (attempt - 1)));
951
980
  }
952
981
  }
953
982
  return stagedFile;
@@ -4097,8 +4126,8 @@ function runPackageManagerCommand(projectRoot, packageManager, args, options = {
4097
4126
  const ownGroup = !inherit && platform !== "win32";
4098
4127
  const timeoutMs = options.timeoutMs ?? (inherit ? void 0 : PACKAGE_MANAGER_TIMEOUT_MS);
4099
4128
  const started = Date.now();
4100
- return new Promise((resolve3) => {
4101
- let stdout2 = "";
4129
+ return new Promise((resolve4) => {
4130
+ let stdout3 = "";
4102
4131
  let stderr = "";
4103
4132
  let timedOut = false;
4104
4133
  const child = (options.spawnImpl ?? spawn)(invocation.command, invocation.args, {
@@ -4131,7 +4160,7 @@ function runPackageManagerCommand(projectRoot, packageManager, args, options = {
4131
4160
  killTree();
4132
4161
  }, timeoutMs);
4133
4162
  child.stdout?.on("data", (chunk) => {
4134
- stdout2 = (stdout2 + chunk.toString("utf8")).slice(-OUTPUT_KEEP);
4163
+ stdout3 = (stdout3 + chunk.toString("utf8")).slice(-OUTPUT_KEEP);
4135
4164
  });
4136
4165
  child.stderr?.on("data", (chunk) => {
4137
4166
  stderr = (stderr + chunk.toString("utf8")).slice(-OUTPUT_KEEP);
@@ -4148,7 +4177,7 @@ function runPackageManagerCommand(projectRoot, packageManager, args, options = {
4148
4177
  removeSignalHandlers();
4149
4178
  child.stdout?.destroy();
4150
4179
  child.stderr?.destroy();
4151
- resolve3({ invocation, exitCode, stdout: stdout2, stderr, durationMs: Date.now() - started, timedOut, ...spawnError ? { spawnError } : {} });
4180
+ resolve4({ invocation, exitCode, stdout: stdout3, stderr, durationMs: Date.now() - started, timedOut, ...spawnError ? { spawnError } : {} });
4152
4181
  };
4153
4182
  child.on("error", (error) => void finish(null, error.message));
4154
4183
  child.on("exit", (code) => void finish(code));
@@ -4203,7 +4232,7 @@ function groupAlive(pid) {
4203
4232
  async function waitForGroupExit(pid) {
4204
4233
  const deadline = Date.now() + TREE_EXIT_WAIT_MS;
4205
4234
  while (groupAlive(pid) && Date.now() < deadline) {
4206
- await new Promise((resolve3) => setTimeout(resolve3, 20));
4235
+ await new Promise((resolve4) => setTimeout(resolve4, 20));
4207
4236
  }
4208
4237
  }
4209
4238
  function installArgs(packageManager, dependencies) {
@@ -4498,7 +4527,8 @@ function expectsValue(name) {
4498
4527
  "preset",
4499
4528
  "preset-file",
4500
4529
  "template",
4501
- "package-manager"
4530
+ "package-manager",
4531
+ "agent"
4502
4532
  ].includes(name);
4503
4533
  }
4504
4534
  function normalizeOptionName(name) {
@@ -6163,6 +6193,917 @@ function withSourceExtension(path, extension) {
6163
6193
  return toPosixPath(join10(directory === "." ? "" : directory, `${fileName}.${extension}`));
6164
6194
  }
6165
6195
 
6196
+ // src/skills.ts
6197
+ import { randomBytes as randomBytes2 } from "crypto";
6198
+ import { homedir as homedir2 } from "os";
6199
+ import { stdin as stdin2, stdout as stdout2 } from "process";
6200
+ import { createInterface } from "readline/promises";
6201
+
6202
+ // ../install-core/src/lock.ts
6203
+ import { randomBytes } from "crypto";
6204
+ import { closeSync as closeSync2, lstatSync as lstatSync5, openSync as openSync2, readFileSync as readFileSync5, unlinkSync as unlinkSync2, writeSync as writeSync2 } from "fs";
6205
+ import { join as join11 } from "path";
6206
+ var STALE_LOCK_MS = 10 * 6e4;
6207
+ function acquireInstallLock(projectRoot, options) {
6208
+ const now = options.now ?? Date.now;
6209
+ const alive = options.isProcessAlive ?? isProcessAlive;
6210
+ const dir = ensureWorkDir(projectRoot);
6211
+ const path = join11(dir, LOCK_FILE);
6212
+ const contents = { pid: process.pid, startedAt: now(), planId: options.planId, nonce: randomBytes(16).toString("hex") };
6213
+ for (let attempt = 0; attempt < 2; attempt += 1) {
6214
+ let fd;
6215
+ try {
6216
+ fd = openSync2(path, "wx", 384);
6217
+ } catch (error) {
6218
+ if (!(isNodeError(error) && error.code === "EEXIST")) throw error;
6219
+ const existing = inspectExisting(path);
6220
+ const stale = existing && now() - existing.startedAt > STALE_LOCK_MS && !alive(existing.pid);
6221
+ if (attempt === 0 && stale) {
6222
+ unlinkSync2(path);
6223
+ continue;
6224
+ }
6225
+ throw new WensityError("write_failed", "Another Wensity installation is in progress for this project. Retry when it finishes.", {
6226
+ details: { reason: "install_locked", ...existing ? { heldSince: new Date(existing.startedAt).toISOString() } : {} }
6227
+ });
6228
+ }
6229
+ try {
6230
+ writeSync2(fd, JSON.stringify(contents));
6231
+ } finally {
6232
+ closeSync2(fd);
6233
+ }
6234
+ let released = false;
6235
+ return {
6236
+ release() {
6237
+ if (released) return;
6238
+ released = true;
6239
+ try {
6240
+ const stat2 = lstatSync5(path);
6241
+ if (!stat2.isFile() || stat2.isSymbolicLink()) return;
6242
+ const current = JSON.parse(readFileSync5(path, "utf8"));
6243
+ if (current.nonce === contents.nonce) unlinkSync2(path);
6244
+ } catch {
6245
+ }
6246
+ removeWorkDirIfEmpty(projectRoot);
6247
+ }
6248
+ };
6249
+ }
6250
+ throw new WensityError("write_failed", "Could not acquire the Wensity install lock.");
6251
+ }
6252
+ function inspectExisting(path) {
6253
+ let stat2;
6254
+ try {
6255
+ stat2 = lstatSync5(path);
6256
+ } catch {
6257
+ return void 0;
6258
+ }
6259
+ if (stat2.isSymbolicLink() || !stat2.isFile()) {
6260
+ throw new WensityError(
6261
+ "path_unsafe",
6262
+ `${WORK_DIR_NAME}/${LOCK_FILE} is not a regular file. It was not followed or removed; delete it yourself.`
6263
+ );
6264
+ }
6265
+ try {
6266
+ const parsed = JSON.parse(readFileSync5(path, "utf8"));
6267
+ if (typeof parsed.pid === "number" && typeof parsed.startedAt === "number") return parsed;
6268
+ } catch {
6269
+ }
6270
+ return void 0;
6271
+ }
6272
+ function isProcessAlive(pid) {
6273
+ try {
6274
+ process.kill(pid, 0);
6275
+ return true;
6276
+ } catch (error) {
6277
+ return isNodeError(error) && error.code === "EPERM";
6278
+ }
6279
+ }
6280
+
6281
+ // ../install-core/src/skills.ts
6282
+ import { randomUUID as randomUUID2 } from "crypto";
6283
+ import { existsSync as existsSync5, lstatSync as lstatSync6, readFileSync as readFileSync6 } from "fs";
6284
+ import { mkdir as mkdir7, readFile as readFile8, rename as rename3, rm as rm3, rmdir, unlink, writeFile as writeFile6 } from "fs/promises";
6285
+ import { dirname as dirname8, join as join12, posix as posix4, resolve as resolve3 } from "path";
6286
+ var SKILL_AGENTS = ["claude", "agents"];
6287
+ var SKILL_AGENT_DIRS = {
6288
+ claude: ".claude/skills",
6289
+ agents: ".agents/skills"
6290
+ };
6291
+ var SKILL_AGENT_LABELS = {
6292
+ claude: "Claude Code",
6293
+ agents: "Codex, Cursor, Copilot, Windsurf, Gemini CLI, OpenCode"
6294
+ };
6295
+ var AGENT_MARKERS = {
6296
+ claude: [".claude", "CLAUDE.md"],
6297
+ agents: [".agents", "AGENTS.md", ".codex", ".cursor", ".windsurf", ".gemini", "GEMINI.md", ".opencode", ".github/copilot-instructions.md"]
6298
+ };
6299
+ var SKILL_LOCK_FILE = "wensity-skills.json";
6300
+ function skillAliases(skill) {
6301
+ return [.../* @__PURE__ */ new Set([skill.name, skill.name.replace(/^wensity-/, ""), skill.slug])];
6302
+ }
6303
+ function resolveSkillNames(index, inputs) {
6304
+ const byAlias = /* @__PURE__ */ new Map();
6305
+ for (const skill of index.skills) for (const alias of skillAliases(skill)) byAlias.set(alias, skill);
6306
+ const resolved = [];
6307
+ const unknown = [];
6308
+ for (const input of inputs) {
6309
+ const skill = byAlias.get(input.trim().toLowerCase());
6310
+ if (!skill) unknown.push(input);
6311
+ else if (!resolved.includes(skill)) resolved.push(skill);
6312
+ }
6313
+ if (unknown.length > 0) {
6314
+ const lines = unknown.map((input) => {
6315
+ const suggestions = suggestSkillNames(index, input);
6316
+ return suggestions.length > 0 ? `Unknown skill "${input}". Did you mean ${suggestions.join(", ")}?` : `Unknown skill "${input}".`;
6317
+ });
6318
+ throw new WensityError("item_not_found", `${lines.join("\n")}
6319
+ Run \`wensity skills list\` to see every skill.`);
6320
+ }
6321
+ return resolved;
6322
+ }
6323
+ function suggestSkillNames(index, input) {
6324
+ const wanted = input.trim().toLowerCase().replace(/^wensity-/, "");
6325
+ const scored = index.skills.map((skill) => {
6326
+ const best = Math.min(
6327
+ ...skillAliases(skill).map((alias) => {
6328
+ const short = alias.replace(/^wensity-/, "");
6329
+ if (wanted.length >= 3 && (short.includes(wanted) || wanted.includes(short))) return 0;
6330
+ return editDistance(wanted, short);
6331
+ })
6332
+ );
6333
+ return { name: skill.name.replace(/^wensity-/, ""), best };
6334
+ });
6335
+ const limit = Math.max(2, Math.floor(wanted.length / 3));
6336
+ return scored.filter((entry) => entry.best <= limit).sort((a, b) => a.best - b.best || a.name.localeCompare(b.name)).slice(0, 3).map((entry) => entry.name);
6337
+ }
6338
+ function editDistance(a, b) {
6339
+ const row = Array.from({ length: b.length + 1 }, (_, index) => index);
6340
+ for (let i = 1; i <= a.length; i += 1) {
6341
+ let previous = row[0];
6342
+ row[0] = i;
6343
+ for (let j = 1; j <= b.length; j += 1) {
6344
+ const current = row[j];
6345
+ row[j] = Math.min(row[j] + 1, row[j - 1] + 1, previous + (a[i - 1] === b[j - 1] ? 0 : 1));
6346
+ previous = current;
6347
+ }
6348
+ }
6349
+ return row[b.length];
6350
+ }
6351
+ function compareSkillVersions(a, b) {
6352
+ const x = a.split(".").map((part) => Number.parseInt(part, 10) || 0);
6353
+ const y = b.split(".").map((part) => Number.parseInt(part, 10) || 0);
6354
+ for (let i = 0; i < 3; i += 1) if ((x[i] ?? 0) !== (y[i] ?? 0)) return (x[i] ?? 0) - (y[i] ?? 0);
6355
+ return 0;
6356
+ }
6357
+ function changesBetween(skill, from, to) {
6358
+ return (skill.changes ?? []).filter(
6359
+ (change) => compareSkillVersions(change.version, from) > 0 && compareSkillVersions(change.version, to) <= 0
6360
+ );
6361
+ }
6362
+ function skillsWithUpdates(index, lock, except = []) {
6363
+ return index.skills.filter((skill) => {
6364
+ const entry = lock.skills[skill.name];
6365
+ return entry !== void 0 && !except.includes(skill.name) && compareSkillVersions(entry.version, skill.version) < 0;
6366
+ });
6367
+ }
6368
+ function detectSkillAgents(root) {
6369
+ return SKILL_AGENTS.filter((agent) => AGENT_MARKERS[agent].some((marker) => existsSync5(join12(root, ...marker.split("/")))));
6370
+ }
6371
+ function parseSkillAgent(value) {
6372
+ const normalized = value.trim().toLowerCase();
6373
+ if (normalized === "claude" || normalized === "claude-code") return "claude";
6374
+ if (normalized === "agents" || normalized === "other" || normalized === "codex" || normalized === "cursor") return "agents";
6375
+ throw new WensityError("config_invalid", `Unknown agent "${value}". Use --agent claude (Claude Code) or --agent agents (Codex, Cursor, Copilot, Windsurf, Gemini CLI, OpenCode).`);
6376
+ }
6377
+ function findSkillsProjectRoot(cwd, home) {
6378
+ const start = resolve3(cwd);
6379
+ const stop = home ? resolve3(home) : void 0;
6380
+ let current = start;
6381
+ while (true) {
6382
+ if (current === stop) break;
6383
+ if ([SKILL_LOCK_FILE, "package.json", ".git"].some((marker) => existsSync5(join12(current, marker)))) return current;
6384
+ const parent = dirname8(current);
6385
+ if (parent === current) break;
6386
+ current = parent;
6387
+ }
6388
+ if (start === stop) {
6389
+ throw new WensityError("project_ambiguous", "Your home folder is not a project. Run this from your project folder, or add --global to install for every project.");
6390
+ }
6391
+ if (dirname8(start) === start) {
6392
+ throw new WensityError("project_ambiguous", `${start} is a filesystem root, not a project. Run this from your project folder.`);
6393
+ }
6394
+ return start;
6395
+ }
6396
+ function skillLockPath(options) {
6397
+ if (!options.global) return join12(options.root, SKILL_LOCK_FILE);
6398
+ const env = options.env;
6399
+ if (env.WENSITY_SKILLS_LOCK_FILE) return env.WENSITY_SKILLS_LOCK_FILE;
6400
+ const home = env.HOME;
6401
+ if (!env.WENSITY_CONFIG_HOME && !env.XDG_CONFIG_HOME && !home) {
6402
+ throw new WensityError("config_missing", "HOME is not set, so the CLI cannot find its config folder.");
6403
+ }
6404
+ const configHome = env.WENSITY_CONFIG_HOME ?? env.XDG_CONFIG_HOME ?? join12(home, ".config");
6405
+ return join12(configHome, "wensity", "skills.json");
6406
+ }
6407
+ function skillFolder(agent, name) {
6408
+ return posix4.join(SKILL_AGENT_DIRS[agent], name);
6409
+ }
6410
+ function emptySkillLock() {
6411
+ return { version: 1, skills: {} };
6412
+ }
6413
+ async function readSkillLock(path) {
6414
+ let text;
6415
+ try {
6416
+ const stat2 = lstatSync6(path);
6417
+ if (stat2.isSymbolicLink() || !stat2.isFile()) {
6418
+ throw new WensityError("path_unsafe", `${path} is not a regular file. Remove it and try again.`);
6419
+ }
6420
+ text = await readFile8(path, "utf8");
6421
+ } catch (error) {
6422
+ if (isNodeError(error) && error.code === "ENOENT") return emptySkillLock();
6423
+ throw error;
6424
+ }
6425
+ try {
6426
+ const parsed = JSON.parse(text);
6427
+ if (parsed.version !== 1 || !parsed.skills || typeof parsed.skills !== "object") throw new Error("bad shape");
6428
+ return { version: 1, skills: parsed.skills };
6429
+ } catch {
6430
+ throw new WensityError("config_invalid", `${path} is not a valid skills record. Fix or delete it, then try again.`);
6431
+ }
6432
+ }
6433
+ async function writeSkillLock(path, lock) {
6434
+ if (Object.keys(lock.skills).length === 0) {
6435
+ await rm3(path, { force: true });
6436
+ return;
6437
+ }
6438
+ const sorted = {
6439
+ version: 1,
6440
+ skills: Object.fromEntries(Object.keys(lock.skills).sort().map((name) => [name, lock.skills[name]]))
6441
+ };
6442
+ await mkdir7(dirname8(path), { recursive: true });
6443
+ const temp = join12(dirname8(path), `.wensity-skills.${randomUUID2()}.tmp`);
6444
+ await writeFile6(temp, `${JSON.stringify(sorted, null, 2)}
6445
+ `, "utf8");
6446
+ await rename3(temp, path);
6447
+ }
6448
+ var SAFE_SEGMENT2 = /^[A-Za-z0-9._-]+$/;
6449
+ function sanitizeSkillFilePath(path) {
6450
+ const unsafe3 = () => new WensityError("path_unsafe", `Unsafe skill file path from the server: ${path.slice(0, 120)}`);
6451
+ if (typeof path !== "string" || path.length === 0 || path.length > MAX_TARGET_PATH_LENGTH) throw unsafe3();
6452
+ if (/^[\\/]/.test(path) || /^[A-Za-z]:/.test(path) || path.includes("\\")) throw unsafe3();
6453
+ const parts = path.split("/");
6454
+ for (const part of parts) {
6455
+ if (part === "" || part === "." || part === ".." || !SAFE_SEGMENT2.test(part)) throw unsafe3();
6456
+ }
6457
+ return parts.join("/");
6458
+ }
6459
+ function verifySkillPayload(payload, expectedName) {
6460
+ if (!payload || payload.name !== expectedName || !Array.isArray(payload.files) || payload.files.length === 0) {
6461
+ throw new WensityError("source_integrity_failed", `The server sent an unexpected answer for ${expectedName}. Nothing was written.`);
6462
+ }
6463
+ const seen = /* @__PURE__ */ new Set();
6464
+ const files = payload.files.map((file) => {
6465
+ const path = sanitizeSkillFilePath(file.path);
6466
+ if (seen.has(path.toLowerCase())) throw new WensityError("source_integrity_failed", `${expectedName} lists ${path} twice. Nothing was written.`);
6467
+ seen.add(path.toLowerCase());
6468
+ if (typeof file.content !== "string" || file.content.includes("\0")) {
6469
+ throw new WensityError("source_integrity_failed", `${expectedName}/${path} is not a text file. Nothing was written.`);
6470
+ }
6471
+ if (sha256(file.content) !== file.sha256) {
6472
+ throw new WensityError("source_integrity_failed", `${expectedName}/${path} did not match its checksum. Nothing was written. Try again.`);
6473
+ }
6474
+ const content = file.content.replace(/\r\n?/g, "\n");
6475
+ return { ...file, path, content, sha256: sha256(content), executable: file.executable === true };
6476
+ });
6477
+ if (!files.some((file) => file.path === "SKILL.md")) {
6478
+ throw new WensityError("source_integrity_failed", `${expectedName} has no SKILL.md. Nothing was written.`);
6479
+ }
6480
+ return { ...payload, files };
6481
+ }
6482
+ function assertNoSymlinkedDirs(root, relativePath) {
6483
+ const segments = relativePath.split("/").filter(Boolean);
6484
+ let current = root;
6485
+ for (const segment of segments.slice(0, -1)) {
6486
+ current = join12(current, segment);
6487
+ let stat2;
6488
+ try {
6489
+ stat2 = lstatSync6(current);
6490
+ } catch (error) {
6491
+ if (isNodeError(error) && error.code === "ENOENT") return;
6492
+ throw error;
6493
+ }
6494
+ if (stat2.isSymbolicLink()) {
6495
+ throw new WensityError("path_unsafe", `${toPosixPath(current)} is a symlink. Wensity does not write skills through symlinks; point --agent at a real folder or remove the link.`);
6496
+ }
6497
+ if (!stat2.isDirectory()) {
6498
+ throw new WensityError("path_unsafe", `${toPosixPath(current)} is a file, not a folder. Nothing was written.`);
6499
+ }
6500
+ }
6501
+ }
6502
+ function readDiskHash(absolutePath) {
6503
+ try {
6504
+ const stat2 = lstatSync6(absolutePath);
6505
+ if (stat2.isSymbolicLink() || !stat2.isFile()) {
6506
+ throw new WensityError("path_unsafe", `${toPosixPath(absolutePath)} is not a regular file. Nothing was written.`);
6507
+ }
6508
+ return sha256(readFileSync6(absolutePath));
6509
+ } catch (error) {
6510
+ if (isNodeError(error) && error.code === "ENOENT") return void 0;
6511
+ throw error;
6512
+ }
6513
+ }
6514
+ function planSkillInstall(input) {
6515
+ const { root, payload, previous, force } = input;
6516
+ const writes = [];
6517
+ const deletes = [];
6518
+ const outcomes = [];
6519
+ const targets = { ...previous?.targets ?? {} };
6520
+ for (const agent of input.agents) {
6521
+ const folder = skillFolder(agent, payload.name);
6522
+ const recorded = previous?.targets[agent]?.files ?? {};
6523
+ const nextFiles = {};
6524
+ for (const file of payload.files) {
6525
+ const relativePath = posix4.join(folder, file.path);
6526
+ assertNoSymlinkedDirs(root, relativePath);
6527
+ const absolutePath = resolveInside(root, relativePath);
6528
+ const onDisk = readDiskHash(absolutePath);
6529
+ const wasWritten = recorded[file.path];
6530
+ const edited = onDisk !== void 0 && wasWritten !== void 0 && onDisk !== wasWritten;
6531
+ const stranger = onDisk !== void 0 && wasWritten === void 0 && onDisk !== file.sha256;
6532
+ const mode = file.executable ? 493 : void 0;
6533
+ let status;
6534
+ if (onDisk === void 0) {
6535
+ status = "added";
6536
+ writes.push({ relativePath, content: file.content, action: "create", ...mode ? { mode } : {} });
6537
+ nextFiles[file.path] = file.sha256;
6538
+ } else if (onDisk === file.sha256) {
6539
+ status = "unchanged";
6540
+ nextFiles[file.path] = file.sha256;
6541
+ } else if ((edited || stranger) && !force) {
6542
+ status = edited ? "skipped" : "conflict";
6543
+ if (edited) nextFiles[file.path] = wasWritten;
6544
+ } else {
6545
+ status = "updated";
6546
+ writes.push({ relativePath, content: file.content, action: "overwrite", ...mode ? { mode } : {} });
6547
+ nextFiles[file.path] = file.sha256;
6548
+ }
6549
+ outcomes.push({ agent, relativePath, skillPath: file.path, status });
6550
+ }
6551
+ const current = new Set(payload.files.map((file) => file.path));
6552
+ for (const [skillPath, writtenHash] of Object.entries(recorded)) {
6553
+ if (current.has(skillPath)) continue;
6554
+ const relativePath = posix4.join(folder, sanitizeSkillFilePath(skillPath));
6555
+ assertNoSymlinkedDirs(root, relativePath);
6556
+ const onDisk = readDiskHash(resolveInside(root, relativePath));
6557
+ if (onDisk === void 0) continue;
6558
+ if (onDisk === writtenHash || force) {
6559
+ deletes.push(relativePath);
6560
+ outcomes.push({ agent, relativePath, skillPath, status: "removed" });
6561
+ } else {
6562
+ outcomes.push({ agent, relativePath, skillPath, status: "kept" });
6563
+ }
6564
+ }
6565
+ targets[agent] = { files: nextFiles };
6566
+ }
6567
+ const now = (input.now ?? /* @__PURE__ */ new Date()).toISOString();
6568
+ return {
6569
+ name: payload.name,
6570
+ version: payload.version,
6571
+ ...previous ? { previousVersion: previous.version } : {},
6572
+ writes,
6573
+ deletes,
6574
+ outcomes,
6575
+ entry: {
6576
+ version: payload.version,
6577
+ tier: payload.tier,
6578
+ targets,
6579
+ installedAt: previous?.installedAt ?? now,
6580
+ updatedAt: now
6581
+ }
6582
+ };
6583
+ }
6584
+ function planSkillRemove(root, name, entry) {
6585
+ const deletes = [];
6586
+ const outcomes = [];
6587
+ const folders = [];
6588
+ for (const agent of SKILL_AGENTS) {
6589
+ const target = entry.targets[agent];
6590
+ if (!target) continue;
6591
+ const folder = skillFolder(agent, name);
6592
+ folders.push(folder);
6593
+ for (const [skillPath, writtenHash] of Object.entries(target.files)) {
6594
+ const relativePath = posix4.join(folder, sanitizeSkillFilePath(skillPath));
6595
+ assertNoSymlinkedDirs(root, relativePath);
6596
+ const onDisk = readDiskHash(resolveInside(root, relativePath));
6597
+ if (onDisk === void 0) continue;
6598
+ if (onDisk === writtenHash) {
6599
+ deletes.push(relativePath);
6600
+ outcomes.push({ agent, relativePath, skillPath, status: "removed" });
6601
+ } else {
6602
+ outcomes.push({ agent, relativePath, skillPath, status: "kept" });
6603
+ }
6604
+ }
6605
+ }
6606
+ return { name, deletes, outcomes, folders };
6607
+ }
6608
+ async function deleteSkillFiles(root, relativePaths, folders = []) {
6609
+ const parents = /* @__PURE__ */ new Set();
6610
+ for (const relativePath of relativePaths) {
6611
+ assertNoSymlinkedDirs(root, relativePath);
6612
+ const absolutePath = resolveInside(root, relativePath);
6613
+ try {
6614
+ const stat2 = lstatSync6(absolutePath);
6615
+ if (!stat2.isFile()) continue;
6616
+ await unlink(absolutePath);
6617
+ } catch (error) {
6618
+ if (!(isNodeError(error) && error.code === "ENOENT")) throw error;
6619
+ }
6620
+ let dir = posix4.dirname(relativePath);
6621
+ while (folders.some((folder) => dir === folder || dir.startsWith(`${folder}/`))) {
6622
+ parents.add(dir);
6623
+ dir = posix4.dirname(dir);
6624
+ }
6625
+ }
6626
+ for (const folder of folders) parents.add(folder);
6627
+ for (const dir of [...parents].sort((a, b) => b.split("/").length - a.split("/").length)) {
6628
+ try {
6629
+ const absolute = resolveInside(root, dir);
6630
+ if (lstatSync6(absolute).isDirectory()) await rmdir(absolute);
6631
+ } catch {
6632
+ }
6633
+ }
6634
+ }
6635
+
6636
+ // src/skills.ts
6637
+ var PRICING_URL2 = "https://ui.wensity.com/pricing";
6638
+ var LOGIN_COMMAND = "npx wensity@latest login";
6639
+ var REQUEST_TIMEOUT_MS = 3e4;
6640
+ var SKILLS_USAGE = `Usage:
6641
+ wensity skills add <name...> [--all] [--agent claude|agents] [--global] [--dry-run] [--yes] [--force]
6642
+ wensity skills list [--global] [--json]
6643
+ wensity skills update [name...] [--global] [--dry-run] [--force]
6644
+ wensity skills remove <name...> [--global] [--dry-run]`;
6645
+ async function skillsCommand(context, parsed, positionals) {
6646
+ const [subcommand, ...names] = positionals;
6647
+ if (parsed.options.has("token")) {
6648
+ throw new CliError(`Skills commands never take a token. Run \`${LOGIN_COMMAND}\` once; the saved login is used from then on.`);
6649
+ }
6650
+ if (!subcommand || !["add", "list", "update", "remove"].includes(subcommand)) {
6651
+ throw new CliError(`${subcommand ? `Unknown skills command: ${subcommand}
6652
+
6653
+ ` : ""}${SKILLS_USAGE}`);
6654
+ }
6655
+ const setup = createSkillsContext(context, parsed, subcommand === "list");
6656
+ switch (subcommand) {
6657
+ case "add":
6658
+ return addSkills(setup, names);
6659
+ case "update":
6660
+ return updateSkills(setup, names);
6661
+ case "remove":
6662
+ return removeSkills(setup, names);
6663
+ default:
6664
+ return listSkills(setup);
6665
+ }
6666
+ }
6667
+ function createSkillsContext(context, parsed, lenient) {
6668
+ const global = hasFlag(parsed, "global");
6669
+ const home = context.env.HOME ?? homedir2();
6670
+ let root;
6671
+ if (global) {
6672
+ root = home;
6673
+ } else {
6674
+ try {
6675
+ root = findSkillsProjectRoot(context.cwd, home);
6676
+ } catch (error) {
6677
+ if (!lenient) throw error;
6678
+ root = context.cwd;
6679
+ }
6680
+ }
6681
+ const apiUrl = getStringOption(parsed, "api-url") ?? context.env.WENSITY_API_URL ?? DEFAULT_API_ORIGIN;
6682
+ return {
6683
+ context,
6684
+ parsed,
6685
+ global,
6686
+ root,
6687
+ lockPath: skillLockPath({ global, root, env: context.env }),
6688
+ origin: normalizeUrlOrigin(apiUrl),
6689
+ dryRun: hasFlag(parsed, "dry-run"),
6690
+ force: hasFlag(parsed, "force")
6691
+ };
6692
+ }
6693
+ async function fetchIndex(setup) {
6694
+ const url = joinUrl(setup.origin, "/api/skills");
6695
+ try {
6696
+ return await requestJson2(url, { timeoutMs: REQUEST_TIMEOUT_MS, breakers: false });
6697
+ } catch (error) {
6698
+ throw explain(error, "the skills list");
6699
+ }
6700
+ }
6701
+ async function savedToken(setup) {
6702
+ const credentials = await readCredentials(setup.context.env);
6703
+ if (!credentials) return void 0;
6704
+ try {
6705
+ return normalizeUrlOrigin(credentials.apiBaseUrl) === setup.origin ? credentials.token : void 0;
6706
+ } catch {
6707
+ return void 0;
6708
+ }
6709
+ }
6710
+ async function fetchPayload(setup, skill, token) {
6711
+ const url = joinUrl(setup.origin, `/api/cli/skills/${encodeURIComponent(skill.name)}`);
6712
+ try {
6713
+ const payload = await requestJson2(url, {
6714
+ timeoutMs: REQUEST_TIMEOUT_MS,
6715
+ breakers: false,
6716
+ // Free skills never carry the token; Pro skills only to the origin it was issued for.
6717
+ ...skill.tier === "pro" && token ? { token } : {}
6718
+ });
6719
+ return verifySkillPayload(payload, skill.name);
6720
+ } catch (error) {
6721
+ throw explain(error, skill.name);
6722
+ }
6723
+ }
6724
+ var DOWNLOAD_CONCURRENCY = 4;
6725
+ async function mapLimit(items, limit, task) {
6726
+ const results = new Array(items.length);
6727
+ let next = 0;
6728
+ const workers = Array.from({ length: Math.min(limit, items.length) }, async () => {
6729
+ while (next < items.length) {
6730
+ const index = next;
6731
+ next += 1;
6732
+ results[index] = await task(items[index]);
6733
+ }
6734
+ });
6735
+ await Promise.all(workers);
6736
+ return results;
6737
+ }
6738
+ async function downloadSkills(setup, skills, token, options) {
6739
+ if (skills.length === 0) return { payloads: [] };
6740
+ const label = skills.length === 1 ? skills[0].name : `${skills.length} skills`;
6741
+ return withSpinner(`${options.verb} ${label}`, async () => {
6742
+ const byName = /* @__PURE__ */ new Map();
6743
+ const fetchOne = async (skill) => byName.set(skill.name, await fetchPayload(setup, skill, token));
6744
+ const free = skills.filter((skill) => skill.tier === "free");
6745
+ const pro = skills.filter((skill) => skill.tier === "pro");
6746
+ let proDenied;
6747
+ const freeDone = mapLimit(free, DOWNLOAD_CONCURRENCY, fetchOne);
6748
+ if (pro.length > 0) {
6749
+ try {
6750
+ await fetchOne(pro[0]);
6751
+ await mapLimit(pro.slice(1), DOWNLOAD_CONCURRENCY, fetchOne);
6752
+ } catch (error) {
6753
+ if (!(options.skipProWhenDenied && isProAccessDenied(error))) {
6754
+ await freeDone.catch(() => void 0);
6755
+ throw error;
6756
+ }
6757
+ proDenied = error.message;
6758
+ }
6759
+ }
6760
+ await freeDone;
6761
+ const payloads = skills.flatMap((skill) => {
6762
+ const payload = byName.get(skill.name);
6763
+ return payload ? [payload] : [];
6764
+ });
6765
+ return proDenied ? { payloads, proDenied } : { payloads };
6766
+ });
6767
+ }
6768
+ function proAccessDenied(message) {
6769
+ return Object.assign(new CliError(message), { proAccessDenied: true });
6770
+ }
6771
+ function isProAccessDenied(error) {
6772
+ return typeof error === "object" && error !== null && error.proAccessDenied === true;
6773
+ }
6774
+ function explain(error, what) {
6775
+ if (!isWensityError(error)) return error;
6776
+ const upstream = error.upstream;
6777
+ if (error.code === "entitlement_required") {
6778
+ const message = upstream?.message ?? `${what} comes with Wensity Pro.`;
6779
+ return proAccessDenied(message.includes(PRICING_URL2) ? message : `${message}
6780
+ Pricing: ${pricingBlue(PRICING_URL2)}`);
6781
+ }
6782
+ if (error.code === "auth_invalid" || error.code === "auth_revoked" || error.code === "auth_user_missing") {
6783
+ if (upstream?.error === "missing_token") return new CliError(`${what} comes with Wensity Pro. Log in first: ${LOGIN_COMMAND}`);
6784
+ return new CliError(`Your saved Wensity login was not accepted (${upstream?.message ?? error.message}).
6785
+ Log in again: ${LOGIN_COMMAND}`);
6786
+ }
6787
+ if (error.code === "network_error") {
6788
+ return new CliError(`Could not reach Wensity to get ${what}. Check your connection and try again.
6789
+ ${error.message}`);
6790
+ }
6791
+ if (error.code === "rate_limited") {
6792
+ return new CliError(`Too many requests right now. Wait a minute and try again.`);
6793
+ }
6794
+ return error;
6795
+ }
6796
+ function explicitAgents(argv) {
6797
+ const values = [];
6798
+ for (let index = 0; index < argv.length; index += 1) {
6799
+ const token = argv[index];
6800
+ if (token === "--agent") {
6801
+ const value = argv[index + 1];
6802
+ if (!value || value.startsWith("--")) throw new CliError("--agent expects claude or agents.");
6803
+ values.push(value);
6804
+ index += 1;
6805
+ } else if (token.startsWith("--agent=")) {
6806
+ values.push(token.slice("--agent=".length));
6807
+ }
6808
+ }
6809
+ const agents = values.flatMap((value) => value.split(",")).filter(Boolean).map((value) => parseSkillAgent(value));
6810
+ return SKILL_AGENTS.filter((agent) => agents.includes(agent));
6811
+ }
6812
+ async function chooseAgents(setup) {
6813
+ const explicit = explicitAgents(setup.context.argv);
6814
+ if (explicit.length > 0) return explicit;
6815
+ const detected = detectSkillAgents(setup.root);
6816
+ if (detected.length > 0) return detected;
6817
+ const interactive = stdin2.isTTY === true && stdout2.isTTY === true && !hasFlag(setup.parsed, "yes");
6818
+ if (!interactive) return [...SKILL_AGENTS];
6819
+ const where = setup.global ? "in your home folder" : "in this project";
6820
+ const rl = createInterface({ input: stdin2, output: stdout2 });
6821
+ try {
6822
+ const answer = await rl.question(
6823
+ [
6824
+ `No agent folders found ${where}. Which agents should get the skills?`,
6825
+ ` 1) ${SKILL_AGENT_LABELS.claude} (${SKILL_AGENT_DIRS.claude})`,
6826
+ ` 2) ${SKILL_AGENT_LABELS.agents} (${SKILL_AGENT_DIRS.agents})`,
6827
+ " 3) Both",
6828
+ "Choose 1, 2 or 3 [3]: "
6829
+ ].join("\n")
6830
+ );
6831
+ const choice = answer.trim();
6832
+ if (choice === "1") return ["claude"];
6833
+ if (choice === "2") return ["agents"];
6834
+ return [...SKILL_AGENTS];
6835
+ } finally {
6836
+ rl.close();
6837
+ }
6838
+ }
6839
+ async function addSkills(setup, names) {
6840
+ const all = hasFlag(setup.parsed, "all");
6841
+ if (!all && names.length === 0) throw new CliError(`Name at least one skill, or use --all.
6842
+
6843
+ ${SKILLS_USAGE}`);
6844
+ if (all && names.length > 0) throw new CliError("Use either skill names or --all, not both.");
6845
+ const index = await fetchIndex(setup);
6846
+ let wanted = all ? [...index.skills] : resolveSkillNames(index, names);
6847
+ const notes = [];
6848
+ const token = wanted.some((skill) => skill.tier === "pro") ? await savedToken(setup) : void 0;
6849
+ const pro = wanted.filter((skill) => skill.tier === "pro");
6850
+ if (pro.length > 0 && !token) {
6851
+ if (!all) {
6852
+ throw new CliError(
6853
+ `${pro.map((skill) => skill.name).join(", ")} ${pro.length === 1 ? "comes" : "come"} with Wensity Pro. Log in first: ${LOGIN_COMMAND}
6854
+ Pricing: ${pricingBlue(PRICING_URL2)}`
6855
+ );
6856
+ }
6857
+ wanted = wanted.filter((skill) => skill.tier !== "pro");
6858
+ notes.push(`Pro skills were left out. Log in to add them: ${LOGIN_COMMAND}`);
6859
+ }
6860
+ const { payloads, proDenied } = await downloadSkills(setup, wanted, token, { verb: "Downloading", skipProWhenDenied: all });
6861
+ if (proDenied) notes.push(`Pro skills were left out: ${proDenied}`);
6862
+ const agents = await chooseAgents(setup);
6863
+ await withInstallLock(setup, async () => {
6864
+ const lock = await readSkillLock(setup.lockPath);
6865
+ const plans = payloads.map(
6866
+ (payload) => planSkillInstall({ root: setup.root, payload, agents, previous: lock.skills[payload.name], force: setup.force })
6867
+ );
6868
+ assertNoConflicts2(plans);
6869
+ if (setup.dryRun) return printPlans(setup, plans, "Skills install preview", notes);
6870
+ await commit(setup, plans, lock);
6871
+ printPlans(setup, plans, payloads.length === 1 ? "Skill installed" : "Skills installed", notes);
6872
+ printNextStep(setup, agents);
6873
+ printUpdateNudge(setup, index, lock, payloads.map((payload) => payload.name));
6874
+ });
6875
+ }
6876
+ async function withInstallLock(setup, task) {
6877
+ if (setup.dryRun) return task();
6878
+ const lock = acquireInstallLock(setup.root, { planId: `skills_${randomBytes2(8).toString("hex")}` });
6879
+ try {
6880
+ return await task();
6881
+ } finally {
6882
+ lock.release();
6883
+ }
6884
+ }
6885
+ function assertNoConflicts2(plans) {
6886
+ const conflicts = plans.flatMap((plan) => plan.outcomes.filter((outcome) => outcome.status === "conflict").map((outcome) => outcome.relativePath));
6887
+ if (conflicts.length > 0) {
6888
+ throw new WensityError(
6889
+ "file_conflict",
6890
+ `These files are already there and were not written by Wensity. Nothing was changed. Use --force to replace them:
6891
+ ${conflicts.map((path) => ` - ${path}`).join("\n")}`
6892
+ );
6893
+ }
6894
+ }
6895
+ async function commit(setup, plans, lock) {
6896
+ const writes = plans.flatMap((plan) => plan.writes);
6897
+ if (writes.length > 0) {
6898
+ const result = await apply({ projectRoot: setup.root, planId: `skills_${randomBytes2(8).toString("hex")}`, files: writes });
6899
+ if (result.status !== "committed") {
6900
+ const where = result.failedPath ? ` while writing ${result.failedPath}` : "";
6901
+ const reverted = result.status === "rolled_back" ? "every file was put back as it was" : `some files could not be restored; see ${result.journalPath}`;
6902
+ throw new WensityError(result.error.code, `Installing skills failed${where}; ${reverted}.
6903
+ ${result.error.message}`);
6904
+ }
6905
+ if (result.cleanupWarning) warn(result.cleanupWarning);
6906
+ }
6907
+ await deleteSkillFiles(
6908
+ setup.root,
6909
+ plans.flatMap((plan) => plan.deletes),
6910
+ plans.flatMap((plan) => SKILL_AGENTS.filter((agent) => plan.entry.targets[agent]).map((agent) => skillFolder(agent, plan.name)))
6911
+ );
6912
+ for (const plan of plans) lock.skills[plan.name] = plan.entry;
6913
+ await writeSkillLock(setup.lockPath, lock);
6914
+ }
6915
+ async function updateSkills(setup, names) {
6916
+ const lock = await readSkillLock(setup.lockPath);
6917
+ const installed = Object.keys(lock.skills);
6918
+ if (installed.length === 0) {
6919
+ throw new CliError(`No Wensity skills are installed ${setup.global ? "globally" : "in this project"}. Add one with: npx wensity@latest skills add <name>`);
6920
+ }
6921
+ const index = await fetchIndex(setup);
6922
+ const wanted = names.length === 0 ? index.skills.filter((skill) => installed.includes(skill.name)) : resolveSkillNames(index, names);
6923
+ const missing = wanted.filter((skill) => !lock.skills[skill.name]);
6924
+ if (missing.length > 0) {
6925
+ throw new CliError(`${missing.map((skill) => skill.name).join(", ")} ${missing.length === 1 ? "is" : "are"} not installed here. Add with: npx wensity@latest skills add ${missing.map((skill) => skill.name.replace(/^wensity-/, "")).join(" ")}`);
6926
+ }
6927
+ const gone = installed.filter((name) => names.length === 0 && !index.skills.some((skill) => skill.name === name));
6928
+ const notes = gone.map((name) => `${name} is no longer in the pack. It was left as it is; remove it with: npx wensity@latest skills remove ${name}`);
6929
+ const token = wanted.some((skill) => skill.tier === "pro") ? await savedToken(setup) : void 0;
6930
+ const lockedPro = wanted.filter((skill) => skill.tier === "pro");
6931
+ if (lockedPro.length > 0 && !token) {
6932
+ throw new CliError(`Updating ${lockedPro.map((skill) => skill.name).join(", ")} needs your Wensity Pro login. Log in first: ${LOGIN_COMMAND}`);
6933
+ }
6934
+ const { payloads } = await downloadSkills(setup, wanted, token, { verb: "Checking", skipProWhenDenied: false });
6935
+ await withInstallLock(setup, async () => {
6936
+ const current = await readSkillLock(setup.lockPath);
6937
+ const plans = payloads.map((payload) => {
6938
+ const previous = current.skills[payload.name] ?? lock.skills[payload.name];
6939
+ const agents2 = SKILL_AGENTS.filter((agent) => previous.targets[agent]);
6940
+ return planSkillInstall({ root: setup.root, payload, agents: agents2, previous, force: setup.force });
6941
+ });
6942
+ assertNoConflicts2(plans);
6943
+ if (setup.dryRun) {
6944
+ printPlans(setup, plans, "Skills update preview", notes);
6945
+ printChanges(index, plans);
6946
+ return;
6947
+ }
6948
+ await commit(setup, plans, current);
6949
+ const changed = plans.some((plan) => plan.writes.length > 0 || plan.deletes.length > 0 || plan.previousVersion !== plan.version);
6950
+ printPlans(setup, plans, changed ? "Skills updated" : "Skills are up to date", notes);
6951
+ printChanges(index, plans);
6952
+ const agents = SKILL_AGENTS.filter((agent) => plans.some((plan) => plan.entry.targets[agent]));
6953
+ if (changed) printNextStep(setup, agents);
6954
+ printUpdateNudge(setup, index, current, plans.map((plan) => plan.name));
6955
+ });
6956
+ }
6957
+ async function removeSkills(setup, names) {
6958
+ if (names.length === 0) throw new CliError(`Name at least one skill to remove.
6959
+
6960
+ ${SKILLS_USAGE}`);
6961
+ const lock = await readSkillLock(setup.lockPath);
6962
+ const byAlias = /* @__PURE__ */ new Map();
6963
+ for (const name of Object.keys(lock.skills)) {
6964
+ byAlias.set(name, name);
6965
+ byAlias.set(name.replace(/^wensity-/, ""), name);
6966
+ }
6967
+ let resolved = [];
6968
+ const unresolved = names.filter((input) => {
6969
+ const name = byAlias.get(input.trim().toLowerCase());
6970
+ if (name && !resolved.includes(name)) resolved.push(name);
6971
+ return !name;
6972
+ });
6973
+ if (unresolved.length > 0) {
6974
+ const index = await fetchIndex(setup).catch(() => void 0);
6975
+ const known = index ? resolveSkillNames(index, unresolved) : [];
6976
+ const notInstalled = known.filter((skill) => !lock.skills[skill.name]);
6977
+ if (!index || notInstalled.length > 0) {
6978
+ const label = index ? notInstalled.map((skill) => skill.name).join(", ") : unresolved.join(", ");
6979
+ throw new CliError(`${label} ${index && notInstalled.length === 1 ? "is" : "are"} not installed ${setup.global ? "globally" : "in this project"}.`);
6980
+ }
6981
+ resolved = [...resolved, ...known.map((skill) => skill.name).filter((name) => !resolved.includes(name))];
6982
+ }
6983
+ await withInstallLock(setup, async () => {
6984
+ const current = await readSkillLock(setup.lockPath);
6985
+ const plans = resolved.filter((name) => current.skills[name]).map((name) => planSkillRemove(setup.root, name, current.skills[name]));
6986
+ printTitle(setup.dryRun ? "Skills remove preview" : "Skills removed");
6987
+ for (const plan of plans) {
6988
+ const removed = plan.outcomes.filter((outcome) => outcome.status === "removed");
6989
+ const kept = plan.outcomes.filter((outcome) => outcome.status === "kept");
6990
+ if (removed.length > 0) {
6991
+ print(success(`${setup.dryRun ? "Would remove" : "Removed"} ${plan.name} (${removed.length} ${removed.length === 1 ? "file" : "files"})`));
6992
+ } else {
6993
+ print(success(`${setup.dryRun ? "Would stop" : "Stopped"} tracking ${plan.name}`));
6994
+ }
6995
+ if (kept.length > 0) {
6996
+ warn(`${plan.name}: you edited ${kept.length === 1 ? "this file" : "these files"}, so ${kept.length === 1 ? "it was" : "they were"} left in place:
6997
+ ${formatList(kept.map((outcome) => outcome.relativePath))}`);
6998
+ }
6999
+ }
7000
+ if (setup.dryRun) return;
7001
+ for (const plan of plans) {
7002
+ await deleteSkillFiles(setup.root, plan.deletes, plan.folders);
7003
+ delete current.skills[plan.name];
7004
+ }
7005
+ await writeSkillLock(setup.lockPath, current);
7006
+ });
7007
+ }
7008
+ async function listSkills(setup) {
7009
+ const [index, lock] = await Promise.all([fetchIndex(setup), readSkillLock(setup.lockPath)]);
7010
+ const rows = index.skills.map((skill) => {
7011
+ const entry = lock.skills[skill.name];
7012
+ const status = !entry ? "" : entry.version === skill.version ? "Up to date" : "Update available";
7013
+ return { skill, entry, status };
7014
+ });
7015
+ if (hasFlag(setup.parsed, "json")) {
7016
+ printJson({
7017
+ pack: index.pack,
7018
+ scope: setup.global ? "global" : "project",
7019
+ skills: rows.map(({ skill, entry, status }) => ({
7020
+ name: skill.name,
7021
+ aliases: skillAliases(skill),
7022
+ tier: skill.tier,
7023
+ title: skill.title,
7024
+ latestVersion: skill.version,
7025
+ installedVersion: entry?.version ?? null,
7026
+ agents: entry ? SKILL_AGENTS.filter((agent) => entry.targets[agent]) : [],
7027
+ updateAvailable: status === "Update available"
7028
+ }))
7029
+ });
7030
+ return;
7031
+ }
7032
+ printTitle(index.pack.title, index.pack.version);
7033
+ print(
7034
+ table(
7035
+ ["Skill", "Title", "Access", "Installed", "Latest", "Status"],
7036
+ rows.map(({ skill, entry, status }) => [
7037
+ skill.name.replace(/^wensity-/, ""),
7038
+ skill.title,
7039
+ badge(skill.tier === "pro" ? "Pro" : "Free", skill.tier),
7040
+ entry?.version ?? "-",
7041
+ skill.version,
7042
+ status
7043
+ ])
7044
+ )
7045
+ );
7046
+ const updates = rows.filter((row) => row.status === "Update available").length;
7047
+ print(
7048
+ nextSteps([
7049
+ updates > 0 ? `Update installed skills: npx wensity@latest skills update${setup.global ? " --global" : ""}` : "Add a skill: npx wensity@latest skills add <skill>",
7050
+ `Pro skills need a login: ${LOGIN_COMMAND}`
7051
+ ])
7052
+ );
7053
+ }
7054
+ var STATUS_WORDS = {
7055
+ added: "added",
7056
+ updated: "updated",
7057
+ unchanged: "unchanged",
7058
+ skipped: "skipped (you edited it)",
7059
+ removed: "removed",
7060
+ kept: "kept (you edited it)"
7061
+ };
7062
+ function printPlans(setup, plans, title, notes) {
7063
+ printTitle(title, setup.global ? "global" : toPosixPath(setup.root));
7064
+ for (const plan of plans) {
7065
+ const counts = /* @__PURE__ */ new Map();
7066
+ for (const outcome of plan.outcomes) counts.set(outcome.status, (counts.get(outcome.status) ?? 0) + 1);
7067
+ const summary = Object.keys(STATUS_WORDS).filter((status) => counts.has(status)).map((status) => `${counts.get(status)} ${STATUS_WORDS[status]}`).join(", ");
7068
+ const versions = plan.previousVersion && plan.previousVersion !== plan.version ? `${plan.previousVersion} to ${plan.version}` : plan.version;
7069
+ const agents = SKILL_AGENTS.filter((agent) => plan.outcomes.some((outcome) => outcome.agent === agent)).map((agent) => SKILL_AGENT_DIRS[agent]).join(", ");
7070
+ print(success(`${plan.name} ${versions} in ${agents}: ${summary}`));
7071
+ const changed = plan.outcomes.filter((outcome) => outcome.status !== "unchanged");
7072
+ if (setup.dryRun && changed.length > 0) {
7073
+ print(formatList(changed.map((outcome) => `${outcome.relativePath} ${STATUS_WORDS[outcome.status] ?? outcome.status}`)));
7074
+ }
7075
+ const skipped = plan.outcomes.filter((outcome) => outcome.status === "skipped" || outcome.status === "kept");
7076
+ if (skipped.length > 0) {
7077
+ warn(`${plan.name}: left your edited ${skipped.length === 1 ? "file" : "files"} as ${skipped.length === 1 ? "it is" : "they are"}. Use --force to replace:
7078
+ ${formatList(skipped.map((outcome) => outcome.relativePath))}`);
7079
+ }
7080
+ }
7081
+ if (setup.dryRun) print(action("Dry run: nothing was written."));
7082
+ for (const note of notes) warn(note);
7083
+ }
7084
+ function printChanges(index, plans) {
7085
+ for (const plan of plans) {
7086
+ if (!plan.previousVersion || plan.previousVersion === plan.version) continue;
7087
+ const skill = index.skills.find((entry) => entry.name === plan.name);
7088
+ const changes = skill ? changesBetween(skill, plan.previousVersion, plan.version) : [];
7089
+ if (changes.length === 0) continue;
7090
+ print(action(`What changed in ${plan.name}:`));
7091
+ print(formatList(changes.flatMap((change) => change.notes.map((note) => `${change.version} ${note}`))));
7092
+ }
7093
+ }
7094
+ function printUpdateNudge(setup, index, lock, except) {
7095
+ const outdated = skillsWithUpdates(index, lock, except);
7096
+ if (outdated.length === 0) return;
7097
+ const what = outdated.length === 1 ? `${outdated[0].name} has an update` : `${outdated.length} skills have updates`;
7098
+ print(action(`${what}. Run: npx wensity@latest skills update${setup.global ? " --global" : ""}`));
7099
+ }
7100
+ function printNextStep(setup, agents) {
7101
+ const steps = [];
7102
+ if (agents.includes("claude")) steps.push("Restart Claude Code to load new skills.");
7103
+ if (agents.includes("agents")) steps.push("Start a new session in your agent to load new skills.");
7104
+ print(nextSteps(steps));
7105
+ }
7106
+
6166
7107
  // src/commands.ts
6167
7108
  async function run(context) {
6168
7109
  const parsed = parseArgs(context.argv);
@@ -6203,6 +7144,9 @@ async function run(context) {
6203
7144
  case "update":
6204
7145
  await addCommand(context, parsed, positionals, true);
6205
7146
  return;
7147
+ case "skills":
7148
+ await skillsCommand(context, parsed, positionals);
7149
+ return;
6206
7150
  case "version":
6207
7151
  print(CLI_VERSION);
6208
7152
  return;
@@ -6271,7 +7215,7 @@ async function initCommand(context, parsed) {
6271
7215
  const templateFiles = template ? await scaffoldProjectTemplate(context.cwd, template, { dryRun, force }) : [];
6272
7216
  const packageManagerFiles = template ? prepareProjectPackageManager(context.cwd, { ...installOptions, dryRun }) : [];
6273
7217
  const config = template ? applyTemplateToConfig(createDefaultConfig(context.cwd, apiUrl), template) : createDefaultConfig(context.cwd, apiUrl);
6274
- const configPath = join11(context.cwd, CONFIG_FILE_NAME);
7218
+ const configPath = join13(context.cwd, CONFIG_FILE_NAME);
6275
7219
  const utilityPath = resolveConfiguredFilePath(context.cwd, config.utils);
6276
7220
  const utilityRelativePath = toPosixPath(config.utils.endsWith(".ts") ? config.utils : `${config.utils}.ts`);
6277
7221
  if (dryRun) {
@@ -6321,7 +7265,7 @@ async function initWithPreset(context, options) {
6321
7265
  presetCode: options.presetCode,
6322
7266
  presetFile: options.presetFile
6323
7267
  });
6324
- const configPath = join11(context.cwd, CONFIG_FILE_NAME);
7268
+ const configPath = join13(context.cwd, CONFIG_FILE_NAME);
6325
7269
  const configExists = await fileExists(configPath);
6326
7270
  const templateFiles = options.template ? await scaffoldProjectTemplate(context.cwd, options.template, {
6327
7271
  dryRun: options.dryRun,
@@ -6541,7 +7485,7 @@ async function preparePresetStyleApplication(projectRoot, config, style, operati
6541
7485
  throw new CliError("No Tailwind CSS path configured. Set tailwind.css in wensity.json.");
6542
7486
  }
6543
7487
  const cssAbsolute = resolveInside(projectRoot, cssRelative);
6544
- const existingCss = await fileExists(cssAbsolute) ? await readFile8(cssAbsolute, "utf8") : "";
7488
+ const existingCss = await fileExists(cssAbsolute) ? await readFile9(cssAbsolute, "utf8") : "";
6545
7489
  const withBase = upsertBaseCssBlock(existingCss, buildBaseCssBlock(baseColorFromStyle(style)));
6546
7490
  const withTokens = upsertPrimitiveCssBlock(withBase, primitiveCss);
6547
7491
  const nextCss = upsertFontImportBlock(withTokens, fontPlan.block);
@@ -6780,11 +7724,11 @@ async function writeWithRollback(projectRoot, files) {
6780
7724
  }
6781
7725
  const result = await apply({
6782
7726
  projectRoot,
6783
- planId: `cli_${randomBytes(8).toString("hex")}`,
7727
+ planId: `cli_${randomBytes3(8).toString("hex")}`,
6784
7728
  files: [...byPath.values()].map((file) => ({
6785
7729
  relativePath: toPosixPath(relative8(projectRoot, file.absolutePath)),
6786
7730
  content: file.content,
6787
- action: existsSync5(file.absolutePath) ? file.kind === "config" ? "modify" : "overwrite" : "create"
7731
+ action: existsSync6(file.absolutePath) ? file.kind === "config" ? "modify" : "overwrite" : "create"
6788
7732
  }))
6789
7733
  });
6790
7734
  if (result.status === "committed") {
@@ -6906,8 +7850,8 @@ function formatAccountPanel(whoami, savedPath) {
6906
7850
  return panel(isPro ? "Wensity Pro" : "Wensity Free", lines);
6907
7851
  }
6908
7852
  async function writeTextFile(path, content) {
6909
- await mkdir7(dirname8(path), { recursive: true });
6910
- await writeFile6(path, content, "utf8");
7853
+ await mkdir8(dirname9(path), { recursive: true });
7854
+ await writeFile7(path, content, "utf8");
6911
7855
  }
6912
7856
  function installOptionsFrom(context, parsed) {
6913
7857
  return {
@@ -6918,7 +7862,7 @@ function installOptionsFrom(context, parsed) {
6918
7862
  async function assertInitPostconditions(projectRoot, relativePaths) {
6919
7863
  const missing = [];
6920
7864
  for (const relativePath of new Set(relativePaths)) {
6921
- const absolute = join11(projectRoot, relativePath);
7865
+ const absolute = join13(projectRoot, relativePath);
6922
7866
  let size = 0;
6923
7867
  try {
6924
7868
  size = (await stat(absolute)).size;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wensity",
3
- "version": "0.1.14",
3
+ "version": "0.1.15",
4
4
  "description": "Install React components, UI primitives, and shareable Tailwind CSS presets into your Next.js project as source code you own.",
5
5
  "license": "MIT",
6
6
  "author": "Wensity <hey@wensity.com> (https://wensity.com)",
@@ -53,7 +53,7 @@
53
53
  "lucide-react": "^1.24.0",
54
54
  "tsup": "^8.3.5",
55
55
  "typescript": "^5.7.2",
56
- "vitest": "^4.1.5"
56
+ "vitest": "^4.1.12"
57
57
  },
58
58
  "publishConfig": {
59
59
  "access": "public"