wensity 0.1.11 → 0.1.13

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 +169 -40
  2. package/dist/index.js +2289 -647
  3. package/package.json +25 -12
package/dist/index.js CHANGED
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/commands.ts
4
- import { mkdir as mkdir6, readFile as readFile6, writeFile as writeFile6 } from "fs/promises";
5
- import { dirname as dirname7, join as join7 } from "path";
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";
6
8
 
7
- // src/constants.ts
8
- var CLI_NAME = "wensity";
9
- var CLI_VERSION = "0.1.11";
9
+ // ../install-core/src/constants.ts
10
10
  var CONFIG_FILE_NAME = "wensity.json";
11
11
  var DEFAULT_API_ORIGIN = "https://ui.wensity.com";
12
12
  var DEFAULT_REGISTRY_PATH = "/api/registry";
@@ -14,6 +14,10 @@ var DEFAULT_COMPONENTS_DIR = "src/components/wensity";
14
14
  var DEFAULT_UTILS_PATH = "src/lib/utils";
15
15
  var DEFAULT_COMPONENTS_ALIAS = "@/components/wensity";
16
16
  var DEFAULT_UTILS_ALIAS = "@/lib/utils";
17
+
18
+ // src/constants.ts
19
+ var CLI_NAME = "wensity";
20
+ var CLI_VERSION = "0.1.13";
17
21
  var INIT_PRIMITIVE_CSS_SLUG = "button";
18
22
  var HELP_TEXT = `Wensity CLI ${CLI_VERSION}
19
23
 
@@ -25,8 +29,8 @@ Usage:
25
29
  wensity logout
26
30
  wensity whoami [--json]
27
31
  wensity list [--type component|block] [options]
28
- wensity add <slug> [--type component|block] [options]
29
- wensity update <slug> [--type component|block] [options]
32
+ wensity add <slug...> [--type component|block] [options]
33
+ wensity update <slug...> [--type component|block] [options]
30
34
  wensity version
31
35
 
32
36
  Commands:
@@ -37,8 +41,8 @@ Commands:
37
41
  logout Remove saved CLI credentials
38
42
  whoami Verify the saved token and print the authenticated account
39
43
  list List registry components or blocks
40
- add Download a component or block source file into this project
41
- update Re-download a component or block and overwrite the local copy
44
+ add Download one or more component/block source files into this project
45
+ update Re-download one or more components/blocks and overwrite the local copies
42
46
  version Print the CLI version
43
47
 
44
48
  Options:
@@ -49,6 +53,7 @@ Options:
49
53
  --preset <code> Apply a shareable Wensity style preset (wsty1.\u2026); merges style when config exists
50
54
  --preset-file <path> Read a preset code or JSON payload from a file
51
55
  --template <name> Scaffold a new app before init: next, vite, astro, react-router
56
+ --package-manager <pm> Force npm, pnpm, yarn, or bun instead of auto-detection
52
57
  --force Overwrite generated files when adding or initializing
53
58
  --no-install Write files without running the package manager
54
59
  --dry-run Print planned writes and installs without changing files
@@ -63,80 +68,954 @@ Preset notes:
63
68
  Plain init refuses an existing wensity.json unless --force
64
69
  init --preset merges style + primitive CSS; does not rewrite installed source
65
70
  init --template writes a minimal app scaffold in the current directory before Wensity files
66
- add/update read style.iconLibrary for UI primitives only (Tabler|Lucide)
71
+ add/update accept several slugs at once and read style.iconLibrary for UI primitives only (Tabler|Lucide)
67
72
  Invalid/expired/unsupported codes exit non-zero with an actionable message
68
73
  `;
69
74
 
70
- // src/errors.ts
71
- var CliError = class extends Error {
72
- exitCode;
73
- constructor(message, exitCode = 1) {
74
- super(message);
75
- this.name = "CliError";
76
- this.exitCode = exitCode;
75
+ // ../install-core/src/errors.ts
76
+ var RETRYABLE_CODES = /* @__PURE__ */ new Set([
77
+ "rate_limited",
78
+ "network_error",
79
+ "server_error",
80
+ "source_unavailable",
81
+ "source_integrity_failed",
82
+ "plan_in_progress",
83
+ "write_failed",
84
+ "dependency_install_failed"
85
+ ]);
86
+ var WensityError = class extends Error {
87
+ code;
88
+ retryable;
89
+ upstream;
90
+ details;
91
+ constructor(code, message, options = {}) {
92
+ super(message, options.cause === void 0 ? void 0 : { cause: options.cause });
93
+ this.name = "WensityError";
94
+ this.code = code;
95
+ this.retryable = RETRYABLE_CODES.has(code);
96
+ if (options.upstream) this.upstream = options.upstream;
97
+ if (options.details) this.details = options.details;
77
98
  }
78
99
  };
100
+ function isWensityError(error) {
101
+ return error instanceof WensityError;
102
+ }
79
103
  function isNodeError(error) {
80
104
  return error instanceof Error && "code" in error;
81
105
  }
82
106
 
83
- // src/args.ts
84
- function parseArgs(argv) {
85
- const positionals = [];
86
- const options = /* @__PURE__ */ new Map();
87
- for (let index = 0; index < argv.length; index += 1) {
88
- const token = argv[index];
89
- if (!token.startsWith("--")) {
90
- positionals.push(token);
91
- continue;
107
+ // ../install-core/src/http.ts
108
+ var DEFAULT_RETRY_POLICY = { maxAttempts: 3, baseDelayMs: 500, maxRetryAfterMs: 2e3 };
109
+ var RETRYABLE_STATUSES = /* @__PURE__ */ new Set([429, 502, 503, 504]);
110
+ var CircuitBreakers = class {
111
+ #options;
112
+ #state = /* @__PURE__ */ new Map();
113
+ constructor(options = {}) {
114
+ this.#options = { threshold: 5, cooldownMs: 3e4, now: Date.now, ...options };
115
+ }
116
+ /** Milliseconds until `origin` may be tried again, or 0 when it may be tried now. */
117
+ remainingMs(origin) {
118
+ const state = this.#state.get(origin);
119
+ return state ? Math.max(0, state.openUntil - this.#options.now()) : 0;
120
+ }
121
+ recordFailure(origin) {
122
+ const state = this.#state.get(origin) ?? { failures: 0, openUntil: 0 };
123
+ state.failures += 1;
124
+ if (state.failures >= this.#options.threshold) state.openUntil = this.#options.now() + this.#options.cooldownMs;
125
+ this.#state.set(origin, state);
126
+ }
127
+ recordSuccess(origin) {
128
+ this.#state.delete(origin);
129
+ }
130
+ reset() {
131
+ this.#state.clear();
132
+ }
133
+ };
134
+ var sharedCircuitBreakers = new CircuitBreakers();
135
+ async function requestJson(url, options = {}) {
136
+ const {
137
+ token,
138
+ userAgent,
139
+ timeoutMs,
140
+ fetch: fetchImpl = fetch,
141
+ retry,
142
+ breakers = sharedCircuitBreakers,
143
+ sleep = defaultSleep,
144
+ random = Math.random,
145
+ ...init
146
+ } = options;
147
+ const headers = new Headers(init.headers);
148
+ headers.set("accept", "application/json");
149
+ if (userAgent) headers.set("user-agent", userAgent);
150
+ if (init.body && !headers.has("content-type")) {
151
+ headers.set("content-type", "application/json");
152
+ }
153
+ if (token) {
154
+ headers.set("authorization", `Bearer ${token}`);
155
+ }
156
+ const policy = { ...DEFAULT_RETRY_POLICY, ...retry || {} };
157
+ const method = (init.method ?? "GET").toUpperCase();
158
+ const idempotent = (method === "GET" || method === "HEAD") && !init.body;
159
+ const maxAttempts = retry === false || !idempotent ? 1 : Math.max(1, Math.floor(policy.maxAttempts));
160
+ const origin = originOf(url);
161
+ let last;
162
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
163
+ const waitMs = breakers && origin ? breakers.remainingMs(origin) : 0;
164
+ if (waitMs > 0) {
165
+ throw last ?? circuitOpen(url, origin, waitMs);
166
+ }
167
+ const outcome = await attemptOnce(url, { ...init, headers }, fetchImpl, timeoutMs);
168
+ if (breakers && origin) {
169
+ if (outcome.kind === "failed" && outcome.countsAsFailure) breakers.recordFailure(origin);
170
+ else breakers.recordSuccess(origin);
171
+ }
172
+ if (outcome.kind === "ok") return outcome.value;
173
+ last = outcome.error;
174
+ if (!outcome.retryable || attempt === maxAttempts) break;
175
+ if (outcome.retryAfterMs !== void 0 && outcome.retryAfterMs > policy.maxRetryAfterMs) break;
176
+ const backoff = random() * policy.baseDelayMs * 2 ** (attempt - 1);
177
+ await sleep(outcome.retryAfterMs ?? backoff);
178
+ }
179
+ throw last;
180
+ }
181
+ async function attemptOnce(url, init, fetchImpl, timeoutMs) {
182
+ const controller = new AbortController();
183
+ let timedOut = false;
184
+ const timer = timeoutMs === void 0 ? void 0 : setTimeout(() => {
185
+ timedOut = true;
186
+ controller.abort(new Error(`timed out after ${timeoutMs} ms`));
187
+ }, timeoutMs);
188
+ const callerSignal = init.signal;
189
+ const forwardAbort = () => controller.abort(callerSignal?.reason);
190
+ if (callerSignal?.aborted) forwardAbort();
191
+ else callerSignal?.addEventListener("abort", forwardAbort, { once: true });
192
+ const transportFailure = (prefix, error) => {
193
+ const message = timedOut ? `timed out after ${timeoutMs} ms` : error instanceof Error ? error.message : "Unknown network error";
194
+ const cancelled = Boolean(callerSignal?.aborted) && !timedOut;
195
+ return {
196
+ kind: "failed",
197
+ error: new WensityError("network_error", `${prefix} ${url}: ${message}`, { cause: error }),
198
+ // A timeout already used the whole budget, and a caller's cancel is not the origin's fault.
199
+ retryable: !timedOut && !cancelled,
200
+ countsAsFailure: !cancelled
201
+ };
202
+ };
203
+ try {
204
+ let response;
205
+ try {
206
+ response = await fetchImpl(url, { ...init, signal: controller.signal });
207
+ } catch (error) {
208
+ return transportFailure("Could not reach", error);
92
209
  }
93
- if (token === "--") {
94
- positionals.push(...argv.slice(index + 1));
95
- break;
210
+ let text;
211
+ try {
212
+ text = await response.text();
213
+ } catch (error) {
214
+ return transportFailure("Could not read the response from", error);
96
215
  }
97
- const inlineValueIndex = token.indexOf("=");
98
- const rawName = inlineValueIndex > -1 ? token.slice(2, inlineValueIndex) : token.slice(2);
99
- const name = normalizeOptionName(rawName);
100
- if (!name) {
101
- throw new CliError(`Invalid option: ${token}`);
216
+ if (!response.ok) {
217
+ const upstream = readUpstreamError(response, parseJsonOrNull(text));
218
+ const message = upstream.message ?? upstream.error ?? response.statusText;
219
+ return {
220
+ kind: "failed",
221
+ error: new WensityError(errorCodeFor(upstream), `${response.status} ${message}`, { upstream }),
222
+ retryable: RETRYABLE_STATUSES.has(response.status),
223
+ countsAsFailure: response.status === 429 || response.status >= 500,
224
+ ...upstream.retryAfterSeconds !== void 0 ? { retryAfterMs: upstream.retryAfterSeconds * 1e3 } : {}
225
+ };
102
226
  }
103
- if (inlineValueIndex > -1) {
104
- options.set(name, token.slice(inlineValueIndex + 1));
105
- continue;
227
+ return { kind: "ok", value: parseJson(text) };
228
+ } finally {
229
+ if (timer) clearTimeout(timer);
230
+ callerSignal?.removeEventListener("abort", forwardAbort);
231
+ }
232
+ }
233
+ function circuitOpen(url, origin, waitMs) {
234
+ const seconds = Math.ceil(waitMs / 1e3);
235
+ return new WensityError(
236
+ "network_error",
237
+ `Could not reach ${url}: requests to ${origin} kept failing, so they are paused for ${seconds} s.`,
238
+ { details: { circuit: "open", retryAfterSeconds: seconds } }
239
+ );
240
+ }
241
+ function originOf(url) {
242
+ try {
243
+ return new URL(url).origin;
244
+ } catch {
245
+ return void 0;
246
+ }
247
+ }
248
+ function defaultSleep(ms) {
249
+ return new Promise((resolve3) => setTimeout(resolve3, ms));
250
+ }
251
+ function parseJsonOrNull(text) {
252
+ try {
253
+ return parseJson(text);
254
+ } catch {
255
+ return null;
256
+ }
257
+ }
258
+ function parseJson(text) {
259
+ if (!text.trim()) return null;
260
+ try {
261
+ return JSON.parse(text);
262
+ } catch {
263
+ throw new WensityError("source_integrity_failed", "Registry returned invalid JSON.");
264
+ }
265
+ }
266
+ function readUpstreamError(response, payload) {
267
+ const upstream = { status: response.status };
268
+ const record = payload && typeof payload === "object" ? payload : {};
269
+ if (typeof record.error === "string") upstream.error = record.error;
270
+ if (typeof record.message === "string") upstream.message = record.message;
271
+ if (record.plan && typeof record.plan === "object") upstream.plan = record.plan;
272
+ const retryAfter = readRetryAfter(response, record);
273
+ if (retryAfter !== void 0) upstream.retryAfterSeconds = retryAfter;
274
+ return upstream;
275
+ }
276
+ function readRetryAfter(response, record) {
277
+ const rateLimit = record.rateLimit;
278
+ if (rateLimit && typeof rateLimit.retryAfterSeconds === "number") return rateLimit.retryAfterSeconds;
279
+ const header = Number(response.headers.get("retry-after"));
280
+ return Number.isFinite(header) && header > 0 ? header : void 0;
281
+ }
282
+ var UPSTREAM_ERROR_CODES = {
283
+ missing_token: "auth_invalid",
284
+ invalid_token: "auth_invalid",
285
+ token_revoked: "auth_revoked",
286
+ token_user_missing: "auth_user_missing",
287
+ pro_license_required: "entitlement_required",
288
+ component_not_found: "item_not_found",
289
+ block_not_found: "item_not_found",
290
+ invalid_component_slug: "invalid_slug",
291
+ invalid_block_slug: "invalid_slug",
292
+ rate_limit_exceeded: "rate_limited",
293
+ component_source_unreadable: "source_unavailable",
294
+ block_source_unreadable: "source_unavailable",
295
+ component_source_graph_unreadable: "source_unavailable",
296
+ cli_auth_failed: "server_error"
297
+ };
298
+ function errorCodeFor(upstream) {
299
+ const known = upstream.error ? UPSTREAM_ERROR_CODES[upstream.error] : void 0;
300
+ if (known) return known;
301
+ switch (upstream.status) {
302
+ case 401:
303
+ return "auth_invalid";
304
+ // A 403 without `pro_license_required` is not a licensing answer, so it
305
+ // falls through to server_error rather than guessing at entitlement.
306
+ case 404:
307
+ return "item_not_found";
308
+ case 429:
309
+ return "rate_limited";
310
+ default:
311
+ return "server_error";
312
+ }
313
+ }
314
+
315
+ // ../install-core/src/paths.ts
316
+ import { lstatSync, realpathSync } from "fs";
317
+ import { dirname, isAbsolute, join, relative, resolve, sep } from "path";
318
+ function normalizeUrlOrigin(input) {
319
+ const trimmed = input.trim().replace(/\/+$/, "");
320
+ const withProtocol = /^https?:\/\//i.test(trimmed) ? trimmed : `https://${trimmed}`;
321
+ const url = new URL(withProtocol);
322
+ if (url.protocol !== "https:" && !isLocalHttpUrl(url)) {
323
+ throw new WensityError("config_invalid", "API URL must use HTTPS, except for localhost development URLs.");
324
+ }
325
+ return url.origin;
326
+ }
327
+ function isLocalHttpUrl(url) {
328
+ if (url.protocol !== "http:") return false;
329
+ return url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "::1";
330
+ }
331
+ function joinUrl(originOrUrl, path) {
332
+ const base = originOrUrl.endsWith("/") ? originOrUrl : `${originOrUrl}/`;
333
+ const normalizedPath = path.startsWith("/") ? path.slice(1) : path;
334
+ return new URL(normalizedPath, base).toString();
335
+ }
336
+ function resolveInside(root, candidate) {
337
+ if (isAbsolute(candidate)) {
338
+ throw new WensityError("path_unsafe", `Path must be project-relative: ${candidate}`);
339
+ }
340
+ const absolutePath = resolve(root, candidate);
341
+ if (escapes(relative(root, absolutePath))) {
342
+ throw new WensityError("path_unsafe", `Path escapes the project: ${candidate}`);
343
+ }
344
+ return absolutePath;
345
+ }
346
+ function escapes(relativePath) {
347
+ return relativePath === ".." || relativePath.startsWith(`..${sep}`) || isAbsolute(relativePath);
348
+ }
349
+ var MAX_TARGET_PATH_LENGTH = 240;
350
+ var SAFE_SEGMENT = /^[A-Za-z0-9._-]+$/;
351
+ function assertTargetPathLength(relativePath) {
352
+ if (relativePath.length > MAX_TARGET_PATH_LENGTH) {
353
+ throw new WensityError(
354
+ "path_unsafe",
355
+ `Path is longer than ${MAX_TARGET_PATH_LENGTH} characters: ${relativePath.slice(0, 60)}\u2026`
356
+ );
357
+ }
358
+ }
359
+ function toPosixPath(input) {
360
+ return input.split(sep).join("/");
361
+ }
362
+ function parentDirectory(input) {
363
+ return dirname(input);
364
+ }
365
+ function stripKnownComponentPrefix(path) {
366
+ const normalized = path.replace(/\\/g, "/").replace(/^\/+/, "");
367
+ const prefixes = ["components/wensity/", "src/components/wensity/"];
368
+ const prefix = prefixes.find((candidate) => normalized.startsWith(candidate));
369
+ return prefix ? normalized.slice(prefix.length) : normalized;
370
+ }
371
+ function sanitizePayloadPath(path, fallbackFileName) {
372
+ const unsafe3 = () => new WensityError("path_unsafe", `Unsafe registry file path: ${path}`);
373
+ if (/^[\\/]/.test(path) || /^[A-Za-z]:/.test(path)) throw unsafe3();
374
+ const normalized = stripKnownComponentPrefix(path);
375
+ const parts = normalized.split("/").filter(Boolean);
376
+ if (parts.length === 0) return fallbackFileName;
377
+ for (const part of parts) {
378
+ if (part === "." || part === ".." || !SAFE_SEGMENT.test(part)) throw unsafe3();
379
+ }
380
+ if (normalized.length > MAX_TARGET_PATH_LENGTH) throw unsafe3();
381
+ const last = parts.at(-1) ?? fallbackFileName;
382
+ if (!/\.(tsx|ts|jsx|js)$/.test(last)) {
383
+ parts[parts.length - 1] = fallbackFileName;
384
+ }
385
+ return parts.join("/");
386
+ }
387
+ function componentFileName(slug) {
388
+ return `${slugToPascalCase(slug)}.tsx`;
389
+ }
390
+ function slugToPascalCase(slug) {
391
+ return slug.split(/[^a-zA-Z0-9]+/).filter(Boolean).map((part) => `${part.charAt(0).toUpperCase()}${part.slice(1)}`).join("");
392
+ }
393
+ function extensionlessPath(path) {
394
+ return path.replace(/\.(tsx|ts|jsx|js)$/, "");
395
+ }
396
+ function assertNoSymlinkEscape(projectRoot, absolutePath) {
397
+ const root = resolve(projectRoot);
398
+ const relativePath = relative(root, resolve(absolutePath));
399
+ if (escapes(relativePath)) {
400
+ throw new WensityError("path_unsafe", `Path escapes the project: ${toPosixPath(relativePath)}`);
401
+ }
402
+ let realRoot;
403
+ try {
404
+ realRoot = realpathSync(root);
405
+ } catch (error) {
406
+ if (isNodeError(error) && error.code === "ENOENT") return;
407
+ throw error;
408
+ }
409
+ let current = root;
410
+ for (const segment of relativePath.split(sep).filter(Boolean)) {
411
+ current = join(current, segment);
412
+ let isSymlink;
413
+ try {
414
+ isSymlink = lstatSync(current).isSymbolicLink();
415
+ } catch (error) {
416
+ if (isNodeError(error) && error.code === "ENOENT") return;
417
+ throw error;
106
418
  }
107
- const nextToken = argv[index + 1];
108
- if (nextToken && !nextToken.startsWith("--") && expectsValue(name)) {
109
- options.set(name, nextToken);
110
- index += 1;
111
- continue;
419
+ if (!isSymlink) continue;
420
+ let target;
421
+ try {
422
+ target = realpathSync(current);
423
+ } catch {
424
+ throw new WensityError("path_unsafe", `Path passes through a broken symlink: ${toPosixPath(relative(root, current))}`);
425
+ }
426
+ const fromRoot = relative(realRoot, target);
427
+ if (escapes(fromRoot)) {
428
+ throw new WensityError(
429
+ "path_unsafe",
430
+ `Path escapes the project through a symlink: ${toPosixPath(relative(root, current))}`
431
+ );
112
432
  }
113
- options.set(name, true);
114
433
  }
115
- return { positionals, options };
116
434
  }
117
- function getStringOption(args, name) {
118
- const value = args.options.get(name);
119
- if (value === void 0) return void 0;
120
- if (typeof value === "boolean") {
121
- throw new CliError(`--${name} expects a value.`);
435
+
436
+ // ../install-core/src/client.ts
437
+ function createWensityClient(options) {
438
+ const origin = options.origin;
439
+ const registryUrl = options.registryUrl ?? joinUrl(origin, "/api/registry");
440
+ const blocksRegistryUrl = options.blocksRegistryUrl ?? joinUrl(registryOrigin(registryUrl), "/api/blocks");
441
+ const apiBaseUrl = options.apiBaseUrl ?? origin;
442
+ const tokenOrigin = new URL(apiBaseUrl).origin;
443
+ const { token } = options;
444
+ const download = (task) => task();
445
+ function get(url, withToken = false) {
446
+ const request = {
447
+ userAgent: options.userAgent,
448
+ ...options.headers ? { headers: withoutAuthorization(options.headers) } : {},
449
+ ...options.timeoutMs !== void 0 ? { timeoutMs: options.timeoutMs } : {},
450
+ ...options.fetch ? { fetch: options.fetch } : {},
451
+ ...options.retry !== void 0 ? { retry: options.retry } : {},
452
+ ...options.breakers !== void 0 ? { breakers: options.breakers } : {},
453
+ ...options.sleep ? { sleep: options.sleep } : {}
454
+ };
455
+ if (withToken && token) {
456
+ assertTokenDestination(url, tokenOrigin);
457
+ request.token = token;
458
+ }
459
+ return requestJson(url, request);
122
460
  }
123
- return value;
461
+ function indexUrl(kind) {
462
+ return kind === "block" ? blocksRegistryUrl : registryUrl;
463
+ }
464
+ async function fetchItemMetadata(slug, kind) {
465
+ return get(registryMetadataUrl(indexUrl(kind), slug));
466
+ }
467
+ async function fetchComponentSource(slug, opts) {
468
+ const metadata = opts.metadata ?? await fetchItemMetadata(slug, "component");
469
+ const wrap = opts.download ?? download;
470
+ if (token) {
471
+ const cliEndpoint = metadata.endpoints?.cli ?? `/api/cli/${slug}`;
472
+ const endpoint2 = cliEndpoint.startsWith("http") ? cliEndpoint : joinUrl(apiBaseUrl, cliEndpoint);
473
+ try {
474
+ return await wrap(() => get(endpoint2, true));
475
+ } catch (error) {
476
+ if (metadata.access === "pro" || !isAuthFailure(error)) throw error;
477
+ opts.onCredentialsRejected?.(error);
478
+ }
479
+ }
480
+ if (metadata.access === "pro") {
481
+ throw proRequired(slug, metadata.name, "component");
482
+ }
483
+ const publicSourceEndpoint = metadata.endpoints?.registry ?? registrySlugUrl(registryUrl, slug);
484
+ const endpoint = publicSourceEndpoint.startsWith("http") ? publicSourceEndpoint : joinUrl(registryOrigin(registryUrl), publicSourceEndpoint);
485
+ return wrap(() => get(endpoint));
486
+ }
487
+ async function fetchBlockSource(slug, opts) {
488
+ const metadata = opts.metadata ?? await fetchItemMetadata(slug, "block");
489
+ if (!token) {
490
+ throw proRequired(slug, metadata.name, "block");
491
+ }
492
+ const cliEndpoint = metadata.endpoints?.cli ?? `/api/cli/blocks/${slug}`;
493
+ const endpoint = cliEndpoint.startsWith("http") ? cliEndpoint : joinUrl(apiBaseUrl, cliEndpoint);
494
+ return (opts.download ?? download)(() => get(endpoint, true));
495
+ }
496
+ return {
497
+ fetchComponentIndex: () => get(registryUrl),
498
+ fetchBlockIndex: () => get(blocksRegistryUrl),
499
+ fetchItemMetadata,
500
+ async fetchSourcePayload(slug, kind, opts = {}) {
501
+ if (kind === "block") {
502
+ return await fetchBlockSource(slug, opts);
503
+ }
504
+ return await fetchComponentSource(slug, opts);
505
+ },
506
+ async whoami() {
507
+ if (!token) {
508
+ throw new WensityError("auth_required", "No Wensity API token is configured.");
509
+ }
510
+ return get(joinUrl(apiBaseUrl, "/api/cli/whoami"), true);
511
+ }
512
+ };
124
513
  }
125
- function hasFlag(args, name) {
126
- return args.options.get(name) === true;
514
+ function withoutAuthorization(headers) {
515
+ return Object.fromEntries(Object.entries(headers).filter(([name]) => name.toLowerCase() !== "authorization"));
127
516
  }
128
- function expectsValue(name) {
129
- return ["api-url", "registry", "token", "type", "preset", "preset-file", "template"].includes(name);
517
+ function isAuthFailure(error) {
518
+ const status = isWensityError(error) ? error.upstream?.status : void 0;
519
+ return status === 401 || status === 403;
130
520
  }
131
- function normalizeOptionName(name) {
132
- if (name === "dry") return "dry-run";
133
- return name;
521
+ function proRequired(slug, name, kind) {
522
+ return new WensityError("auth_required", `${name} is a Pro ${kind}.`, {
523
+ details: { slug, name, kind }
524
+ });
525
+ }
526
+ function assertTokenDestination(url, tokenOrigin) {
527
+ const destination = new URL(url).origin;
528
+ if (destination !== tokenOrigin) {
529
+ throw new WensityError(
530
+ "source_integrity_failed",
531
+ `Refusing to send the saved API token to ${destination}; it belongs to ${tokenOrigin}.`
532
+ );
533
+ }
534
+ }
535
+ function registryMetadataUrl(registry, slug) {
536
+ const url = new URL(registry);
537
+ url.searchParams.set("slug", slug);
538
+ return url.toString();
539
+ }
540
+ function registrySlugUrl(registry, slug) {
541
+ return joinUrl(registry.endsWith("/") ? registry : `${registry}/`, slug);
542
+ }
543
+ function registryOrigin(registry) {
544
+ const url = new URL(registry);
545
+ return `${url.protocol}//${url.host}`;
134
546
  }
135
547
 
136
- // src/config.ts
137
- import { existsSync } from "fs";
138
- import { access, mkdir, readFile, writeFile } from "fs/promises";
139
- import { dirname as dirname2, join as join2, relative as relative2 } from "path";
548
+ // ../install-core/src/apply.ts
549
+ import { createHash } from "crypto";
550
+ import {
551
+ closeSync,
552
+ constants as fsConstants,
553
+ fchmodSync,
554
+ fstatSync,
555
+ fsyncSync,
556
+ ftruncateSync,
557
+ lstatSync as lstatSync3,
558
+ mkdirSync as mkdirSync2,
559
+ openSync,
560
+ renameSync,
561
+ rmdirSync as rmdirSync2,
562
+ unlinkSync,
563
+ writeSync
564
+ } from "fs";
565
+ import { chmod, copyFile, mkdir, open, readFile, rename, rm } from "fs/promises";
566
+ import { dirname as dirname2, join as join3, relative as relative3 } from "path";
567
+
568
+ // ../install-core/src/workdir.ts
569
+ import { lstatSync as lstatSync2, mkdirSync, readdirSync, readFileSync, realpathSync as realpathSync2, rmdirSync } from "fs";
570
+ import { isAbsolute as isAbsolute2, join as join2, relative as relative2 } from "path";
571
+ var WORK_DIR_NAME = ".wensity-tmp";
572
+ var JOURNAL_FILE = "journal.json";
573
+ var LOCK_FILE = "install.lock";
574
+ function unsafe(message) {
575
+ return new WensityError("path_unsafe", message);
576
+ }
577
+ function lstatOrUndefined(path) {
578
+ try {
579
+ return lstatSync2(path);
580
+ } catch (error) {
581
+ if (isNodeError(error) && error.code === "ENOENT") return void 0;
582
+ throw error;
583
+ }
584
+ }
585
+ function assertRealDirectoryInside(projectRoot, path) {
586
+ const stat2 = lstatOrUndefined(path);
587
+ if (!stat2) throw unsafe(`${WORK_DIR_NAME} disappeared while it was in use.`);
588
+ if (stat2.isSymbolicLink() || !stat2.isDirectory()) {
589
+ throw unsafe(`${toPosixPath(relative2(projectRoot, path))} is not a real directory (a symlink, junction, or file). Remove it and retry.`);
590
+ }
591
+ const fromRoot = relative2(realpathSync2(projectRoot), realpathSync2(path));
592
+ if (fromRoot === ".." || fromRoot.startsWith("../") || fromRoot.startsWith("..\\") || isAbsolute2(fromRoot)) {
593
+ throw unsafe(`${WORK_DIR_NAME} resolves outside the project.`);
594
+ }
595
+ }
596
+ function ensureWorkDir(projectRoot) {
597
+ const dir = join2(projectRoot, WORK_DIR_NAME);
598
+ if (!lstatOrUndefined(dir)) {
599
+ try {
600
+ mkdirSync(dir, { mode: 448 });
601
+ } catch (error) {
602
+ if (!(isNodeError(error) && error.code === "EEXIST")) throw error;
603
+ }
604
+ }
605
+ assertRealDirectoryInside(projectRoot, dir);
606
+ return dir;
607
+ }
608
+ function removeWorkDirIfEmpty(projectRoot) {
609
+ const dir = join2(projectRoot, WORK_DIR_NAME);
610
+ const stat2 = lstatOrUndefined(dir);
611
+ if (!stat2 || stat2.isSymbolicLink() || !stat2.isDirectory()) return;
612
+ try {
613
+ rmdirSync(dir);
614
+ } catch {
615
+ }
616
+ }
617
+ function findIncompleteJournals(projectRoot) {
618
+ const dir = join2(projectRoot, WORK_DIR_NAME);
619
+ if (!lstatOrUndefined(dir)) return [];
620
+ assertRealDirectoryInside(projectRoot, dir);
621
+ const lock = lstatOrUndefined(join2(dir, LOCK_FILE));
622
+ if (lock && (lock.isSymbolicLink() || !lock.isFile())) {
623
+ throw unsafe(`${WORK_DIR_NAME}/${LOCK_FILE} is not a regular file. It was not followed or removed; delete it yourself.`);
624
+ }
625
+ const found = [];
626
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
627
+ if (!entry.isDirectory()) continue;
628
+ const journalPath = `${WORK_DIR_NAME}/${entry.name}`;
629
+ const file = join2(dir, entry.name, JOURNAL_FILE);
630
+ const stat2 = lstatOrUndefined(file);
631
+ let journal;
632
+ if (stat2?.isFile()) {
633
+ try {
634
+ journal = JSON.parse(readFileSync(file, "utf8"));
635
+ } catch {
636
+ journal = void 0;
637
+ }
638
+ }
639
+ const status = typeof journal?.status === "string" ? journal.status : "unknown";
640
+ if (status === "committed" || status === "rolled_back") continue;
641
+ const entries = Array.isArray(journal?.entries) ? journal.entries : [];
642
+ found.push({
643
+ journalPath,
644
+ status,
645
+ affectedPaths: entries.map((item) => item.relativePath),
646
+ backupsAvailable: entries.filter((item) => item.existed && item.backup && lstatOrUndefined(join2(dir, entry.name, item.backup))?.isFile()).map((item) => item.relativePath),
647
+ backupsMissing: entries.filter((item) => !item.existed).map((item) => item.relativePath)
648
+ });
649
+ }
650
+ return found.sort((a, b) => a.journalPath.localeCompare(b.journalPath));
651
+ }
652
+ function recoveryRequired(journal) {
653
+ return new WensityError(
654
+ "recovery_required",
655
+ "A previous Wensity installation did not finish. Recover it before installing again.",
656
+ {
657
+ details: {
658
+ journalPath: journal.journalPath,
659
+ status: journal.status,
660
+ affectedPaths: journal.affectedPaths,
661
+ backupsAvailable: journal.backupsAvailable,
662
+ backupsMissing: journal.backupsMissing,
663
+ steps: RECOVERY_STEPS(journal.journalPath)
664
+ }
665
+ }
666
+ );
667
+ }
668
+ function RECOVERY_STEPS(journalPath) {
669
+ return [
670
+ `Inspect ${journalPath}/${JOURNAL_FILE} (if it is missing, no project file was changed by that run)`,
671
+ "For each entry with existed:true, restore the file from its backup/<n> copy",
672
+ "For each entry with existed:false that now exists, delete it only if you did not create it yourself",
673
+ `Verify with git diff, then delete the ${journalPath} directory yourself`,
674
+ "Re-run the installation"
675
+ ];
676
+ }
677
+ function assertNoIncompleteJournal(projectRoot) {
678
+ const [first] = findIncompleteJournals(projectRoot);
679
+ if (first) throw recoveryRequired(first);
680
+ }
681
+
682
+ // ../install-core/src/apply.ts
683
+ var WINDOWS_RETRY_CODES = /* @__PURE__ */ new Set(["EPERM", "EBUSY", "EACCES"]);
684
+ var WINDOWS_PLACEMENT_ATTEMPTS = 5;
685
+ function sha256(data) {
686
+ return createHash("sha256").update(data).digest("hex");
687
+ }
688
+ async function apply(options) {
689
+ const { projectRoot, planId, files } = options;
690
+ const hooks = options.testHooks ?? {};
691
+ const platform = options.platform ?? process.platform;
692
+ const emit = (event) => hooks.onEvent?.(event);
693
+ const fault = (at, index = 0) => {
694
+ if (hooks.fault && hooks.fault.at === at && (hooks.fault.file ?? 0) === index) {
695
+ throw new WensityError("write_failed", `Injected fault at ${at}.`);
696
+ }
697
+ };
698
+ if (!/^[A-Za-z0-9_-]{1,80}$/.test(planId)) throw new WensityError("path_unsafe", `Invalid journal name: ${planId}`);
699
+ assertNoIncompleteJournal(projectRoot);
700
+ const targets = files.map((file) => {
701
+ assertTargetPathLength(file.relativePath);
702
+ const absolutePath = resolveInside(projectRoot, file.relativePath);
703
+ assertNoSymlinkEscape(projectRoot, absolutePath);
704
+ if (file.content.includes("\0")) {
705
+ throw new WensityError("source_integrity_failed", `${file.relativePath} contains a NUL byte; Wensity only installs text source.`);
706
+ }
707
+ const stat2 = lstatOrUndefined2(absolutePath);
708
+ if (stat2 && (stat2.isSymbolicLink() || !stat2.isFile())) {
709
+ throw new WensityError("path_unsafe", `${file.relativePath} exists but is not a regular file (a symlink or directory). Nothing was written.`);
710
+ }
711
+ if (file.action === "create" && stat2) {
712
+ throw new WensityError("plan_stale", `${file.relativePath} appeared after planning.`, {
713
+ details: { reason: "precondition_mismatch", path: file.relativePath, expected: "absent", found: "present" }
714
+ });
715
+ }
716
+ if (file.action !== "create" && !stat2) {
717
+ throw new WensityError("plan_stale", `${file.relativePath} was removed after planning.`, {
718
+ details: { reason: "precondition_mismatch", path: file.relativePath, expected: "present", found: "absent" }
719
+ });
720
+ }
721
+ return { ...file, absolutePath, existed: Boolean(stat2), mode: stat2 ? stat2.mode & 511 : void 0 };
722
+ });
723
+ const workDir = ensureWorkDir(projectRoot);
724
+ const journalDir = join3(workDir, planId);
725
+ try {
726
+ mkdirSync2(journalDir, { mode: 448 });
727
+ } catch (error) {
728
+ if (isNodeError(error) && error.code === "EEXIST") {
729
+ throw new WensityError("path_unsafe", `${WORK_DIR_NAME}/${planId} already exists; it was not reused.`);
730
+ }
731
+ throw error;
732
+ }
733
+ const journalPath = toPosixPath(relative3(projectRoot, journalDir));
734
+ const journalFile = join3(journalDir, JOURNAL_FILE);
735
+ const entries = targets.map((target) => ({
736
+ relativePath: target.relativePath,
737
+ existed: target.existed,
738
+ expectedSha256: sha256(target.content),
739
+ action: target.action
740
+ }));
741
+ const writeJournal = async (status, step) => {
742
+ await writeSynced(journalFile, `${JSON.stringify({ version: 1, planId, status, step, entries }, null, 2)}
743
+ `, 384, true);
744
+ await syncDirectory(journalDir);
745
+ emit({ type: "journal", status, step });
746
+ };
747
+ const attempted = targets.map(() => false);
748
+ const placed = targets.map(() => void 0);
749
+ const createdDirs = [];
750
+ let failedPath;
751
+ try {
752
+ await mkdir(join3(journalDir, "backup"), { mode: 448 });
753
+ await mkdir(join3(journalDir, "staged"), { mode: 448 });
754
+ for (const [index, target] of targets.entries()) {
755
+ failedPath = target.relativePath;
756
+ if (target.existed) {
757
+ const backup = `backup/${index}`;
758
+ await copyFile(target.absolutePath, join3(journalDir, backup), fsConstants.COPYFILE_EXCL);
759
+ entries[index].backup = backup;
760
+ entries[index].sha256Before = sha256(await readFile(join3(journalDir, backup)));
761
+ entries[index].mode = target.mode;
762
+ emit({ type: "backup", path: target.relativePath });
763
+ }
764
+ fault("backup", index);
765
+ }
766
+ failedPath = void 0;
767
+ await writeJournal("in_progress", "backed_up");
768
+ fault("journal");
769
+ await writeJournal("in_progress", "staging");
770
+ for (const [index, target] of targets.entries()) {
771
+ failedPath = target.relativePath;
772
+ await writeSynced(join3(journalDir, "staged", String(index)), target.content, target.mode ?? 420);
773
+ emit({ type: "stage", path: target.relativePath });
774
+ fault("stage", index);
775
+ }
776
+ await writeJournal("in_progress", "placing");
777
+ for (const [index, target] of targets.entries()) {
778
+ failedPath = target.relativePath;
779
+ await makeParents(projectRoot, dirname2(target.absolutePath), createdDirs);
780
+ attempted[index] = true;
781
+ placed[index] = await place(join3(journalDir, "staged", String(index)), target, platform, hooks, emit, projectRoot);
782
+ assertPlaced(projectRoot, target, placed[index]);
783
+ fault("place", index);
784
+ const onDisk = sha256(await readFile(target.absolutePath));
785
+ emit({ type: "verify", path: target.relativePath });
786
+ if (onDisk !== entries[index].expectedSha256) {
787
+ throw new WensityError("source_integrity_failed", `${target.relativePath} did not verify after writing (hash mismatch).`, {
788
+ details: { reason: "post_write_hash_mismatch", path: target.relativePath }
789
+ });
790
+ }
791
+ fault("verify", index);
792
+ }
793
+ } catch (cause) {
794
+ const error = isWensityError(cause) ? cause : new WensityError("write_failed", `Writing ${failedPath ?? "the project files"} failed: ${cause.message}`, { cause });
795
+ return rollback({ projectRoot, journalDir, journalPath, targets, entries, attempted, placed, createdDirs, writeJournal, error, failedPath, emit });
796
+ }
797
+ try {
798
+ fault("commit");
799
+ await writeJournal("committed", "done");
800
+ } catch (cause) {
801
+ const error = isWensityError(cause) ? cause : new WensityError("write_failed", `Recording the commit failed: ${cause.message}`, { cause });
802
+ return rollback({ projectRoot, journalDir, journalPath, targets, entries, attempted, placed, createdDirs, writeJournal, error, failedPath: void 0, emit });
803
+ }
804
+ let cleanupWarning;
805
+ try {
806
+ await rm(journalDir, { recursive: true, force: true });
807
+ removeWorkDirIfEmpty(projectRoot);
808
+ } catch (error) {
809
+ cleanupWarning = `The install committed, but ${journalPath} could not be removed (${error.message}). It is marked committed and can be deleted.`;
810
+ }
811
+ return {
812
+ status: "committed",
813
+ ...cleanupWarning ? { cleanupWarning } : {},
814
+ changes: targets.map((target, index) => ({
815
+ path: target.relativePath,
816
+ action: target.action === "create" ? "created" : target.action === "modify" ? "modified" : "overwritten",
817
+ bytes: Buffer.byteLength(target.content),
818
+ sha256: entries[index].expectedSha256
819
+ }))
820
+ };
821
+ }
822
+ async function rollback(input) {
823
+ const { journalDir, targets, entries, attempted, placed } = input;
824
+ const restored = [];
825
+ const removed = [];
826
+ const unrecovered = [];
827
+ for (let index = targets.length - 1; index >= 0; index -= 1) {
828
+ if (!attempted[index]) continue;
829
+ const target = targets[index];
830
+ const entry = entries[index];
831
+ try {
832
+ const backupBytes = entry.existed ? await readFile(join3(journalDir, entry.backup)) : void 0;
833
+ input.emit({ type: "rollback", path: target.relativePath });
834
+ const outcome = rollbackOneSync(input.projectRoot, target, entry, placed[index], backupBytes);
835
+ if (outcome === "restored") restored.push(target.relativePath);
836
+ else removed.push(target.relativePath);
837
+ } catch (error) {
838
+ unrecovered.push({
839
+ path: target.relativePath,
840
+ recovery: entry.existed ? `Copy ${input.journalPath}/${entry.backup} back to ${target.relativePath} (${error.message})` : `Delete ${target.relativePath} if you did not create it yourself (${error.message})`
841
+ });
842
+ }
843
+ }
844
+ for (const dir of [...input.createdDirs].reverse()) {
845
+ try {
846
+ assertNoSymlinkEscape(input.projectRoot, dir);
847
+ const stat2 = lstatSync3(dir);
848
+ if (stat2.isDirectory() && !stat2.isSymbolicLink()) rmdirSync2(dir);
849
+ } catch {
850
+ }
851
+ }
852
+ if (unrecovered.length > 0) {
853
+ await input.writeJournal("rollback_failed", "rollback").catch(() => void 0);
854
+ return {
855
+ status: "rollback_failed",
856
+ ...input.failedPath ? { failedPath: input.failedPath } : {},
857
+ journalPath: input.journalPath,
858
+ restored,
859
+ removed,
860
+ unrecovered,
861
+ error: input.error
862
+ };
863
+ }
864
+ let cleanupWarning;
865
+ try {
866
+ await input.writeJournal("rolled_back", "done");
867
+ } catch (error) {
868
+ cleanupWarning = `Every file was restored, but ${input.journalPath} could not be marked rolled back (${error.message}).`;
869
+ }
870
+ try {
871
+ await rm(journalDir, { recursive: true, force: true });
872
+ removeWorkDirIfEmpty(input.projectRoot);
873
+ } catch (error) {
874
+ cleanupWarning = `Every file was restored, but ${input.journalPath} could not be removed (${error.message}); it may block the next install until you delete it.`;
875
+ }
876
+ return {
877
+ status: "rolled_back",
878
+ ...input.failedPath ? { failedPath: input.failedPath } : {},
879
+ restored,
880
+ removed,
881
+ error: input.error,
882
+ ...cleanupWarning ? { cleanupWarning } : {}
883
+ };
884
+ }
885
+ function identityOf(stat2) {
886
+ return { dev: stat2.dev, ino: stat2.ino };
887
+ }
888
+ function sameFile(a, b) {
889
+ return a.dev === b.dev && a.ino === b.ino;
890
+ }
891
+ function unsafe2(relativePath, why) {
892
+ return new WensityError("path_unsafe", `${relativePath} ${why}; it was not touched.`);
893
+ }
894
+ function rollbackOneSync(projectRoot, target, entry, placedFile, backupBytes) {
895
+ const { relativePath, absolutePath } = target;
896
+ if (!placedFile) return entry.existed ? "restored" : "removed";
897
+ assertNoSymlinkEscape(projectRoot, absolutePath);
898
+ const current = lstatOrUndefinedBig(absolutePath);
899
+ if (current && (current.isSymbolicLink() || !current.isFile())) throw unsafe2(relativePath, "was replaced by a symlink or non-file");
900
+ if (!current) throw unsafe2(relativePath, "was deleted after it was written");
901
+ if (!sameFile(identityOf(current), placedFile)) throw unsafe2(relativePath, "was replaced by another file after it was written");
902
+ if (!entry.existed) {
903
+ unlinkSync(absolutePath);
904
+ return "removed";
905
+ }
906
+ if (sha256(backupBytes) !== entry.sha256Before) throw new Error("the backup no longer matches its recorded hash; it was not written back");
907
+ const fd = openSync(absolutePath, fsConstants.O_WRONLY | (fsConstants.O_NOFOLLOW ?? 0));
908
+ try {
909
+ const opened = identityOf(fstatSync(fd, { bigint: true }));
910
+ assertNoSymlinkEscape(projectRoot, absolutePath);
911
+ const again = lstatOrUndefinedBig(absolutePath);
912
+ if (!sameFile(opened, placedFile) || !again || !sameFile(identityOf(again), placedFile)) {
913
+ throw unsafe2(relativePath, "moved while it was being restored");
914
+ }
915
+ ftruncateSync(fd, 0);
916
+ let offset = 0;
917
+ while (offset < backupBytes.length) offset += writeSync(fd, backupBytes, offset, backupBytes.length - offset, offset);
918
+ fsyncSync(fd);
919
+ if (entry.mode !== void 0) fchmodSync(fd, entry.mode);
920
+ } finally {
921
+ closeSync(fd);
922
+ }
923
+ return "restored";
924
+ }
925
+ function lstatOrUndefinedBig(path) {
926
+ try {
927
+ return lstatSync3(path, { bigint: true });
928
+ } catch (error) {
929
+ if (isNodeError(error) && error.code === "ENOENT") return void 0;
930
+ throw error;
931
+ }
932
+ }
933
+ async function place(staged, target, platform, hooks, emit, projectRoot) {
934
+ const stagedFile = identityOf(lstatSync3(staged, { bigint: true }));
935
+ const attempts = platform === "win32" && target.existed ? WINDOWS_PLACEMENT_ATTEMPTS : 1;
936
+ for (let attempt = 1; ; attempt += 1) {
937
+ emit({ type: "place", path: target.relativePath, attempt });
938
+ try {
939
+ if (hooks.rename) {
940
+ assertNoSymlinkEscape(projectRoot, target.absolutePath);
941
+ await hooks.rename(staged, target.absolutePath);
942
+ } else {
943
+ assertNoSymlinkEscape(projectRoot, target.absolutePath);
944
+ renameSync(staged, target.absolutePath);
945
+ }
946
+ break;
947
+ } catch (error) {
948
+ const retryable = isNodeError(error) && WINDOWS_RETRY_CODES.has(error.code ?? "");
949
+ if (!retryable || attempt >= attempts) throw error;
950
+ await new Promise((resolve3) => setTimeout(resolve3, 25 * 2 ** (attempt - 1)));
951
+ }
952
+ }
953
+ return stagedFile;
954
+ }
955
+ function assertPlaced(projectRoot, target, placedFile) {
956
+ assertNoSymlinkEscape(projectRoot, target.absolutePath);
957
+ const now = lstatOrUndefinedBig(target.absolutePath);
958
+ if (!now || now.isSymbolicLink() || !sameFile(identityOf(now), placedFile)) {
959
+ throw new WensityError("path_unsafe", `${target.relativePath} is not the file that was just written; the install is being rolled back.`);
960
+ }
961
+ }
962
+ async function makeParents(projectRoot, dir, created) {
963
+ const missing = [];
964
+ for (let current = dir; current !== projectRoot && !lstatOrUndefined2(current); current = dirname2(current)) {
965
+ missing.unshift(current);
966
+ }
967
+ for (const path of missing) {
968
+ await mkdir(path);
969
+ created.push(path);
970
+ }
971
+ }
972
+ async function writeSynced(path, data, mode, replace = false) {
973
+ if (replace) {
974
+ const temp = `${path}.next`;
975
+ await writeSynced(temp, data, mode);
976
+ await rename(temp, path);
977
+ return;
978
+ }
979
+ const handle = await open(path, "wx", mode);
980
+ try {
981
+ await handle.writeFile(data, "utf8");
982
+ await handle.sync();
983
+ } finally {
984
+ await handle.close();
985
+ }
986
+ await chmod(path, mode);
987
+ }
988
+ async function syncDirectory(dir) {
989
+ try {
990
+ const handle = await open(dir, "r");
991
+ try {
992
+ await handle.sync();
993
+ } finally {
994
+ await handle.close();
995
+ }
996
+ } catch {
997
+ }
998
+ }
999
+ function lstatOrUndefined2(path) {
1000
+ try {
1001
+ return lstatSync3(path);
1002
+ } catch (error) {
1003
+ if (isNodeError(error) && error.code === "ENOENT") return void 0;
1004
+ throw error;
1005
+ }
1006
+ }
1007
+
1008
+ // ../install-core/src/project.ts
1009
+ import { spawnSync as spawnSync2 } from "child_process";
1010
+ import { existsSync as existsSync4, lstatSync as lstatSync4, readdirSync as readdirSync2, readFileSync as readFileSync4, statSync as statSync4 } from "fs";
1011
+ import { readFile as readFile3 } from "fs/promises";
1012
+ import { join as join7, relative as relative6 } from "path";
1013
+
1014
+ // ../install-core/src/config.ts
1015
+ import { existsSync as existsSync2, realpathSync as realpathSync3, statSync as statSync2 } from "fs";
1016
+ import { access, mkdir as mkdir2, readFile as readFile2, writeFile } from "fs/promises";
1017
+ import { homedir } from "os";
1018
+ import { dirname as dirname3, join as join5, posix as posix2, relative as relative5, resolve as resolve2 } from "path";
140
1019
 
141
1020
  // ../wensity-preset/src/constants.ts
142
1021
  var ICON_LIBRARIES = ["tabler", "lucide"];
@@ -1488,6 +2367,7 @@ var TABLER_TO_LUCIDE = {
1488
2367
  IconChevronsRight: "ChevronsRight",
1489
2368
  IconCircleX: "CircleX",
1490
2369
  IconClock: "Clock",
2370
+ IconColorPicker: "Pipette",
1491
2371
  IconCornerDownLeft: "CornerDownLeft",
1492
2372
  IconDots: "Ellipsis",
1493
2373
  IconDownload: "Download",
@@ -2521,76 +3401,93 @@ function resolvedTokensToCssVars(tokens, mode) {
2521
3401
  [RESOLVED_TOKEN_CSS_VARS.fontMono]: tokens.fontMono
2522
3402
  };
2523
3403
  }
2524
-
2525
- // src/paths.ts
2526
- import { dirname, isAbsolute, relative, resolve, sep } from "path";
2527
- function normalizeUrlOrigin(input) {
2528
- const trimmed = input.trim().replace(/\/+$/, "");
2529
- const withProtocol = /^https?:\/\//i.test(trimmed) ? trimmed : `https://${trimmed}`;
2530
- const url = new URL(withProtocol);
2531
- if (url.protocol !== "https:" && !isLocalHttpUrl(url)) {
2532
- throw new CliError("API URL must use HTTPS, except for localhost development URLs.");
2533
- }
2534
- return url.origin;
2535
- }
2536
- function isLocalHttpUrl(url) {
2537
- if (url.protocol !== "http:") return false;
2538
- return url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "::1";
2539
- }
2540
- function joinUrl(originOrUrl, path) {
2541
- const base = originOrUrl.endsWith("/") ? originOrUrl : `${originOrUrl}/`;
2542
- const normalizedPath = path.startsWith("/") ? path.slice(1) : path;
2543
- return new URL(normalizedPath, base).toString();
2544
- }
2545
- function resolveInside(root, candidate) {
2546
- if (isAbsolute(candidate)) {
2547
- throw new Error(`Path must be project-relative: ${candidate}`);
2548
- }
2549
- const absolutePath = resolve(root, candidate);
2550
- const relativePath = relative(root, absolutePath);
2551
- if (relativePath.startsWith("..") || isAbsolute(relativePath)) {
2552
- throw new Error(`Path escapes the project: ${candidate}`);
3404
+ function resolvedNamedBaseSemanticCssVars(baseColor, mode) {
3405
+ if (baseColor === "inherit") return {};
3406
+ const seeds = BASE_COLOR_PALETTE[baseColor]?.[mode];
3407
+ if (!seeds) return {};
3408
+ if (mode === "light") {
3409
+ return {
3410
+ // Soft family wash over white — the main light-mode tell (slate vs stone).
3411
+ "--background": `color-mix(in oklab, ${seeds.surfaceHover} 62%, white)`,
3412
+ "--foreground": `color-mix(in oklab, ${seeds.textSecondary} 38%, black)`,
3413
+ "--surface": seeds.surfaceElevated,
3414
+ "--surface-muted": seeds.surfaceHover,
3415
+ "--muted": seeds.surfaceHover,
3416
+ "--muted-foreground": seeds.textSecondary,
3417
+ "--border": seeds.borderSubtle,
3418
+ "--border-strong": `color-mix(in oklab, ${seeds.textSecondary} 30%, transparent)`
3419
+ };
2553
3420
  }
2554
- return absolutePath;
2555
- }
2556
- function toPosixPath(input) {
2557
- return input.split(sep).join("/");
2558
- }
2559
- function parentDirectory(input) {
2560
- return dirname(input);
2561
- }
2562
- function stripKnownComponentPrefix(path) {
2563
- const normalized = path.replace(/\\/g, "/").replace(/^\/+/, "");
2564
- const prefixes = ["components/wensity/", "src/components/wensity/"];
2565
- const prefix = prefixes.find((candidate) => normalized.startsWith(candidate));
2566
- return prefix ? normalized.slice(prefix.length) : normalized;
3421
+ return {
3422
+ // Canvas sits under elevated cards (darker than surfaceElevated).
3423
+ "--background": `color-mix(in oklab, ${seeds.surfaceElevated} 58%, black)`,
3424
+ "--foreground": `color-mix(in oklab, ${seeds.textSecondary} 22%, white)`,
3425
+ "--surface": seeds.surfaceElevated,
3426
+ "--surface-muted": seeds.surfaceHover,
3427
+ "--muted": seeds.surfaceHover,
3428
+ "--muted-foreground": seeds.textSecondary,
3429
+ "--border": seeds.borderSubtle,
3430
+ "--border-strong": `color-mix(in oklab, ${seeds.textSecondary} 38%, transparent)`
3431
+ };
2567
3432
  }
2568
- function sanitizePayloadPath(path, fallbackFileName) {
2569
- const normalized = stripKnownComponentPrefix(path);
2570
- const parts = normalized.split("/").filter(Boolean);
2571
- if (parts.length === 0) return fallbackFileName;
2572
- for (const part of parts) {
2573
- if (part === "." || part === ".." || part.includes("\0")) {
2574
- throw new Error(`Unsafe registry file path: ${path}`);
2575
- }
2576
- }
2577
- const last = parts.at(-1) ?? fallbackFileName;
2578
- if (!/\.(tsx|ts|jsx|js)$/.test(last)) {
2579
- parts[parts.length - 1] = fallbackFileName;
3433
+ var INHERIT_BASE_SEMANTIC_CSS_VARS = {
3434
+ light: {
3435
+ "--background": "#fafafa",
3436
+ "--foreground": "#0a0a0a",
3437
+ "--surface": "#ffffff",
3438
+ "--surface-muted": "#f4f4f5",
3439
+ "--muted": "#f4f4f5",
3440
+ "--muted-foreground": "#52525b",
3441
+ "--border": "rgba(10, 10, 10, 0.08)",
3442
+ "--border-strong": "rgba(10, 10, 10, 0.16)"
3443
+ },
3444
+ dark: {
3445
+ "--background": "#0a0a0b",
3446
+ "--foreground": "#f5f5f6",
3447
+ "--surface": "#111113",
3448
+ "--surface-muted": "#18181b",
3449
+ "--muted": "#1c1c1f",
3450
+ "--muted-foreground": "#a1a1aa",
3451
+ "--border": "rgba(255, 255, 255, 0.08)",
3452
+ "--border-strong": "rgba(255, 255, 255, 0.14)"
2580
3453
  }
2581
- return parts.join("/");
2582
- }
2583
- function componentFileName(slug) {
2584
- return `${slugToPascalCase(slug)}.tsx`;
2585
- }
2586
- function slugToPascalCase(slug) {
2587
- return slug.split(/[^a-zA-Z0-9]+/).filter(Boolean).map((part) => `${part.charAt(0).toUpperCase()}${part.slice(1)}`).join("");
3454
+ };
3455
+ function exportedBaseSemanticCssVars(baseColor, mode) {
3456
+ const named = resolvedNamedBaseSemanticCssVars(baseColor, mode);
3457
+ return Object.keys(named).length > 0 ? named : { ...INHERIT_BASE_SEMANTIC_CSS_VARS[mode] };
3458
+ }
3459
+ var BASE_SEMANTIC_THEME_UTILITIES = {
3460
+ "--color-background": "var(--background)",
3461
+ "--color-foreground": "var(--foreground)",
3462
+ "--color-surface": "var(--surface)",
3463
+ "--color-surface-muted": "var(--surface-muted)",
3464
+ "--color-muted": "var(--muted)",
3465
+ "--color-muted-foreground": "var(--muted-foreground)",
3466
+ "--color-border": "var(--border)",
3467
+ "--color-border-strong": "var(--border-strong)"
3468
+ };
3469
+ var BASE_CSS_START = "/* \u2500\u2500 Wensity Base Tokens \u2014 generated by the Wensity CLI.\n Edit these values to retheme your app; re-running `wensity init --preset`\n replaces this block. \u2500\u2500 */";
3470
+ var BASE_CSS_END = "/* \u2500\u2500 End Wensity Base Tokens \u2500\u2500 */";
3471
+ function formatBaseDeclarations(entries, indent = " ") {
3472
+ return Object.entries(entries).map(([name, value]) => `${indent}${name}: ${value};`).join("\n");
2588
3473
  }
2589
- function extensionlessPath(path) {
2590
- return path.replace(/\.(tsx|ts|jsx|js)$/, "");
3474
+ function buildBaseCssBlock(baseColor = "inherit") {
3475
+ return [
3476
+ BASE_CSS_START,
3477
+ "@theme inline {",
3478
+ formatBaseDeclarations(BASE_SEMANTIC_THEME_UTILITIES),
3479
+ "}",
3480
+ ":root {",
3481
+ formatBaseDeclarations(exportedBaseSemanticCssVars(baseColor, "light")),
3482
+ "}",
3483
+ ".dark {",
3484
+ formatBaseDeclarations(exportedBaseSemanticCssVars(baseColor, "dark")),
3485
+ "}",
3486
+ BASE_CSS_END
3487
+ ].join("\n");
2591
3488
  }
2592
3489
 
2593
- // src/style-shape.ts
3490
+ // ../install-core/src/style-shape.ts
2594
3491
  function isCanonicalStyleShape(value) {
2595
3492
  if (!value || typeof value !== "object" || Array.isArray(value)) return false;
2596
3493
  const record = value;
@@ -2600,30 +3497,184 @@ function normalizeProjectStyle(style) {
2600
3497
  if (isCanonicalStyleShape(style)) {
2601
3498
  return normalizeStyleConfigCanonical(style);
2602
3499
  }
2603
- return normalizeStyleConfig(style);
3500
+ return normalizeStyleConfig(style);
3501
+ }
3502
+ function resolveTokensForStyle(style) {
3503
+ if (isCanonicalStyleShape(style)) {
3504
+ return resolveStyleConfigCanonical(normalizeStyleConfigCanonical(style));
3505
+ }
3506
+ return resolveStyleConfig(normalizeStyleConfig(style));
3507
+ }
3508
+ function baseColorFromStyle(style) {
3509
+ if (isCanonicalStyleShape(style)) {
3510
+ return normalizeStyleConfigCanonical(style).baseColor;
3511
+ }
3512
+ return "inherit";
3513
+ }
3514
+ function curatedFontsFromStyle(style) {
3515
+ if (isCanonicalStyleShape(style)) {
3516
+ const normalized = normalizeStyleConfigCanonical(style);
3517
+ return {
3518
+ headingFont: normalized.headingFont,
3519
+ bodyFont: normalized.bodyFont
3520
+ };
3521
+ }
3522
+ return { headingFont: "system", bodyFont: "system" };
3523
+ }
3524
+
3525
+ // ../install-core/src/tsconfig-paths.ts
3526
+ import { existsSync, readFileSync as readFileSync2, statSync } from "fs";
3527
+ import { join as join4, posix, relative as relative4 } from "path";
3528
+ var MAX_CONFIG_HOPS = 8;
3529
+ var CONFIG_DIR = "${configDir}";
3530
+ function containedFile(projectRoot, relativePath) {
3531
+ let absolute;
3532
+ try {
3533
+ absolute = resolveInside(projectRoot, relativePath);
3534
+ assertNoSymlinkEscape(projectRoot, absolute);
3535
+ } catch {
3536
+ return void 0;
3537
+ }
3538
+ return existsSync(absolute) ? absolute : void 0;
3539
+ }
3540
+ function readPathMapping(projectRoot) {
3541
+ for (const file of ["tsconfig.json", "jsconfig.json"]) {
3542
+ if (!containedFile(projectRoot, file)) continue;
3543
+ const root = parseConfig(projectRoot, file);
3544
+ const own = resolveConfig(projectRoot, file, 0);
3545
+ if (!root || own === UNKNOWN) return void 0;
3546
+ if (own.paths) return toMapping(own, posix.dirname(file));
3547
+ let unresolvedReference = false;
3548
+ for (const reference of root.references ?? []) {
3549
+ if (typeof reference.path !== "string") continue;
3550
+ const referenced = referencePath(projectRoot, posix.join(posix.dirname(file), reference.path));
3551
+ const resolved = referenced ? resolveConfig(projectRoot, referenced, 0) : UNKNOWN;
3552
+ if (resolved === UNKNOWN) unresolvedReference = true;
3553
+ else if (resolved.paths) return toMapping(resolved, posix.dirname(referenced));
3554
+ }
3555
+ return unresolvedReference ? void 0 : toMapping(own, posix.dirname(file));
3556
+ }
3557
+ return void 0;
3558
+ }
3559
+ function aliasResolvesTo(mapping, alias, target) {
3560
+ const wanted = posix.normalize(stripExtension(target));
3561
+ for (const [key, values] of Object.entries(mapping.paths)) {
3562
+ const wildcard = key.endsWith("/*");
3563
+ const prefix = wildcard ? key.slice(0, -1) : key;
3564
+ if (wildcard ? !alias.startsWith(prefix) : alias !== key) continue;
3565
+ const rest = wildcard ? alias.slice(prefix.length) : "";
3566
+ for (const value of values) {
3567
+ const targetBase = value.endsWith("/*") ? value.slice(0, -1) : value;
3568
+ const candidate = posix.normalize(posix.join(mapping.base, targetBase, rest));
3569
+ if (stripExtension(candidate) === wanted) return true;
3570
+ }
3571
+ }
3572
+ return false;
3573
+ }
3574
+ function atAliasRoot(mapping) {
3575
+ const target = mapping?.paths["@/*"]?.find((value) => value.endsWith("/*"));
3576
+ if (target === void 0) return void 0;
3577
+ const dir = posix.normalize(posix.join(mapping.base, target.slice(0, -2)));
3578
+ if (dir === ".." || dir.startsWith("../") || posix.isAbsolute(dir)) return void 0;
3579
+ if (dir === "node_modules" || dir.startsWith("node_modules/")) return void 0;
3580
+ return dir === "." ? "" : dir;
3581
+ }
3582
+ var UNKNOWN = /* @__PURE__ */ Symbol("unknown");
3583
+ function resolveConfig(projectRoot, file, hops) {
3584
+ const parsed = parseConfig(projectRoot, file);
3585
+ if (!parsed || hops > MAX_CONFIG_HOPS) return UNKNOWN;
3586
+ const dir = posix.dirname(file);
3587
+ const options = parsed.compilerOptions ?? {};
3588
+ let resolved = {};
3589
+ const parents = typeof parsed.extends === "string" ? [parsed.extends] : parsed.extends ?? [];
3590
+ for (const parent of parents) {
3591
+ if (typeof parent !== "string") return UNKNOWN;
3592
+ const parentFile = extendsTarget(projectRoot, dir, parent);
3593
+ const inherited = parentFile ? resolveConfig(projectRoot, parentFile, hops + 1) : UNKNOWN;
3594
+ if (inherited === UNKNOWN) return UNKNOWN;
3595
+ resolved = {
3596
+ baseUrl: inherited.baseUrl ?? resolved.baseUrl,
3597
+ paths: inherited.paths ?? resolved.paths
3598
+ };
3599
+ }
3600
+ if (options.baseUrl !== void 0) {
3601
+ resolved.baseUrl = options.baseUrl.startsWith(CONFIG_DIR) ? options.baseUrl : posix.normalize(posix.join(dir, options.baseUrl));
3602
+ }
3603
+ if (options.paths) resolved.paths = { map: options.paths, dir };
3604
+ return resolved;
3605
+ }
3606
+ function toMapping(resolved, configDir) {
3607
+ const expand = (value) => posix.normalize(posix.join(configDir, value.slice(CONFIG_DIR.length)));
3608
+ const baseUrl = resolved.baseUrl?.startsWith(CONFIG_DIR) ? expand(resolved.baseUrl) : resolved.baseUrl;
3609
+ if (!resolved.paths) return { base: baseUrl ?? ".", paths: {} };
3610
+ const base = baseUrl ?? resolved.paths.dir;
3611
+ const rebase = (value) => {
3612
+ if (!value.startsWith(CONFIG_DIR)) return value;
3613
+ const wildcard = value.endsWith("/*");
3614
+ const target = expand(wildcard ? value.slice(0, -2) : value);
3615
+ const rel = posix.relative(base, target) || ".";
3616
+ return wildcard ? `${rel}/*` : rel;
3617
+ };
3618
+ const paths = Object.fromEntries(Object.entries(resolved.paths.map).map(([key, values]) => [key, values.map(rebase)]));
3619
+ return { base, paths };
3620
+ }
3621
+ function extendsTarget(projectRoot, fromDir, specifier) {
3622
+ if (specifier.startsWith("./") || specifier.startsWith("../") || specifier === "." || specifier === "..") {
3623
+ return configFile(projectRoot, posix.join(fromDir, specifier));
3624
+ }
3625
+ if (posix.isAbsolute(specifier) || /^[A-Za-z]:[\\/]/.test(specifier)) return void 0;
3626
+ const inPackage = posix.join("node_modules", specifier);
3627
+ const direct = configFile(projectRoot, inPackage);
3628
+ if (direct) return direct;
3629
+ const manifest = containedFile(projectRoot, posix.join(inPackage, "package.json"));
3630
+ if (manifest) {
3631
+ try {
3632
+ const field = JSON.parse(readFileSync2(manifest, "utf8")).tsconfig;
3633
+ if (typeof field === "string") {
3634
+ const fromField = configFile(projectRoot, posix.join(inPackage, field));
3635
+ if (fromField) return fromField;
3636
+ }
3637
+ } catch {
3638
+ }
3639
+ }
3640
+ return configFile(projectRoot, posix.join(inPackage, "tsconfig.json"));
2604
3641
  }
2605
- function resolveTokensForStyle(style) {
2606
- if (isCanonicalStyleShape(style)) {
2607
- return resolveStyleConfigCanonical(normalizeStyleConfigCanonical(style));
3642
+ function parseConfig(projectRoot, file) {
3643
+ const path = containedFile(projectRoot, file);
3644
+ if (!path) return void 0;
3645
+ try {
3646
+ return JSON.parse(stripJsonComments(readFileSync2(path, "utf8")));
3647
+ } catch {
3648
+ return void 0;
2608
3649
  }
2609
- return resolveStyleConfig(normalizeStyleConfig(style));
2610
3650
  }
2611
- function curatedFontsFromStyle(style) {
2612
- if (isCanonicalStyleShape(style)) {
2613
- const normalized = normalizeStyleConfigCanonical(style);
2614
- return {
2615
- headingFont: normalized.headingFont,
2616
- bodyFont: normalized.bodyFont
2617
- };
3651
+ function configFile(projectRoot, path) {
3652
+ const normalized = posix.normalize(path);
3653
+ for (const candidate of [normalized, `${normalized}.json`]) {
3654
+ const absolute = containedFile(projectRoot, candidate);
3655
+ if (absolute && statSync(absolute).isFile()) return toPosixPath(relative4(projectRoot, absolute));
2618
3656
  }
2619
- return { headingFont: "system", bodyFont: "system" };
3657
+ return void 0;
3658
+ }
3659
+ function referencePath(projectRoot, path) {
3660
+ const absolute = containedFile(projectRoot, path);
3661
+ if (!absolute) return void 0;
3662
+ const file = statSync(absolute).isDirectory() ? join4(absolute, "tsconfig.json") : absolute;
3663
+ const relativeFile = toPosixPath(relative4(projectRoot, file));
3664
+ return containedFile(projectRoot, relativeFile) ? relativeFile : void 0;
3665
+ }
3666
+ function stripJsonComments(text) {
3667
+ return text.replace(/("(?:\\.|[^"\\])*")|\/\*[\s\S]*?\*\/|\/\/[^\n]*/g, (match, string) => string ?? "").replace(/,(\s*[}\]])/g, "$1");
3668
+ }
3669
+ function stripExtension(path) {
3670
+ return path.replace(/\.(tsx|ts|jsx|js)$/, "");
2620
3671
  }
2621
3672
 
2622
- // src/config.ts
2623
- async function loadConfig(cwd, overrides = {}) {
2624
- const foundPath = await findUp(cwd, CONFIG_FILE_NAME);
2625
- const projectRoot = foundPath ? dirname2(foundPath) : cwd;
2626
- const configPath = foundPath ?? join2(projectRoot, CONFIG_FILE_NAME);
3673
+ // ../install-core/src/config.ts
3674
+ async function loadConfig(cwd, overrides = {}, options = {}) {
3675
+ const { projectRoot, configPath: foundPath } = await locateProject(cwd, options.home);
3676
+ const configPath = foundPath ?? join5(projectRoot, CONFIG_FILE_NAME);
3677
+ if (foundPath && options.rejectEscapingSymlink) assertNoSymlinkEscape(projectRoot, foundPath);
2627
3678
  const defaults = createDefaultConfig(projectRoot, overrides.apiUrl ?? DEFAULT_API_ORIGIN);
2628
3679
  if (!foundPath) {
2629
3680
  return {
@@ -2634,7 +3685,7 @@ async function loadConfig(cwd, overrides = {}) {
2634
3685
  };
2635
3686
  }
2636
3687
  try {
2637
- const parsed = JSON.parse(await readFile(foundPath, "utf8"));
3688
+ const parsed = JSON.parse(await readFile2(foundPath, "utf8"));
2638
3689
  return {
2639
3690
  config: normalizeConfig(
2640
3691
  {
@@ -2656,22 +3707,23 @@ async function loadConfig(cwd, overrides = {}) {
2656
3707
  };
2657
3708
  } catch (error) {
2658
3709
  if (error instanceof SyntaxError) {
2659
- throw new CliError(`${CONFIG_FILE_NAME} contains invalid JSON.`);
3710
+ throw new WensityError("config_invalid", `${CONFIG_FILE_NAME} contains invalid JSON.`);
2660
3711
  }
2661
3712
  if (error instanceof PresetValidationError) {
2662
- throw new CliError(`${CONFIG_FILE_NAME}: ${error.message}`);
3713
+ throw new WensityError("config_invalid", `${CONFIG_FILE_NAME}: ${error.message}`);
2663
3714
  }
2664
3715
  throw error;
2665
3716
  }
2666
3717
  }
2667
3718
  function createDefaultConfig(projectRoot, apiUrl = DEFAULT_API_ORIGIN) {
2668
3719
  const apiOrigin = normalizeUrlOrigin(apiUrl);
3720
+ const sourceRoot = defaultSourceRoot(projectRoot);
2669
3721
  return {
2670
3722
  $schema: joinUrl(apiOrigin, "/schema/wensity.json"),
2671
3723
  version: CONFIG_SCHEMA_VERSION,
2672
3724
  registry: joinUrl(apiOrigin, DEFAULT_REGISTRY_PATH),
2673
- components: DEFAULT_COMPONENTS_DIR,
2674
- utils: DEFAULT_UTILS_PATH,
3725
+ components: posix2.join(sourceRoot, "components/wensity"),
3726
+ utils: posix2.join(sourceRoot, "lib/utils"),
2675
3727
  tailwind: {
2676
3728
  config: detectTailwindConfig(projectRoot),
2677
3729
  css: detectGlobalCss(projectRoot)
@@ -2685,20 +3737,21 @@ function createDefaultConfig(projectRoot, apiUrl = DEFAULT_API_ORIGIN) {
2685
3737
  }
2686
3738
  async function writeConfigFile(path, config, force) {
2687
3739
  if (!force && await fileExists(path)) {
2688
- throw new CliError(`${toPosixPath(relative2(process.cwd(), path))} already exists. Use --force to overwrite it.`);
3740
+ throw new WensityError("file_conflict", `${toPosixPath(relative5(process.cwd(), path))} already exists. Use --force to overwrite it.`);
2689
3741
  }
2690
- await mkdir(dirname2(path), { recursive: true });
3742
+ await mkdir2(dirname3(path), { recursive: true });
2691
3743
  await writeFile(path, `${JSON.stringify(config, null, 2)}
2692
3744
  `, "utf8");
2693
3745
  }
2694
3746
  async function mergeStyleIntoConfigFile(configPath, style) {
2695
- const raw = JSON.parse(await readFile(configPath, "utf8"));
3747
+ const raw = JSON.parse(await readFile2(configPath, "utf8"));
3748
+ const defaults = raw.components && raw.utils ? void 0 : createDefaultConfig(dirname3(configPath));
2696
3749
  const merged = {
2697
3750
  ...raw,
2698
3751
  version: raw.version ?? CONFIG_SCHEMA_VERSION,
2699
3752
  registry: raw.registry ?? "",
2700
- components: raw.components ?? DEFAULT_COMPONENTS_DIR,
2701
- utils: raw.utils ?? DEFAULT_UTILS_PATH,
3753
+ components: raw.components ?? defaults?.components ?? DEFAULT_COMPONENTS_DIR,
3754
+ utils: raw.utils ?? defaults?.utils ?? DEFAULT_UTILS_PATH,
2702
3755
  aliases: {
2703
3756
  components: raw.aliases?.components ?? DEFAULT_COMPONENTS_ALIAS,
2704
3757
  utils: raw.aliases?.utils ?? DEFAULT_UTILS_ALIAS
@@ -2716,7 +3769,7 @@ function resolveConfiguredFilePath(projectRoot, relativePath, extension = "ts")
2716
3769
  return resolveInside(projectRoot, withExtension(relativePath, extension));
2717
3770
  } catch (error) {
2718
3771
  const message = error instanceof Error ? error.message : String(error);
2719
- throw new CliError(message);
3772
+ throw new WensityError("path_unsafe", message);
2720
3773
  }
2721
3774
  }
2722
3775
  async function fileExists(path) {
@@ -2728,16 +3781,52 @@ async function fileExists(path) {
2728
3781
  throw error;
2729
3782
  }
2730
3783
  }
2731
- async function findUp(start, fileName) {
2732
- let current = start;
3784
+ function assertProjectRootCandidate(path, home = homedir()) {
3785
+ const resolved = resolve2(path);
3786
+ if (isFilesystemRoot(resolved)) {
3787
+ throw new WensityError(
3788
+ "project_ambiguous",
3789
+ `${resolved} is a filesystem root, not a project. Run Wensity from your project directory (the one with package.json).`
3790
+ );
3791
+ }
3792
+ if (isHomeDirectory(resolved, home)) {
3793
+ throw new WensityError(
3794
+ "project_ambiguous",
3795
+ "Your home directory is not a project. Run Wensity from your project directory (the one with package.json)."
3796
+ );
3797
+ }
3798
+ }
3799
+ async function locateProject(start, home = homedir()) {
3800
+ assertProjectRootCandidate(start, home);
3801
+ const origin = resolve2(start);
3802
+ let current = origin;
2733
3803
  while (true) {
2734
- const candidate = join2(current, fileName);
2735
- if (await fileExists(candidate)) return candidate;
2736
- const parent = dirname2(current);
2737
- if (parent === current) return void 0;
3804
+ const candidate = join5(current, CONFIG_FILE_NAME);
3805
+ if (await fileExists(candidate)) return { projectRoot: current, configPath: candidate };
3806
+ if (await fileExists(join5(current, "package.json"))) return { projectRoot: current, configPath: void 0 };
3807
+ const parent = dirname3(current);
3808
+ if (isFilesystemRoot(parent) || isHomeDirectory(parent, home)) return { projectRoot: origin, configPath: void 0 };
2738
3809
  current = parent;
2739
3810
  }
2740
3811
  }
3812
+ function isFilesystemRoot(path) {
3813
+ return dirname3(path) === path;
3814
+ }
3815
+ function isHomeDirectory(path, home) {
3816
+ if (!home) return false;
3817
+ if (resolve2(path) === resolve2(home)) return true;
3818
+ try {
3819
+ return realpathSync3(path) === realpathSync3(home);
3820
+ } catch {
3821
+ return false;
3822
+ }
3823
+ }
3824
+ function defaultSourceRoot(projectRoot) {
3825
+ const fromAlias = atAliasRoot(readPathMapping(projectRoot));
3826
+ if (fromAlias !== void 0) return fromAlias;
3827
+ const src = join5(projectRoot, "src");
3828
+ return existsSync2(src) && statSync2(src).isDirectory() ? "src" : "";
3829
+ }
2741
3830
  function normalizeConfig(config, overrides) {
2742
3831
  const apiOrigin = overrides.apiUrl ? normalizeUrlOrigin(overrides.apiUrl) : void 0;
2743
3832
  try {
@@ -2757,14 +3846,14 @@ function normalizeConfig(config, overrides) {
2757
3846
  };
2758
3847
  } catch (error) {
2759
3848
  if (error instanceof PresetValidationError) {
2760
- throw new CliError(`${CONFIG_FILE_NAME}: ${error.message}`);
3849
+ throw new WensityError("config_invalid", `${CONFIG_FILE_NAME}: ${error.message}`);
2761
3850
  }
2762
3851
  throw error;
2763
3852
  }
2764
3853
  }
2765
3854
  function detectTailwindConfig(projectRoot) {
2766
3855
  const candidates = ["tailwind.config.ts", "tailwind.config.js", "tailwind.config.mjs"];
2767
- return candidates.find((candidate) => existsSync(join2(projectRoot, candidate))) ?? "tailwind.config.ts";
3856
+ return candidates.find((candidate) => existsSync2(join5(projectRoot, candidate))) ?? "tailwind.config.ts";
2768
3857
  }
2769
3858
  function detectGlobalCss(projectRoot) {
2770
3859
  const candidates = [
@@ -2774,230 +3863,356 @@ function detectGlobalCss(projectRoot) {
2774
3863
  "src/index.css",
2775
3864
  "styles/globals.css"
2776
3865
  ];
2777
- return candidates.find((candidate) => existsSync(join2(projectRoot, candidate))) ?? "src/app/globals.css";
3866
+ return candidates.find((candidate) => existsSync2(join5(projectRoot, candidate))) ?? "src/app/globals.css";
2778
3867
  }
2779
3868
  function withExtension(path, extension) {
2780
3869
  if (/\.(ts|tsx|js|jsx)$/.test(path)) return path;
2781
3870
  return `${path}.${extension}`;
2782
3871
  }
2783
3872
 
2784
- // src/credentials.ts
2785
- import { mkdir as mkdir2, readFile as readFile2, rename, rm, writeFile as writeFile2, chmod } from "fs/promises";
2786
- import { dirname as dirname3, join as join3 } from "path";
2787
- import { randomUUID } from "crypto";
2788
- import { stdin, stdout } from "process";
2789
- function credentialsPath(env) {
2790
- if (env.WENSITY_CREDENTIALS_FILE) return env.WENSITY_CREDENTIALS_FILE;
2791
- const configHome = env.WENSITY_CONFIG_HOME ?? env.XDG_CONFIG_HOME ?? join3(requiredHome(env), ".config");
2792
- return join3(configHome, "wensity", "credentials.json");
3873
+ // ../install-core/src/package-manager.ts
3874
+ import { accessSync, constants as fsConstants2, existsSync as existsSync3, readFileSync as readFileSync3, statSync as statSync3, writeFileSync } from "fs";
3875
+ import { spawn, spawnSync } from "child_process";
3876
+ import { join as join6, posix as posix3, win32 } from "path";
3877
+ var PACKAGE_MANAGERS = ["npm", "pnpm", "yarn", "bun"];
3878
+ function parsePackageManager(value) {
3879
+ if (!value) return void 0;
3880
+ const normalized = value.trim().toLowerCase();
3881
+ if (isPackageManager(normalized)) return normalized;
3882
+ throw new WensityError(
3883
+ "unsupported_package_manager",
3884
+ `Unsupported package manager "${value}". Use one of: ${PACKAGE_MANAGERS.join(", ")}.`
3885
+ );
2793
3886
  }
2794
- async function readCredentials(env) {
2795
- const path = credentialsPath(env);
2796
- try {
2797
- const content = await readFile2(path, "utf8");
2798
- const parsed = JSON.parse(content);
2799
- if (parsed.version !== 1 || !parsed.token || !parsed.apiBaseUrl) {
2800
- throw new CliError(`Credentials at ${path} are not valid. Run wensity login again.`);
2801
- }
2802
- return parsed;
2803
- } catch (error) {
2804
- if (isNodeError(error) && error.code === "ENOENT") return void 0;
2805
- if (error instanceof SyntaxError) {
2806
- throw new CliError(`Credentials at ${path} are not valid JSON. Run wensity login again.`);
2807
- }
2808
- throw error;
3887
+ function packageManagerFromUserAgent(env = process.env) {
3888
+ const agent = env.npm_config_user_agent;
3889
+ if (!agent) return void 0;
3890
+ const first = agent.trim().split(/\s+/)[0] ?? "";
3891
+ const name = first.split("/")[0]?.toLowerCase();
3892
+ return name && isPackageManager(name) ? name : void 0;
3893
+ }
3894
+ function detectPackageManager(projectRoot, env = process.env) {
3895
+ if (existsSync3(join6(projectRoot, "pnpm-lock.yaml"))) return "pnpm";
3896
+ if (existsSync3(join6(projectRoot, "bun.lockb")) || existsSync3(join6(projectRoot, "bun.lock"))) return "bun";
3897
+ if (existsSync3(join6(projectRoot, "yarn.lock"))) return "yarn";
3898
+ if (existsSync3(join6(projectRoot, "package-lock.json"))) return "npm";
3899
+ return packageManagerFromUserAgent(env) ?? "npm";
3900
+ }
3901
+ function isPackageManager(value) {
3902
+ return PACKAGE_MANAGERS.includes(value);
3903
+ }
3904
+ function missingDependencies(projectRoot, dependencies) {
3905
+ const installed = readInstalledPackageNames(projectRoot);
3906
+ if (!installed) return dependencies;
3907
+ return dependencies.filter((dependency) => !installed.has(dependency));
3908
+ }
3909
+ function readInstalledPackageNames(projectRoot) {
3910
+ const packageJsonPath = join6(projectRoot, "package.json");
3911
+ if (!existsSync3(packageJsonPath)) return void 0;
3912
+ const packageJson2 = JSON.parse(readFileSync3(packageJsonPath, "utf8"));
3913
+ return new Set(
3914
+ Object.keys({
3915
+ ...packageJson2.dependencies,
3916
+ ...packageJson2.devDependencies,
3917
+ ...packageJson2.peerDependencies,
3918
+ ...packageJson2.optionalDependencies
3919
+ })
3920
+ );
3921
+ }
3922
+ var YARN_CONFIG_FILE = ".yarnrc.yml";
3923
+ function prepareProjectPackageManager(projectRoot, options = {}) {
3924
+ if (resolvePackageManager(projectRoot, options) !== "yarn") return [];
3925
+ const configPath = join6(projectRoot, YARN_CONFIG_FILE);
3926
+ if (existsSync3(configPath)) return [];
3927
+ if (!options.dryRun) writeFileSync(configPath, "nodeLinker: node-modules\n", "utf8");
3928
+ return [YARN_CONFIG_FILE];
3929
+ }
3930
+ async function runProjectInstall(projectRoot, options = {}) {
3931
+ const packageJsonPath = join6(projectRoot, "package.json");
3932
+ if (!existsSync3(packageJsonPath)) return;
3933
+ const packageManager = resolvePackageManager(projectRoot, options);
3934
+ await runPackageManager(projectRoot, packageManager, ["install"], options);
3935
+ }
3936
+ function resolvePackageManager(projectRoot, options = {}) {
3937
+ return options.packageManager ?? detectPackageManager(projectRoot, options.env);
3938
+ }
3939
+ async function runInstall(projectRoot, dependencies, options = {}) {
3940
+ if (dependencies.length === 0) return;
3941
+ const packageJsonPath = join6(projectRoot, "package.json");
3942
+ if (!existsSync3(packageJsonPath)) {
3943
+ (options.onWarning ?? writeWarning)(
3944
+ `No package.json found. Install these dependencies manually: ${dependencies.join(" ")}`
3945
+ );
3946
+ return;
2809
3947
  }
3948
+ const packageManager = resolvePackageManager(projectRoot, options);
3949
+ await runPackageManager(projectRoot, packageManager, installArgs(packageManager, dependencies), options);
2810
3950
  }
2811
- async function writeCredentials(env, credentials) {
2812
- const path = credentialsPath(env);
2813
- const directory = dirname3(path);
2814
- const temporaryPath = join3(directory, `.credentials.${randomUUID()}.tmp`);
2815
- await mkdir2(directory, { recursive: true, mode: 448 });
2816
- await writeFile2(temporaryPath, `${JSON.stringify(credentials, null, 2)}
2817
- `, {
2818
- encoding: "utf8",
2819
- mode: 384
3951
+ async function runPackageManager(projectRoot, packageManager, args, options) {
3952
+ const outcome = await runPackageManagerCommand(projectRoot, packageManager, args, {
3953
+ output: "inherit",
3954
+ forwardSignals: true,
3955
+ ...options.spawnImpl ? { spawnImpl: options.spawnImpl } : {}
2820
3956
  });
2821
- await rename(temporaryPath, path);
2822
- await chmod(path, 384);
2823
- return path;
3957
+ throwForFailure(packageManager, args, outcome);
2824
3958
  }
2825
- async function deleteCredentials(env) {
2826
- const path = credentialsPath(env);
2827
- try {
2828
- await rm(path, { force: false });
2829
- return true;
2830
- } catch (error) {
2831
- if (isNodeError(error) && error.code === "ENOENT") return false;
2832
- throw error;
3959
+ function throwForFailure(packageManager, args, outcome) {
3960
+ if (outcome.spawnError) {
3961
+ throw new WensityError("dependency_install_failed", `Could not run ${packageManager}: ${outcome.spawnError}`);
3962
+ }
3963
+ if (outcome.timedOut) {
3964
+ throw new WensityError(
3965
+ "dependency_install_failed",
3966
+ `${packageManager} ${args.join(" ")} did not finish within ${PACKAGE_MANAGER_TIMEOUT_MS / 1e3} s and was stopped.`
3967
+ );
3968
+ }
3969
+ if (outcome.exitCode !== 0) {
3970
+ throw new WensityError("dependency_install_failed", `${packageManager} ${args.join(" ")} failed with exit code ${outcome.exitCode ?? 1}.`);
2833
3971
  }
2834
3972
  }
2835
- async function readTokenInteractively(prompt = "Wensity API token: ") {
2836
- if (!stdin.isTTY) {
2837
- const chunks = [];
2838
- for await (const chunk of stdin) {
2839
- chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
2840
- }
2841
- return Buffer.concat(chunks).toString("utf8").trim();
3973
+ var PACKAGE_MANAGER_TIMEOUT_MS = 18e4;
3974
+ var PACKAGE_NAME = /^(@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/;
3975
+ var SUBCOMMANDS = /* @__PURE__ */ new Set(["add", "install"]);
3976
+ var WINDOWS_UNSAFE = /["%!^&|<>()\r\n\t\u0000-\u001f\u007f]/;
3977
+ function assertValidPackageName(name) {
3978
+ if (typeof name !== "string" || name.length > 214 || !PACKAGE_NAME.test(name) || /(^|\/|^@)[-._]/.test(name)) {
3979
+ throw new WensityError("dependency_install_failed", `Refusing to install an invalid package name: ${JSON.stringify(String(name).slice(0, 80))}`);
2842
3980
  }
2843
- return new Promise((resolveToken, rejectToken) => {
2844
- const input = stdin;
2845
- const previousRawMode = input.isRaw;
2846
- let token = "";
2847
- function cleanup() {
2848
- input.off("data", onData);
2849
- if (typeof previousRawMode === "boolean") {
2850
- input.setRawMode(previousRawMode);
2851
- }
2852
- input.pause();
2853
- }
2854
- function onData(chunk) {
2855
- const value = chunk.toString("utf8");
2856
- if (value === "") {
2857
- cleanup();
2858
- stdout.write("\n");
2859
- rejectToken(new CliError("Login cancelled.", 130));
2860
- return;
2861
- }
2862
- if (value === "\r" || value === "\n" || value === "\r\n") {
2863
- cleanup();
2864
- stdout.write("\n");
2865
- resolveToken(token.trim());
2866
- return;
3981
+ }
3982
+ var WINDOWS_JS_ENTRIES = {
3983
+ npm: ["node_modules/npm/bin/npm-cli.js"],
3984
+ pnpm: ["node_modules/pnpm/bin/pnpm.cjs", "node_modules/pnpm/bin/pnpm.mjs"],
3985
+ yarn: ["node_modules/yarn/bin/yarn.js"],
3986
+ bun: []
3987
+ };
3988
+ function buildPackageManagerInvocation(packageManager, args, options = {}) {
3989
+ if (!PACKAGE_MANAGERS.includes(packageManager)) parsePackageManager(packageManager);
3990
+ const [subcommand, ...names] = args;
3991
+ if (!subcommand || !SUBCOMMANDS.has(subcommand)) {
3992
+ throw new WensityError("dependency_install_failed", `Refusing to run ${packageManager} with an unexpected subcommand.`);
3993
+ }
3994
+ for (const name of names) assertValidPackageName(name);
3995
+ const platform = options.platform ?? process.platform;
3996
+ const env = options.env ?? process.env;
3997
+ const display = [packageManager, ...args].join(" ");
3998
+ const isFile = options.isFile ?? defaultIsFile;
3999
+ if (platform !== "win32") {
4000
+ const isExecutable = options.isExecutable ?? defaultIsExecutable;
4001
+ const dirs = (env.PATH ?? "").split(posix3.delimiter).filter(Boolean);
4002
+ const resolved = dirs.map((dir) => posix3.join(dir, packageManager)).find(isExecutable);
4003
+ if (!resolved) throw notFound(packageManager);
4004
+ return { shape: "posix", command: resolved, args, windowsVerbatimArguments: false, display };
4005
+ }
4006
+ const pathValue = env.PATH ?? env.Path ?? Object.entries(env).find(([key]) => key.toLowerCase() === "path")?.[1] ?? "";
4007
+ const extensions = (env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").map((ext) => ext.toLowerCase()).filter(Boolean);
4008
+ for (const dir of pathValue.split(win32.delimiter).filter(Boolean)) {
4009
+ for (const extension of extensions) {
4010
+ const candidate = win32.join(dir, `${packageManager}${extension}`);
4011
+ if (!isFile(candidate)) continue;
4012
+ if (extension === ".exe" || extension === ".com") {
4013
+ return { shape: "windows-exe", command: candidate, args, windowsVerbatimArguments: false, display };
2867
4014
  }
2868
- if (value === "\x7F") {
2869
- token = token.slice(0, -1);
2870
- return;
4015
+ if (extension !== ".cmd" && extension !== ".bat") continue;
4016
+ const entry = WINDOWS_JS_ENTRIES[packageManager].map((relative9) => win32.join(dir, relative9)).find(isFile);
4017
+ if (entry) {
4018
+ return { shape: "windows-node", command: options.nodePath ?? process.execPath, args: [entry, ...args], windowsVerbatimArguments: false, display };
2871
4019
  }
2872
- token += value;
4020
+ const cmd = resolveCmdExe(env);
4021
+ return {
4022
+ shape: "windows-cmd",
4023
+ command: cmd,
4024
+ args: ["/d", "/s", "/c", `"${quoteWindowsCommandLine([candidate, ...args])}"`],
4025
+ windowsVerbatimArguments: true,
4026
+ display
4027
+ };
2873
4028
  }
2874
- stdout.write(prompt);
2875
- input.setRawMode(true);
2876
- input.resume();
2877
- input.on("data", onData);
2878
- });
2879
- }
2880
- function requiredHome(env) {
2881
- if (env.HOME) return env.HOME;
2882
- throw new CliError("HOME is not set, so the CLI cannot locate ~/.config/wensity.");
4029
+ }
4030
+ throw notFound(packageManager);
2883
4031
  }
2884
-
2885
- // src/font-install.ts
2886
- var FONT_CSS_START_MARKER = "/* \u2500\u2500 Wensity Fonts";
2887
- var FONT_CSS_END_MARKER = "/* \u2500\u2500 End Wensity Fonts \u2500\u2500 */";
2888
- function planFontInstall(headingFont, bodyFont) {
2889
- const packages = [];
2890
- const importLines = [];
2891
- const seenPackages = /* @__PURE__ */ new Set();
2892
- const seenImports = /* @__PURE__ */ new Set();
2893
- const pushFont = (meta) => {
2894
- if (!meta) return;
2895
- if (!seenPackages.has(meta.packageName)) {
2896
- seenPackages.add(meta.packageName);
2897
- packages.push(meta.packageName);
2898
- }
2899
- for (const specifier of curatedFontCssImportSpecifiers(meta)) {
2900
- const line = `@import "${specifier}";`;
2901
- if (!seenImports.has(line)) {
2902
- seenImports.add(line);
2903
- importLines.push(line);
2904
- }
4032
+ function quoteWindowsCommandLine(values) {
4033
+ return values.map((value) => {
4034
+ if (value.length === 0 || WINDOWS_UNSAFE.test(value)) {
4035
+ throw new WensityError("dependency_install_failed", `Refusing to build a command line from an unsafe value: ${JSON.stringify(value.slice(0, 80))}`);
2905
4036
  }
2906
- };
2907
- if (headingFont !== "system") {
2908
- pushFont(HEADING_FONT_PACKAGES[headingFont]);
2909
- }
2910
- if (bodyFont !== "system") {
2911
- pushFont(BODY_FONT_PACKAGES[bodyFont]);
4037
+ return /\s/.test(value) ? `"${value}"` : value;
4038
+ }).join(" ");
4039
+ }
4040
+ function resolveCmdExe(env) {
4041
+ const fromEnv = env.ComSpec ?? env.COMSPEC;
4042
+ if (fromEnv && win32.isAbsolute(fromEnv) && win32.basename(fromEnv).toLowerCase() === "cmd.exe" && !WINDOWS_UNSAFE.test(fromEnv)) {
4043
+ return fromEnv;
2912
4044
  }
2913
- packages.sort();
2914
- if (importLines.length === 0) {
2915
- return { packages: [], importLines: [], block: null };
4045
+ const systemRoot = env.SystemRoot ?? env.SYSTEMROOT ?? "C:\\Windows";
4046
+ if (!win32.isAbsolute(systemRoot) || WINDOWS_UNSAFE.test(systemRoot)) {
4047
+ throw new WensityError("dependency_install_failed", "Could not locate cmd.exe safely.");
2916
4048
  }
2917
- const block = [
2918
- `${FONT_CSS_START_MARKER} \u2500\u2500 */`,
2919
- ...importLines,
2920
- FONT_CSS_END_MARKER
2921
- ].join("\n");
2922
- return { packages, importLines, block };
4049
+ return win32.join(systemRoot, "System32", "cmd.exe");
2923
4050
  }
2924
- function upsertFontImportBlock(existingCss, block) {
2925
- const start = existingCss.indexOf(FONT_CSS_START_MARKER);
2926
- const end = existingCss.indexOf(FONT_CSS_END_MARKER);
2927
- if (start !== -1 || end !== -1) {
2928
- if (start === -1 || end === -1 || end <= start) {
2929
- throw new CliError(
2930
- "Configured Tailwind CSS has a partial Wensity Fonts block. Remove the broken markers and re-run install."
2931
- );
2932
- }
2933
- const before2 = existingCss.slice(0, start).replace(/\s*$/, "");
2934
- const after2 = existingCss.slice(end + FONT_CSS_END_MARKER.length).replace(/^\s*/, "");
2935
- if (!block) {
2936
- const joined = [before2, after2].filter((part) => part.length > 0).join("\n\n");
2937
- return joined ? `${joined.replace(/\n{3,}/g, "\n\n")}
2938
- ` : "";
2939
- }
2940
- const normalizedBlock2 = block.trim();
2941
- const prefix2 = before2 ? `${before2}
2942
-
2943
- ` : "";
2944
- const suffix2 = after2 ? `
2945
-
2946
- ${after2}` : "\n";
2947
- return `${prefix2}${normalizedBlock2}${suffix2}`.replace(/\n{3,}/g, "\n\n");
2948
- }
2949
- if (!block) return existingCss;
2950
- const normalizedBlock = block.trim();
2951
- const insertAt = findFontBlockInsertIndex(existingCss);
2952
- const before = existingCss.slice(0, insertAt).replace(/\s*$/, "");
2953
- const after = existingCss.slice(insertAt).replace(/^\s*/, "");
2954
- const prefix = before ? `${before}
2955
-
2956
- ` : "";
2957
- const suffix = after ? `
2958
-
2959
- ${after}` : "\n";
2960
- return `${prefix}${normalizedBlock}${suffix}`.replace(/\n{3,}/g, "\n\n");
4051
+ function notFound(packageManager) {
4052
+ return new WensityError("dependency_install_failed", `Could not run ${packageManager}: it was not found on PATH.`);
2961
4053
  }
2962
- function warnUnusedFontPackageMessage(headingFont, bodyFont, installedPackages) {
2963
- const needed = new Set(planFontInstall(headingFont, bodyFont).packages);
2964
- const curated = [
2965
- ...Object.values(HEADING_FONT_PACKAGES).map((entry) => entry.packageName),
2966
- ...Object.values(BODY_FONT_PACKAGES).map((entry) => entry.packageName)
2967
- ];
2968
- const leftover = curated.filter(
2969
- (pkg) => installedPackages.has(pkg) && !needed.has(pkg)
2970
- );
2971
- if (leftover.length === 0) return void 0;
2972
- return `Project package.json still lists ${leftover.join(", ")} while curated fonts are set to heading="${headingFont}" body="${bodyFont}". Wensity removed the @import for unused fonts but will not uninstall ${leftover.join(", ")} (it may be used outside Wensity).`;
4054
+ function defaultIsExecutable(path) {
4055
+ try {
4056
+ accessSync(path, fsConstants2.X_OK);
4057
+ return statSync3(path).isFile();
4058
+ } catch {
4059
+ return false;
4060
+ }
2973
4061
  }
2974
- function findFontBlockInsertIndex(css) {
2975
- let index = 0;
2976
- const length = css.length;
2977
- while (index < length) {
2978
- while (index < length && /\s/.test(css[index])) index += 1;
2979
- if (index >= length) break;
2980
- if (css.startsWith("/*", index)) {
2981
- const end = css.indexOf("*/", index + 2);
2982
- if (end === -1) break;
2983
- index = end + 2;
2984
- continue;
4062
+ function defaultIsFile(path) {
4063
+ try {
4064
+ return statSync3(path).isFile();
4065
+ } catch {
4066
+ return false;
4067
+ }
4068
+ }
4069
+ var OUTPUT_KEEP = 256 * 1024;
4070
+ var TREE_EXIT_WAIT_MS = 5e3;
4071
+ function runPackageManagerCommand(projectRoot, packageManager, args, options = {}) {
4072
+ const invocation = buildPackageManagerInvocation(packageManager, args, options);
4073
+ const platform = options.platform ?? process.platform;
4074
+ const inherit = options.output === "inherit";
4075
+ const ownGroup = !inherit && platform !== "win32";
4076
+ const timeoutMs = options.timeoutMs ?? (inherit ? void 0 : PACKAGE_MANAGER_TIMEOUT_MS);
4077
+ const started = Date.now();
4078
+ return new Promise((resolve3) => {
4079
+ let stdout2 = "";
4080
+ let stderr = "";
4081
+ let timedOut = false;
4082
+ const child = (options.spawnImpl ?? spawn)(invocation.command, invocation.args, {
4083
+ cwd: projectRoot,
4084
+ env: options.spawnEnv ?? process.env,
4085
+ shell: false,
4086
+ stdio: inherit ? "inherit" : ["ignore", "pipe", "pipe"],
4087
+ windowsHide: true,
4088
+ windowsVerbatimArguments: invocation.windowsVerbatimArguments,
4089
+ detached: ownGroup
4090
+ });
4091
+ const killTree = () => killProcessTree(child.pid, platform, options.env ?? process.env, spawnSync, ownGroup);
4092
+ const signalHandlers = [];
4093
+ if (options.forwardSignals) {
4094
+ for (const signal of ["SIGINT", "SIGTERM"]) {
4095
+ const handler = () => {
4096
+ killTree();
4097
+ removeSignalHandlers();
4098
+ process.kill(process.pid, signal);
4099
+ };
4100
+ signalHandlers.push([signal, handler]);
4101
+ process.once(signal, handler);
4102
+ }
2985
4103
  }
2986
- if (css.startsWith("//", index)) {
2987
- const end = css.indexOf("\n", index);
2988
- if (end === -1) return length;
2989
- index = end + 1;
2990
- continue;
4104
+ const removeSignalHandlers = () => {
4105
+ for (const [signal, handler] of signalHandlers.splice(0)) process.removeListener(signal, handler);
4106
+ };
4107
+ const timer = timeoutMs === void 0 ? void 0 : setTimeout(() => {
4108
+ timedOut = true;
4109
+ killTree();
4110
+ }, timeoutMs);
4111
+ child.stdout?.on("data", (chunk) => {
4112
+ stdout2 = (stdout2 + chunk.toString("utf8")).slice(-OUTPUT_KEEP);
4113
+ });
4114
+ child.stderr?.on("data", (chunk) => {
4115
+ stderr = (stderr + chunk.toString("utf8")).slice(-OUTPUT_KEEP);
4116
+ });
4117
+ let settled = false;
4118
+ const finish = async (exitCode, spawnError) => {
4119
+ if (settled) return;
4120
+ settled = true;
4121
+ clearTimeout(timer);
4122
+ if (ownGroup && child.pid !== void 0 && groupAlive(child.pid)) {
4123
+ killTree();
4124
+ await waitForGroupExit(child.pid);
4125
+ }
4126
+ removeSignalHandlers();
4127
+ child.stdout?.destroy();
4128
+ child.stderr?.destroy();
4129
+ resolve3({ invocation, exitCode, stdout: stdout2, stderr, durationMs: Date.now() - started, timedOut, ...spawnError ? { spawnError } : {} });
4130
+ };
4131
+ child.on("error", (error) => void finish(null, error.message));
4132
+ child.on("exit", (code) => void finish(code));
4133
+ });
4134
+ }
4135
+ function killProcessTree(pid, platform, env, run2 = spawnSync, ownGroup = true) {
4136
+ if (pid === void 0) return;
4137
+ try {
4138
+ if (platform !== "win32" && !ownGroup) {
4139
+ for (const member of [...descendantsOf(pid, run2).reverse(), pid]) {
4140
+ try {
4141
+ process.kill(member, "SIGKILL");
4142
+ } catch {
4143
+ }
4144
+ }
4145
+ return;
2991
4146
  }
2992
- if (css.startsWith("@charset", index) || css.startsWith("@import", index)) {
2993
- const end = css.indexOf(";", index);
2994
- if (end === -1) break;
2995
- index = end + 1;
2996
- continue;
4147
+ if (platform === "win32") {
4148
+ const systemRoot = env.SystemRoot ?? env.SYSTEMROOT ?? "C:\\Windows";
4149
+ run2(win32.join(systemRoot, "System32", "taskkill.exe"), ["/PID", String(pid), "/T", "/F"], { shell: false, stdio: "ignore", windowsHide: true });
4150
+ } else {
4151
+ process.kill(-pid, "SIGKILL");
2997
4152
  }
2998
- break;
4153
+ } catch {
4154
+ }
4155
+ }
4156
+ function descendantsOf(pid, run2) {
4157
+ const listing = run2("ps", ["-A", "-o", "pid=,ppid="], { shell: false, encoding: "utf8" });
4158
+ const children = /* @__PURE__ */ new Map();
4159
+ for (const line of String(listing.stdout ?? "").split("\n")) {
4160
+ const [child, parent] = line.trim().split(/\s+/).map(Number);
4161
+ if (!child || !parent) continue;
4162
+ children.set(parent, [...children.get(parent) ?? [], child]);
4163
+ }
4164
+ const found = [];
4165
+ const queue = [...children.get(pid) ?? []];
4166
+ while (queue.length > 0) {
4167
+ const next = queue.shift();
4168
+ found.push(next);
4169
+ queue.push(...children.get(next) ?? []);
4170
+ }
4171
+ return found;
4172
+ }
4173
+ function groupAlive(pid) {
4174
+ try {
4175
+ process.kill(-pid, 0);
4176
+ return true;
4177
+ } catch (error) {
4178
+ return error.code === "EPERM";
4179
+ }
4180
+ }
4181
+ async function waitForGroupExit(pid) {
4182
+ const deadline = Date.now() + TREE_EXIT_WAIT_MS;
4183
+ while (groupAlive(pid) && Date.now() < deadline) {
4184
+ await new Promise((resolve3) => setTimeout(resolve3, 20));
4185
+ }
4186
+ }
4187
+ function installArgs(packageManager, dependencies) {
4188
+ switch (packageManager) {
4189
+ case "pnpm":
4190
+ return ["add", ...dependencies];
4191
+ case "yarn":
4192
+ return ["add", ...dependencies];
4193
+ case "bun":
4194
+ return ["add", ...dependencies];
4195
+ case "npm":
4196
+ return ["install", ...dependencies];
2999
4197
  }
3000
- return index;
4198
+ }
4199
+ function writeWarning(message) {
4200
+ process.stderr.write(`Warning: ${message}
4201
+ `);
4202
+ }
4203
+
4204
+ // ../install-core/src/project.ts
4205
+ var IMPORT_ALIAS_REMEDY = 'Add a compilerOptions.paths entry such as "@/*": ["./*"] (or ["./src/*"]) to tsconfig.json, or set components, utils and aliases in wensity.json so each alias maps to its folder.';
4206
+ function importAliasesResolve(projectRoot, config) {
4207
+ const mapping = readPathMapping(projectRoot);
4208
+ if (!mapping) return null;
4209
+ return aliasResolvesTo(mapping, config.aliases.components, config.components) && aliasResolvesTo(mapping, config.aliases.utils, config.utils);
4210
+ }
4211
+ function importAliasMismatchMessage(config) {
4212
+ return `Installed files would import ${config.aliases.components} and ${config.aliases.utils}, but the tsconfig/jsconfig paths do not map them to ${config.components} and ${config.utils}, so the imports would not resolve.`;
4213
+ }
4214
+ function importAliasBlockMessage(config) {
4215
+ return `${importAliasMismatchMessage(config)} ${IMPORT_ALIAS_REMEDY}`;
3001
4216
  }
3002
4217
 
3003
4218
  // src/output.ts
@@ -3143,82 +4358,362 @@ function formatProRequiredError(resourceName, resourceType) {
3143
4358
  `Pricing: ${pricingBlue("https://ui.wensity.com/pricing")}`
3144
4359
  ].join("\n");
3145
4360
  }
3146
- function color(message, hex) {
3147
- if (!shouldColor()) return message;
3148
- const [red, green, blue] = hexToRgb(hex);
3149
- return `\x1B[38;2;${red};${green};${blue}m${message}\x1B[39m`;
3150
- }
3151
- function bgBrandRed(message) {
3152
- if (!shouldColor()) return message;
3153
- const [red, green, blue] = hexToRgb(BRAND_RED);
3154
- return `\x1B[48;2;${red};${green};${blue}m${message}\x1B[49m`;
3155
- }
3156
- function shouldColor() {
3157
- if (process.env.FORCE_COLOR && process.env.FORCE_COLOR !== "0") return true;
3158
- return process.env.NO_COLOR === void 0 && process.env.FORCE_COLOR !== "0";
4361
+ function color(message, hex) {
4362
+ if (!shouldColor()) return message;
4363
+ const [red, green, blue] = hexToRgb(hex);
4364
+ return `\x1B[38;2;${red};${green};${blue}m${message}\x1B[39m`;
4365
+ }
4366
+ function bgBrandRed(message) {
4367
+ if (!shouldColor()) return message;
4368
+ const [red, green, blue] = hexToRgb(BRAND_RED);
4369
+ return `\x1B[48;2;${red};${green};${blue}m${message}\x1B[49m`;
4370
+ }
4371
+ function shouldColor() {
4372
+ if (process.env.FORCE_COLOR && process.env.FORCE_COLOR !== "0") return true;
4373
+ return process.env.NO_COLOR === void 0 && process.env.FORCE_COLOR !== "0";
4374
+ }
4375
+ function hexToRgb(hex) {
4376
+ const value = hex.replace("#", "");
4377
+ return [
4378
+ Number.parseInt(value.slice(0, 2), 16),
4379
+ Number.parseInt(value.slice(2, 4), 16),
4380
+ Number.parseInt(value.slice(4, 6), 16)
4381
+ ];
4382
+ }
4383
+ function centerLine(line) {
4384
+ const columns = Math.min(process.stdout.columns || 80, 100);
4385
+ const padding = Math.max(0, Math.floor((columns - line.length) / 2));
4386
+ return `${" ".repeat(padding)}${line}`;
4387
+ }
4388
+
4389
+ // src/errors.ts
4390
+ var PRICING_URL = "https://ui.wensity.com/pricing";
4391
+ var CliError = class _CliError extends Error {
4392
+ exitCode;
4393
+ /** The install-core error this one renders, when there is one. */
4394
+ wensityError;
4395
+ constructor(message, exitCode = 1, options = {}) {
4396
+ super(message, options.cause ? { cause: options.cause } : void 0);
4397
+ this.name = "CliError";
4398
+ this.exitCode = exitCode;
4399
+ if (options.cause) this.wensityError = options.cause;
4400
+ }
4401
+ /**
4402
+ * install-core throws WensityError wherever this code used to throw CliError,
4403
+ * so `instanceof CliError` keeps meaning "a user-facing CLI failure" for both.
4404
+ * Render one with `toCliError` to get the exit code and CLI wording.
4405
+ */
4406
+ static [Symbol.hasInstance](value) {
4407
+ return isWensityError(value) || Function.prototype[Symbol.hasInstance].call(_CliError, value);
4408
+ }
4409
+ static fromWensityError(error) {
4410
+ return new _CliError(renderWensityErrorMessage(error), 1, { cause: error });
4411
+ }
4412
+ };
4413
+ function renderWensityErrorMessage(error) {
4414
+ if (error.code === "entitlement_required") {
4415
+ return `${error.message}
4416
+ Upgrade here: ${pricingBlue(PRICING_URL)}`;
4417
+ }
4418
+ return error.message;
4419
+ }
4420
+ function toCliError(error) {
4421
+ if (isWensityError(error)) return CliError.fromWensityError(error);
4422
+ return error instanceof CliError ? error : void 0;
4423
+ }
4424
+
4425
+ // src/args.ts
4426
+ function parseArgs(argv) {
4427
+ const positionals = [];
4428
+ const options = /* @__PURE__ */ new Map();
4429
+ for (let index = 0; index < argv.length; index += 1) {
4430
+ const token = argv[index];
4431
+ if (!token.startsWith("--")) {
4432
+ positionals.push(token);
4433
+ continue;
4434
+ }
4435
+ if (token === "--") {
4436
+ positionals.push(...argv.slice(index + 1));
4437
+ break;
4438
+ }
4439
+ const inlineValueIndex = token.indexOf("=");
4440
+ const rawName = inlineValueIndex > -1 ? token.slice(2, inlineValueIndex) : token.slice(2);
4441
+ const name = normalizeOptionName(rawName);
4442
+ if (!name) {
4443
+ throw new CliError(`Invalid option: ${token}`);
4444
+ }
4445
+ if (inlineValueIndex > -1) {
4446
+ options.set(name, token.slice(inlineValueIndex + 1));
4447
+ continue;
4448
+ }
4449
+ const nextToken = argv[index + 1];
4450
+ if (nextToken && !nextToken.startsWith("--") && expectsValue(name)) {
4451
+ options.set(name, nextToken);
4452
+ index += 1;
4453
+ continue;
4454
+ }
4455
+ options.set(name, true);
4456
+ }
4457
+ return { positionals, options };
4458
+ }
4459
+ function getStringOption(args, name) {
4460
+ const value = args.options.get(name);
4461
+ if (value === void 0) return void 0;
4462
+ if (typeof value === "boolean") {
4463
+ throw new CliError(`--${name} expects a value.`);
4464
+ }
4465
+ return value;
4466
+ }
4467
+ function hasFlag(args, name) {
4468
+ return args.options.get(name) === true;
4469
+ }
4470
+ function expectsValue(name) {
4471
+ return [
4472
+ "api-url",
4473
+ "registry",
4474
+ "token",
4475
+ "type",
4476
+ "preset",
4477
+ "preset-file",
4478
+ "template",
4479
+ "package-manager"
4480
+ ].includes(name);
4481
+ }
4482
+ function normalizeOptionName(name) {
4483
+ if (name === "dry") return "dry-run";
4484
+ return name;
4485
+ }
4486
+
4487
+ // src/credentials.ts
4488
+ import { stdin, stdout } from "process";
4489
+
4490
+ // ../install-core/src/credentials.ts
4491
+ import { mkdir as mkdir3, readFile as readFile4, rename as rename2, rm as rm2, writeFile as writeFile2, chmod as chmod2 } from "fs/promises";
4492
+ import { dirname as dirname4, join as join8 } from "path";
4493
+ import { randomUUID } from "crypto";
4494
+ function credentialsPath(env) {
4495
+ if (env.WENSITY_CREDENTIALS_FILE) return env.WENSITY_CREDENTIALS_FILE;
4496
+ const configHome = env.WENSITY_CONFIG_HOME ?? env.XDG_CONFIG_HOME ?? join8(requiredHome(env), ".config");
4497
+ return join8(configHome, "wensity", "credentials.json");
4498
+ }
4499
+ async function readCredentials(env) {
4500
+ const path = credentialsPath(env);
4501
+ try {
4502
+ const content = await readFile4(path, "utf8");
4503
+ const parsed = JSON.parse(content);
4504
+ if (parsed.version !== 1 || !parsed.token || !parsed.apiBaseUrl) {
4505
+ throw new WensityError("auth_invalid", `Credentials at ${path} are not valid. Run wensity login again.`);
4506
+ }
4507
+ return parsed;
4508
+ } catch (error) {
4509
+ if (isNodeError(error) && error.code === "ENOENT") return void 0;
4510
+ if (error instanceof SyntaxError) {
4511
+ throw new WensityError("auth_invalid", `Credentials at ${path} are not valid JSON. Run wensity login again.`);
4512
+ }
4513
+ throw error;
4514
+ }
4515
+ }
4516
+ async function writeCredentials(env, credentials) {
4517
+ const path = credentialsPath(env);
4518
+ const directory = dirname4(path);
4519
+ const temporaryPath = join8(directory, `.credentials.${randomUUID()}.tmp`);
4520
+ await mkdir3(directory, { recursive: true, mode: 448 });
4521
+ await writeFile2(temporaryPath, `${JSON.stringify(credentials, null, 2)}
4522
+ `, {
4523
+ encoding: "utf8",
4524
+ mode: 384
4525
+ });
4526
+ await rename2(temporaryPath, path);
4527
+ await chmod2(path, 384);
4528
+ return path;
4529
+ }
4530
+ async function deleteCredentials(env) {
4531
+ const path = credentialsPath(env);
4532
+ try {
4533
+ await rm2(path, { force: false });
4534
+ return true;
4535
+ } catch (error) {
4536
+ if (isNodeError(error) && error.code === "ENOENT") return false;
4537
+ throw error;
4538
+ }
4539
+ }
4540
+ function requiredHome(env) {
4541
+ if (env.HOME) return env.HOME;
4542
+ throw new WensityError("config_missing", "HOME is not set, so the CLI cannot locate ~/.config/wensity.");
4543
+ }
4544
+
4545
+ // src/credentials.ts
4546
+ async function readTokenInteractively(prompt = "Wensity API token: ") {
4547
+ if (!stdin.isTTY) {
4548
+ const chunks = [];
4549
+ for await (const chunk of stdin) {
4550
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
4551
+ }
4552
+ return Buffer.concat(chunks).toString("utf8").trim();
4553
+ }
4554
+ return new Promise((resolveToken, rejectToken) => {
4555
+ const input = stdin;
4556
+ const previousRawMode = input.isRaw;
4557
+ let token = "";
4558
+ function cleanup() {
4559
+ input.off("data", onData);
4560
+ if (typeof previousRawMode === "boolean") {
4561
+ input.setRawMode(previousRawMode);
4562
+ }
4563
+ input.pause();
4564
+ }
4565
+ function onData(chunk) {
4566
+ const value = chunk.toString("utf8");
4567
+ if (value === "") {
4568
+ cleanup();
4569
+ stdout.write("\n");
4570
+ rejectToken(new CliError("Login cancelled.", 130));
4571
+ return;
4572
+ }
4573
+ if (value === "\r" || value === "\n" || value === "\r\n") {
4574
+ cleanup();
4575
+ stdout.write("\n");
4576
+ resolveToken(token.trim());
4577
+ return;
4578
+ }
4579
+ if (value === "\x7F") {
4580
+ token = token.slice(0, -1);
4581
+ return;
4582
+ }
4583
+ token += value;
4584
+ }
4585
+ stdout.write(prompt);
4586
+ input.setRawMode(true);
4587
+ input.resume();
4588
+ input.on("data", onData);
4589
+ });
4590
+ }
4591
+
4592
+ // ../install-core/src/font-install.ts
4593
+ var FONT_CSS_START_MARKER = "/* \u2500\u2500 Wensity Fonts";
4594
+ var FONT_CSS_END_MARKER = "/* \u2500\u2500 End Wensity Fonts \u2500\u2500 */";
4595
+ function planFontInstall(headingFont, bodyFont) {
4596
+ const packages = [];
4597
+ const importLines = [];
4598
+ const seenPackages = /* @__PURE__ */ new Set();
4599
+ const seenImports = /* @__PURE__ */ new Set();
4600
+ const pushFont = (meta) => {
4601
+ if (!meta) return;
4602
+ if (!seenPackages.has(meta.packageName)) {
4603
+ seenPackages.add(meta.packageName);
4604
+ packages.push(meta.packageName);
4605
+ }
4606
+ for (const specifier of curatedFontCssImportSpecifiers(meta)) {
4607
+ const line = `@import "${specifier}";`;
4608
+ if (!seenImports.has(line)) {
4609
+ seenImports.add(line);
4610
+ importLines.push(line);
4611
+ }
4612
+ }
4613
+ };
4614
+ if (headingFont !== "system") {
4615
+ pushFont(HEADING_FONT_PACKAGES[headingFont]);
4616
+ }
4617
+ if (bodyFont !== "system") {
4618
+ pushFont(BODY_FONT_PACKAGES[bodyFont]);
4619
+ }
4620
+ packages.sort();
4621
+ if (importLines.length === 0) {
4622
+ return { packages: [], importLines: [], block: null };
4623
+ }
4624
+ const block = [
4625
+ `${FONT_CSS_START_MARKER} \u2500\u2500 */`,
4626
+ ...importLines,
4627
+ FONT_CSS_END_MARKER
4628
+ ].join("\n");
4629
+ return { packages, importLines, block };
4630
+ }
4631
+ function upsertFontImportBlock(existingCss, block) {
4632
+ const start = existingCss.indexOf(FONT_CSS_START_MARKER);
4633
+ const end = existingCss.indexOf(FONT_CSS_END_MARKER);
4634
+ if (start !== -1 || end !== -1) {
4635
+ if (start === -1 || end === -1 || end <= start) {
4636
+ throw new WensityError(
4637
+ "config_invalid",
4638
+ "Configured Tailwind CSS has a partial Wensity Fonts block. Remove the broken markers and re-run install."
4639
+ );
4640
+ }
4641
+ const before2 = existingCss.slice(0, start).replace(/\s*$/, "");
4642
+ const after2 = existingCss.slice(end + FONT_CSS_END_MARKER.length).replace(/^\s*/, "");
4643
+ if (!block) {
4644
+ const joined = [before2, after2].filter((part) => part.length > 0).join("\n\n");
4645
+ return joined ? `${joined.replace(/\n{3,}/g, "\n\n")}
4646
+ ` : "";
4647
+ }
4648
+ const normalizedBlock2 = block.trim();
4649
+ const prefix2 = before2 ? `${before2}
4650
+
4651
+ ` : "";
4652
+ const suffix2 = after2 ? `
4653
+
4654
+ ${after2}` : "\n";
4655
+ return `${prefix2}${normalizedBlock2}${suffix2}`.replace(/\n{3,}/g, "\n\n");
4656
+ }
4657
+ if (!block) return existingCss;
4658
+ const normalizedBlock = block.trim();
4659
+ const insertAt = findFontBlockInsertIndex(existingCss);
4660
+ const before = existingCss.slice(0, insertAt).replace(/\s*$/, "");
4661
+ const after = existingCss.slice(insertAt).replace(/^\s*/, "");
4662
+ const prefix = before ? `${before}
4663
+
4664
+ ` : "";
4665
+ const suffix = after ? `
4666
+
4667
+ ${after}` : "\n";
4668
+ return `${prefix}${normalizedBlock}${suffix}`.replace(/\n{3,}/g, "\n\n");
3159
4669
  }
3160
- function hexToRgb(hex) {
3161
- const value = hex.replace("#", "");
3162
- return [
3163
- Number.parseInt(value.slice(0, 2), 16),
3164
- Number.parseInt(value.slice(2, 4), 16),
3165
- Number.parseInt(value.slice(4, 6), 16)
4670
+ function warnUnusedFontPackageMessage(headingFont, bodyFont, installedPackages) {
4671
+ const needed = new Set(planFontInstall(headingFont, bodyFont).packages);
4672
+ const curated = [
4673
+ ...Object.values(HEADING_FONT_PACKAGES).map((entry) => entry.packageName),
4674
+ ...Object.values(BODY_FONT_PACKAGES).map((entry) => entry.packageName)
3166
4675
  ];
4676
+ const leftover = curated.filter(
4677
+ (pkg) => installedPackages.has(pkg) && !needed.has(pkg)
4678
+ );
4679
+ if (leftover.length === 0) return void 0;
4680
+ return `Project package.json still lists ${leftover.join(", ")} while curated fonts are set to heading="${headingFont}" body="${bodyFont}". Wensity removed the @import for unused fonts but will not uninstall ${leftover.join(", ")} (it may be used outside Wensity).`;
3167
4681
  }
3168
- function centerLine(line) {
3169
- const columns = Math.min(process.stdout.columns || 80, 100);
3170
- const padding = Math.max(0, Math.floor((columns - line.length) / 2));
3171
- return `${" ".repeat(padding)}${line}`;
4682
+ function findFontBlockInsertIndex(css) {
4683
+ let index = 0;
4684
+ const length = css.length;
4685
+ while (index < length) {
4686
+ while (index < length && /\s/.test(css[index])) index += 1;
4687
+ if (index >= length) break;
4688
+ if (css.startsWith("/*", index)) {
4689
+ const end = css.indexOf("*/", index + 2);
4690
+ if (end === -1) break;
4691
+ index = end + 2;
4692
+ continue;
4693
+ }
4694
+ if (css.startsWith("//", index)) {
4695
+ const end = css.indexOf("\n", index);
4696
+ if (end === -1) return length;
4697
+ index = end + 1;
4698
+ continue;
4699
+ }
4700
+ if (css.startsWith("@charset", index) || css.startsWith("@import", index)) {
4701
+ const end = css.indexOf(";", index);
4702
+ if (end === -1) break;
4703
+ index = end + 1;
4704
+ continue;
4705
+ }
4706
+ break;
4707
+ }
4708
+ return index;
3172
4709
  }
3173
4710
 
3174
4711
  // src/http.ts
3175
- async function requestJson(url, options = {}) {
3176
- const headers = new Headers(options.headers);
3177
- headers.set("accept", "application/json");
3178
- headers.set("user-agent", `${CLI_NAME}/${CLI_VERSION}`);
3179
- if (options.body && !headers.has("content-type")) {
3180
- headers.set("content-type", "application/json");
3181
- }
3182
- if (options.token) {
3183
- headers.set("authorization", `Bearer ${options.token}`);
3184
- }
3185
- let response;
3186
- try {
3187
- response = await fetch(url, { ...options, headers });
3188
- } catch (error) {
3189
- const message = error instanceof Error ? error.message : "Unknown network error";
3190
- throw new CliError(`Could not reach ${url}: ${message}`);
3191
- }
3192
- const text = await response.text();
3193
- const payload = parseJson(text);
3194
- if (!response.ok) {
3195
- const message = formatErrorMessage(payload) ?? response.statusText;
3196
- throw new CliError(`${response.status} ${message}`);
3197
- }
3198
- return payload;
3199
- }
3200
- function parseJson(text) {
3201
- if (!text.trim()) return null;
3202
- try {
3203
- return JSON.parse(text);
3204
- } catch {
3205
- throw new CliError("Registry returned invalid JSON.");
3206
- }
3207
- }
3208
- function formatErrorMessage(payload) {
3209
- if (!payload || typeof payload !== "object") return void 0;
3210
- const record = payload;
3211
- const code = typeof record.error === "string" ? record.error : void 0;
3212
- const message = typeof record.message === "string" ? record.message : code;
3213
- if (code === "pro_license_required") {
3214
- const base = message ?? "This component requires Wensity Pro.";
3215
- return `${base}
3216
- Upgrade here: ${pricingBlue("https://ui.wensity.com/pricing")}`;
3217
- }
3218
- return message;
4712
+ function requestJson2(url, options = {}) {
4713
+ return requestJson(url, { userAgent: `${CLI_NAME}/${CLI_VERSION}`, ...options });
3219
4714
  }
3220
4715
 
3221
- // src/icon-transform.ts
4716
+ // ../install-core/src/icon-transform.ts
3222
4717
  var UI_PRIMITIVES_CATEGORY = "UI Primitives";
3223
4718
  var TABLER_ICONS_PACKAGE = "@tabler/icons-react";
3224
4719
  var LUCIDE_ICONS_PACKAGE = "lucide-react";
@@ -3262,7 +4757,7 @@ function createLucideAdapter() {
3262
4757
  return resolveLucideExport(tablerName);
3263
4758
  } catch (error) {
3264
4759
  const message = error instanceof Error ? error.message : String(error);
3265
- throw new CliError(`${context}: ${message}`);
4760
+ throw new WensityError("source_integrity_failed", `${context}: ${message}`);
3266
4761
  }
3267
4762
  },
3268
4763
  strokePropTo: "strokeWidth"
@@ -3376,13 +4871,15 @@ function rewriteTablerImportsToTarget(content, adapter, context) {
3376
4871
  for (const spec of specs) {
3377
4872
  if (spec.isTypeOnly || typePrefix.includes("type")) {
3378
4873
  if (spec.imported === "Icon" || isIconTransformExclusion(spec.imported)) {
3379
- throw new CliError(
4874
+ throw new WensityError(
4875
+ "source_integrity_failed",
3380
4876
  `${context}: unexpected type-only icon import "${spec.imported}" from ${TABLER_ICONS_PACKAGE}.`
3381
4877
  );
3382
4878
  }
3383
4879
  }
3384
4880
  if (isIconTransformExclusion(spec.imported)) {
3385
- throw new CliError(
4881
+ throw new WensityError(
4882
+ "source_integrity_failed",
3386
4883
  `${context}: "${spec.imported}" is excluded from the icon map (Phase 0 internal geometry). It must not appear as a library import.`
3387
4884
  );
3388
4885
  }
@@ -3405,12 +4902,14 @@ function rewriteTablerImportsToTarget(content, adapter, context) {
3405
4902
  }
3406
4903
  );
3407
4904
  if (hasPackageImport(next, TABLER_ICONS_PACKAGE)) {
3408
- throw new CliError(
4905
+ throw new WensityError(
4906
+ "source_integrity_failed",
3409
4907
  `${context}: failed to rewrite all ${TABLER_ICONS_PACKAGE} imports for ${adapter.id}.`
3410
4908
  );
3411
4909
  }
3412
4910
  if (/(?:import|export)\s+\*\s+as\s+\w+\s+from\s+["']@tabler\/icons-react["']/.test(content)) {
3413
- throw new CliError(
4911
+ throw new WensityError(
4912
+ "source_integrity_failed",
3414
4913
  `${context}: namespace imports from ${TABLER_ICONS_PACKAGE} are not supported. Use named imports.`
3415
4914
  );
3416
4915
  }
@@ -3422,7 +4921,8 @@ function assertNoUnmappedTablerImports(content, context) {
3422
4921
  const specs = parseNamedImportClause(match[1] ?? "");
3423
4922
  for (const spec of specs) {
3424
4923
  if (isIconTransformExclusion(spec.imported)) {
3425
- throw new CliError(
4924
+ throw new WensityError(
4925
+ "source_integrity_failed",
3426
4926
  `${context}: "${spec.imported}" must not appear as a library import (Phase 0 exclusion).`
3427
4927
  );
3428
4928
  }
@@ -3475,70 +4975,12 @@ function rewriteStrokeProps(content, localBindings) {
3475
4975
  }
3476
4976
 
3477
4977
  // src/package-manager.ts
3478
- import { existsSync as existsSync2, readFileSync } from "fs";
3479
- import { spawnSync } from "child_process";
3480
- import { join as join4 } from "path";
3481
- function detectPackageManager(projectRoot) {
3482
- if (existsSync2(join4(projectRoot, "pnpm-lock.yaml"))) return "pnpm";
3483
- if (existsSync2(join4(projectRoot, "bun.lockb")) || existsSync2(join4(projectRoot, "bun.lock"))) return "bun";
3484
- if (existsSync2(join4(projectRoot, "yarn.lock"))) return "yarn";
3485
- if (existsSync2(join4(projectRoot, "package-lock.json"))) return "npm";
3486
- return "npm";
3487
- }
3488
- function missingDependencies(projectRoot, dependencies) {
3489
- const installed = readInstalledPackageNames(projectRoot);
3490
- if (!installed) return dependencies;
3491
- return dependencies.filter((dependency) => !installed.has(dependency));
3492
- }
3493
- function readInstalledPackageNames(projectRoot) {
3494
- const packageJsonPath = join4(projectRoot, "package.json");
3495
- if (!existsSync2(packageJsonPath)) return void 0;
3496
- const packageJson2 = JSON.parse(readFileSync(packageJsonPath, "utf8"));
3497
- return new Set(
3498
- Object.keys({
3499
- ...packageJson2.dependencies,
3500
- ...packageJson2.devDependencies,
3501
- ...packageJson2.peerDependencies,
3502
- ...packageJson2.optionalDependencies
3503
- })
3504
- );
3505
- }
3506
- function runInstall(projectRoot, dependencies) {
3507
- if (dependencies.length === 0) return;
3508
- const packageJsonPath = join4(projectRoot, "package.json");
3509
- if (!existsSync2(packageJsonPath)) {
3510
- warn(`No package.json found. Install these dependencies manually: ${dependencies.join(" ")}`);
3511
- return;
3512
- }
3513
- const packageManager = detectPackageManager(projectRoot);
3514
- const args = installArgs(packageManager, dependencies);
3515
- const result = spawnSync(packageManager, args, {
3516
- cwd: projectRoot,
3517
- stdio: "inherit",
3518
- shell: process.platform === "win32"
3519
- });
3520
- if (result.error) {
3521
- throw new CliError(`Could not run ${packageManager}: ${result.error.message}`);
3522
- }
3523
- if (result.status !== 0) {
3524
- throw new CliError(`${packageManager} ${args.join(" ")} failed with exit code ${result.status ?? 1}.`);
3525
- }
3526
- }
3527
- function installArgs(packageManager, dependencies) {
3528
- switch (packageManager) {
3529
- case "pnpm":
3530
- return ["add", ...dependencies];
3531
- case "yarn":
3532
- return ["add", ...dependencies];
3533
- case "bun":
3534
- return ["add", ...dependencies];
3535
- case "npm":
3536
- return ["install", ...dependencies];
3537
- }
4978
+ function runInstall2(projectRoot, dependencies, options = {}) {
4979
+ return runInstall(projectRoot, dependencies, { onWarning: warn, ...options });
3538
4980
  }
3539
4981
 
3540
4982
  // src/preset.ts
3541
- import { readFile as readFile3 } from "fs/promises";
4983
+ import { readFile as readFile5 } from "fs/promises";
3542
4984
  async function resolvePresetStyle(input) {
3543
4985
  if (input.presetCode && input.presetFile) {
3544
4986
  throw new CliError("Use either --preset or --preset-file, not both.");
@@ -3546,7 +4988,7 @@ async function resolvePresetStyle(input) {
3546
4988
  let code = input.presetCode;
3547
4989
  if (input.presetFile) {
3548
4990
  try {
3549
- const raw = await readFile3(input.presetFile, "utf8");
4991
+ const raw = await readFile5(input.presetFile, "utf8");
3550
4992
  const trimmed = raw.trim();
3551
4993
  if (trimmed.startsWith("{")) {
3552
4994
  const parsed = JSON.parse(trimmed);
@@ -3746,18 +5188,18 @@ function formatTokenPanel(summary) {
3746
5188
  }
3747
5189
 
3748
5190
  // src/preset-init-writes.ts
3749
- import { mkdir as mkdir3, writeFile as writeFile3 } from "fs/promises";
3750
- import { dirname as dirname4 } from "path";
5191
+ import { mkdir as mkdir4, writeFile as writeFile3 } from "fs/promises";
5192
+ import { dirname as dirname5 } from "path";
3751
5193
  async function commitPresetInitWrites(input) {
3752
5194
  let cssWritten = false;
3753
5195
  try {
3754
5196
  if (input.cssChanged) {
3755
- await mkdir3(dirname4(input.cssAbsolute), { recursive: true });
5197
+ await mkdir4(dirname5(input.cssAbsolute), { recursive: true });
3756
5198
  await writeFile3(input.cssAbsolute, input.nextCss, "utf8");
3757
5199
  cssWritten = true;
3758
5200
  }
3759
5201
  if (input.writeUtility) {
3760
- await mkdir3(dirname4(input.utilityAbsolutePath), { recursive: true });
5202
+ await mkdir4(dirname5(input.utilityAbsolutePath), { recursive: true });
3761
5203
  await writeFile3(input.utilityAbsolutePath, input.utilityContent, "utf8");
3762
5204
  }
3763
5205
  await input.commitConfig();
@@ -3773,8 +5215,14 @@ async function commitPresetInitWrites(input) {
3773
5215
  }
3774
5216
 
3775
5217
  // src/project-templates.ts
3776
- import { mkdir as mkdir4, readFile as readFile4, writeFile as writeFile4 } from "fs/promises";
3777
- import { dirname as dirname5, join as join5 } from "path";
5218
+ import { mkdir as mkdir5, readFile as readFile6, writeFile as writeFile4 } from "fs/promises";
5219
+ import { dirname as dirname6, join as join9 } from "path";
5220
+ function templateCss() {
5221
+ return `@import "tailwindcss";
5222
+
5223
+ ${buildBaseCssBlock()}
5224
+ `;
5225
+ }
3778
5226
  var PROJECT_TEMPLATE_IDS = ["next", "vite", "astro", "react-router"];
3779
5227
  function parseProjectTemplate(value) {
3780
5228
  if (!value) return void 0;
@@ -3792,6 +5240,8 @@ function getProjectTemplateDefinition(id) {
3792
5240
  function applyTemplateToConfig(config, template) {
3793
5241
  return {
3794
5242
  ...config,
5243
+ components: "src/components/wensity",
5244
+ utils: "src/lib/utils",
3795
5245
  tailwind: {
3796
5246
  ...config.tailwind,
3797
5247
  css: template.cssPath
@@ -3803,7 +5253,7 @@ async function scaffoldProjectTemplate(projectRoot, template, options) {
3803
5253
  if (!options.force) {
3804
5254
  const conflicts = [];
3805
5255
  for (const file of template.files) {
3806
- if (await fileExists(join5(projectRoot, file.path))) {
5256
+ if (await fileExists(join9(projectRoot, file.path))) {
3807
5257
  conflicts.push(file.path);
3808
5258
  }
3809
5259
  }
@@ -3820,10 +5270,10 @@ async function scaffoldProjectTemplate(projectRoot, template, options) {
3820
5270
  return planned;
3821
5271
  }
3822
5272
  async function writeTemplateFile(projectRoot, file) {
3823
- const absolute = join5(projectRoot, file.path);
3824
- await mkdir4(dirname5(absolute), { recursive: true });
5273
+ const absolute = join9(projectRoot, file.path);
5274
+ await mkdir5(dirname6(absolute), { recursive: true });
3825
5275
  if (file.path === "package.json" && await fileExists(absolute)) {
3826
- const existing = JSON.parse(await readFile4(absolute, "utf8"));
5276
+ const existing = JSON.parse(await readFile6(absolute, "utf8"));
3827
5277
  const next = JSON.parse(file.content);
3828
5278
  await writeFile4(
3829
5279
  absolute,
@@ -3886,11 +5336,14 @@ var templateDefinitions = {
3886
5336
  start: "next start"
3887
5337
  },
3888
5338
  dependencies: {
5339
+ "clsx": "^2.1.1",
3889
5340
  "next": "^16.2.4",
3890
5341
  "react": "^19.2.4",
3891
- "react-dom": "^19.2.4"
5342
+ "react-dom": "^19.2.4",
5343
+ "tailwind-merge": "^3.3.1"
3892
5344
  },
3893
5345
  devDependencies: {
5346
+ "@tailwindcss/postcss": "^4.0.0",
3894
5347
  "@types/node": "^22.10.2",
3895
5348
  "@types/react": "^19.0.0",
3896
5349
  "@types/react-dom": "^19.0.0",
@@ -3899,7 +5352,18 @@ var templateDefinitions = {
3899
5352
  }
3900
5353
  })
3901
5354
  },
3902
- { path: "src/app/globals.css", content: '@import "tailwindcss";\n' },
5355
+ {
5356
+ path: "postcss.config.mjs",
5357
+ content: `const config = {
5358
+ plugins: {
5359
+ "@tailwindcss/postcss": {},
5360
+ },
5361
+ };
5362
+
5363
+ export default config;
5364
+ `
5365
+ },
5366
+ { path: "src/app/globals.css", content: templateCss() },
3903
5367
  {
3904
5368
  path: "src/app/layout.tsx",
3905
5369
  content: `import type { ReactNode } from "react";
@@ -3961,11 +5425,14 @@ export default function RootLayout({ children }: { children: ReactNode }) {
3961
5425
  preview: "vite preview"
3962
5426
  },
3963
5427
  dependencies: {
5428
+ "clsx": "^2.1.1",
3964
5429
  "vite": "^7.3.0",
3965
5430
  "react": "^19.2.4",
3966
- "react-dom": "^19.2.4"
5431
+ "react-dom": "^19.2.4",
5432
+ "tailwind-merge": "^3.3.1"
3967
5433
  },
3968
5434
  devDependencies: {
5435
+ "@tailwindcss/vite": "^4.0.0",
3969
5436
  "@vitejs/plugin-react": "^5.1.2",
3970
5437
  "@types/react": "^19.0.0",
3971
5438
  "@types/react-dom": "^19.0.0",
@@ -3975,7 +5442,7 @@ export default function RootLayout({ children }: { children: ReactNode }) {
3975
5442
  })
3976
5443
  },
3977
5444
  { path: "index.html", content: '<div id="root"></div><script type="module" src="/src/main.tsx"></script>\n' },
3978
- { path: "src/index.css", content: '@import "tailwindcss";\n' },
5445
+ { path: "src/index.css", content: templateCss() },
3979
5446
  {
3980
5447
  path: "src/main.tsx",
3981
5448
  content: `import { createRoot } from "react-dom/client";
@@ -3990,9 +5457,10 @@ createRoot(document.getElementById("root")!).render(
3990
5457
  path: "vite.config.ts",
3991
5458
  content: `import { defineConfig } from "vite";
3992
5459
  import react from "@vitejs/plugin-react";
5460
+ import tailwindcss from "@tailwindcss/vite";
3993
5461
 
3994
5462
  export default defineConfig({
3995
- plugins: [react()],
5463
+ plugins: [react(), tailwindcss()],
3996
5464
  resolve: {
3997
5465
  alias: {
3998
5466
  "@": new URL("./src", import.meta.url).pathname,
@@ -4019,23 +5487,30 @@ export default defineConfig({
4019
5487
  dependencies: {
4020
5488
  "@astrojs/react": "^4.4.0",
4021
5489
  "astro": "^5.16.0",
5490
+ "clsx": "^2.1.1",
4022
5491
  "react": "^19.2.4",
4023
- "react-dom": "^19.2.4"
5492
+ "react-dom": "^19.2.4",
5493
+ "tailwind-merge": "^3.3.1"
4024
5494
  },
4025
5495
  devDependencies: {
5496
+ "@tailwindcss/vite": "^4.0.0",
4026
5497
  "tailwindcss": "^4.0.0",
4027
5498
  "typescript": "^5.7.2"
4028
5499
  }
4029
5500
  })
4030
5501
  },
4031
- { path: "src/styles/global.css", content: '@import "tailwindcss";\n' },
5502
+ { path: "src/styles/global.css", content: templateCss() },
4032
5503
  {
4033
5504
  path: "astro.config.mjs",
4034
5505
  content: `import { defineConfig } from "astro/config";
4035
5506
  import react from "@astrojs/react";
5507
+ import tailwindcss from "@tailwindcss/vite";
4036
5508
 
4037
5509
  export default defineConfig({
4038
5510
  integrations: [react()],
5511
+ vite: {
5512
+ plugins: [tailwindcss()],
5513
+ },
4039
5514
  });
4040
5515
  `
4041
5516
  },
@@ -4064,12 +5539,15 @@ import "../styles/global.css";
4064
5539
  preview: "vite preview"
4065
5540
  },
4066
5541
  dependencies: {
5542
+ "clsx": "^2.1.1",
4067
5543
  "vite": "^7.3.0",
4068
5544
  "react": "^19.2.4",
4069
5545
  "react-dom": "^19.2.4",
4070
- "react-router": "^7.12.0"
5546
+ "react-router": "^7.12.0",
5547
+ "tailwind-merge": "^3.3.1"
4071
5548
  },
4072
5549
  devDependencies: {
5550
+ "@tailwindcss/vite": "^4.0.0",
4073
5551
  "@vitejs/plugin-react": "^5.1.2",
4074
5552
  "@types/react": "^19.0.0",
4075
5553
  "@types/react-dom": "^19.0.0",
@@ -4079,7 +5557,7 @@ import "../styles/global.css";
4079
5557
  })
4080
5558
  },
4081
5559
  { path: "index.html", content: '<div id="root"></div><script type="module" src="/src/main.tsx"></script>\n' },
4082
- { path: "src/app/globals.css", content: '@import "tailwindcss";\n' },
5560
+ { path: "src/app/globals.css", content: templateCss() },
4083
5561
  {
4084
5562
  path: "src/main.tsx",
4085
5563
  content: `import { createRoot } from "react-dom/client";
@@ -4099,9 +5577,10 @@ createRoot(document.getElementById("root")!).render(
4099
5577
  path: "vite.config.ts",
4100
5578
  content: `import { defineConfig } from "vite";
4101
5579
  import react from "@vitejs/plugin-react";
5580
+ import tailwindcss from "@tailwindcss/vite";
4102
5581
 
4103
5582
  export default defineConfig({
4104
- plugins: [react()],
5583
+ plugins: [react(), tailwindcss()],
4105
5584
  resolve: {
4106
5585
  alias: {
4107
5586
  "@": new URL("./src", import.meta.url).pathname,
@@ -4117,7 +5596,7 @@ function formatTemplateFiles(files) {
4117
5596
  return files.map(toPosixPath);
4118
5597
  }
4119
5598
 
4120
- // src/style-css.ts
5599
+ // ../install-core/src/style-css.ts
4121
5600
  var ROOT_SELECTOR = ":root";
4122
5601
  var DARK_SELECTOR = ".dark";
4123
5602
  var SHARED_TOKEN_KEYS = ["radius", "fontSans", "fontDisplay", "fontMono"];
@@ -4201,11 +5680,11 @@ function escapeRegExp(value) {
4201
5680
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4202
5681
  }
4203
5682
 
4204
- // src/component-install.ts
4205
- import { mkdir as mkdir5, readFile as readFile5, writeFile as writeFile5 } from "fs/promises";
4206
- import { dirname as dirname6, join as join6, relative as relative3 } from "path";
5683
+ // ../install-core/src/component-install.ts
5684
+ import { mkdir as mkdir6, readFile as readFile7, writeFile as writeFile5 } from "fs/promises";
5685
+ import { dirname as dirname7, join as join10, relative as relative7 } from "path";
4207
5686
 
4208
- // src/templates.ts
5687
+ // ../install-core/src/templates.ts
4209
5688
  function cnUtilityTemplate() {
4210
5689
  return `import { clsx, type ClassValue } from "clsx";
4211
5690
  import { twMerge } from "tailwind-merge";
@@ -4358,11 +5837,19 @@ export function WensitySecondaryButton({
4358
5837
  `;
4359
5838
  }
4360
5839
 
4361
- // src/component-install.ts
5840
+ // ../install-core/src/component-install.ts
5841
+ function safeTarget(projectRoot, relativePath) {
5842
+ assertTargetPathLength(relativePath);
5843
+ const absolutePath = resolveInside(projectRoot, relativePath);
5844
+ assertNoSymlinkEscape(projectRoot, absolutePath);
5845
+ return absolutePath;
5846
+ }
4362
5847
  var ALWAYS_PRESENT_PACKAGES = /* @__PURE__ */ new Set(["react", "react-dom", "next"]);
4363
5848
  var OVERLAY_HELPER_PATH = "components/wensity/overlay.tsx";
4364
5849
  var PRIMITIVE_CSS_START_MARKER = "/* \u2500\u2500 Wensity Primitive Tokens";
4365
5850
  var PRIMITIVE_CSS_END_MARKER = "/* \u2500\u2500 End Wensity Primitive Tokens \u2500\u2500 */";
5851
+ var BASE_CSS_START_MARKER = "/* \u2500\u2500 Wensity Base Tokens";
5852
+ var BASE_CSS_END_MARKER = "/* \u2500\u2500 End Wensity Base Tokens \u2500\u2500 */";
4366
5853
  async function prepareInstall(projectRoot, config, payload, options = {}) {
4367
5854
  const fallbackFileName = componentFileName(payload.slug);
4368
5855
  const files = [];
@@ -4378,7 +5865,7 @@ async function prepareInstall(projectRoot, config, payload, options = {}) {
4378
5865
  for (const [index, payloadFile] of payload.files.entries()) {
4379
5866
  const payloadFileKind = getPayloadFileKind(payloadFile);
4380
5867
  const relativeOutputPath = getPayloadOutputPath(payloadFile, fallbackFileName, config);
4381
- const absoluteOutputPath = resolveInside(projectRoot, relativeOutputPath);
5868
+ const absoluteOutputPath = safeTarget(projectRoot, relativeOutputPath);
4382
5869
  if (!payloadHelperNeeds) mergeHelperNeeds(inferredHelperNeeds, inferHelperNeeds(payloadFile.content));
4383
5870
  const transformed = transformComponentSource(payloadFile.content, config, {
4384
5871
  isPrimitiveCategory,
@@ -4401,9 +5888,9 @@ async function prepareInstall(projectRoot, config, payload, options = {}) {
4401
5888
  const helperNeeds = payloadHelperNeeds ?? inferredHelperNeeds;
4402
5889
  if (!hasGraphHelperFiles && helperNeeds.utils) {
4403
5890
  const utilityPath = withSourceExtension(config.utils, "ts");
4404
- if (!await fileExists(resolveInside(projectRoot, utilityPath))) {
5891
+ if (!await fileExists(safeTarget(projectRoot, utilityPath))) {
4405
5892
  files.push({
4406
- absolutePath: resolveInside(projectRoot, utilityPath),
5893
+ absolutePath: safeTarget(projectRoot, utilityPath),
4407
5894
  relativePath: utilityPath,
4408
5895
  content: cnUtilityTemplate(),
4409
5896
  kind: "helper"
@@ -4411,11 +5898,11 @@ async function prepareInstall(projectRoot, config, payload, options = {}) {
4411
5898
  }
4412
5899
  }
4413
5900
  if (!hasGraphHelperFiles && helperNeeds.primitives) {
4414
- const primitivesPath = toPosixPath(join6(config.components, "primitives.tsx"));
4415
- if (!await fileExists(resolveInside(projectRoot, primitivesPath))) {
5901
+ const primitivesPath = toPosixPath(join10(config.components, "primitives.tsx"));
5902
+ if (!await fileExists(safeTarget(projectRoot, primitivesPath))) {
4416
5903
  const primitives = primitivesTemplate(config.aliases.utils);
4417
5904
  files.push({
4418
- absolutePath: resolveInside(projectRoot, primitivesPath),
5905
+ absolutePath: safeTarget(projectRoot, primitivesPath),
4419
5906
  relativePath: primitivesPath,
4420
5907
  content: primitives,
4421
5908
  kind: "helper"
@@ -4451,9 +5938,10 @@ async function planPrimitiveCssFile(projectRoot, config, payload) {
4451
5938
  primitiveCss = applyStyleToPrimitiveCss(primitiveCss, style).trim();
4452
5939
  const cssRelative = config.tailwind?.css;
4453
5940
  if (!cssRelative) return void 0;
4454
- const absolutePath = resolveInside(projectRoot, cssRelative);
4455
- const existing = await fileExists(absolutePath) ? await readFile5(absolutePath, "utf8") : "";
4456
- const withTokens = upsertPrimitiveCssBlock(existing, primitiveCss);
5941
+ const absolutePath = safeTarget(projectRoot, cssRelative);
5942
+ const existing = await fileExists(absolutePath) ? await readFile7(absolutePath, "utf8") : "";
5943
+ const withBase = upsertBaseCssBlock(existing, buildBaseCssBlock(baseColorFromStyle(style)));
5944
+ const withTokens = upsertPrimitiveCssBlock(withBase, primitiveCss);
4457
5945
  const fonts = curatedFontsFromStyle(style);
4458
5946
  const fontPlan = planFontInstall(fonts.headingFont, fonts.bodyFont);
4459
5947
  const next = upsertFontImportBlock(withTokens, fontPlan.block);
@@ -4466,19 +5954,45 @@ async function planPrimitiveCssFile(projectRoot, config, payload) {
4466
5954
  };
4467
5955
  }
4468
5956
  function upsertPrimitiveCssBlock(existingCss, block) {
5957
+ return upsertDelimitedBlock(existingCss, block, {
5958
+ startMarker: PRIMITIVE_CSS_START_MARKER,
5959
+ endMarker: PRIMITIVE_CSS_END_MARKER,
5960
+ label: "Wensity Primitive Tokens"
5961
+ });
5962
+ }
5963
+ function upsertBaseCssBlock(existingCss, block) {
5964
+ return upsertDelimitedBlock(existingCss, block, {
5965
+ startMarker: BASE_CSS_START_MARKER,
5966
+ endMarker: BASE_CSS_END_MARKER,
5967
+ label: "Wensity Base Tokens",
5968
+ insertBeforeMarker: PRIMITIVE_CSS_START_MARKER
5969
+ });
5970
+ }
5971
+ function upsertDelimitedBlock(existingCss, block, options) {
4469
5972
  const normalizedBlock = block.trim();
4470
- const start = existingCss.indexOf(PRIMITIVE_CSS_START_MARKER);
4471
- const end = existingCss.indexOf(PRIMITIVE_CSS_END_MARKER);
5973
+ const start = existingCss.indexOf(options.startMarker);
5974
+ const end = existingCss.indexOf(options.endMarker);
4472
5975
  if (start !== -1 && end !== -1 && end > start) {
4473
5976
  const before = existingCss.slice(0, start).replace(/\s*$/, "\n\n");
4474
- const after = existingCss.slice(end + PRIMITIVE_CSS_END_MARKER.length).replace(/^\s*/, "\n");
5977
+ const after = existingCss.slice(end + options.endMarker.length).replace(/^\s*/, "\n");
4475
5978
  return `${before}${normalizedBlock}${after}`.replace(/\n{3,}/g, "\n\n");
4476
5979
  }
4477
5980
  if (start !== -1 || end !== -1) {
4478
- throw new CliError(
4479
- "Configured Tailwind CSS has a partial Wensity Primitive Tokens block. Remove the broken markers and re-run install."
5981
+ throw new WensityError(
5982
+ "config_invalid",
5983
+ `Configured Tailwind CSS has a partial ${options.label} block. Remove the broken markers and re-run install.`
4480
5984
  );
4481
5985
  }
5986
+ if (options.insertBeforeMarker) {
5987
+ const anchor = existingCss.indexOf(options.insertBeforeMarker);
5988
+ if (anchor !== -1) {
5989
+ const before = existingCss.slice(0, anchor).replace(/\s*$/, "\n\n");
5990
+ const after = existingCss.slice(anchor);
5991
+ return `${before}${normalizedBlock}
5992
+
5993
+ ${after}`.replace(/\n{3,}/g, "\n\n");
5994
+ }
5995
+ }
4482
5996
  const trimmed = existingCss.replace(/\s*$/, "");
4483
5997
  if (!trimmed) return `${normalizedBlock}
4484
5998
  `;
@@ -4495,18 +6009,12 @@ async function assertNoConflicts(files, force) {
4495
6009
  if (await fileExists(file.absolutePath)) conflicts.push(file.relativePath);
4496
6010
  }
4497
6011
  if (conflicts.length > 0) {
4498
- throw new CliError(`Refusing to overwrite existing files. Use --force to replace:
6012
+ throw new WensityError("file_conflict", `Refusing to overwrite existing files. Use --force to replace:
4499
6013
  ${conflicts.map((path) => ` - ${path}`).join("\n")}`);
4500
6014
  }
4501
6015
  }
4502
- async function writePlannedFiles(files) {
4503
- for (const file of files) {
4504
- await mkdir5(dirname6(file.absolutePath), { recursive: true });
4505
- await writeFile5(file.absolutePath, file.content, "utf8");
4506
- }
4507
- }
4508
6016
  function summarizeFiles(projectRoot, files) {
4509
- return files.map((file) => toPosixPath(relative3(projectRoot, file.absolutePath)));
6017
+ return files.map((file) => toPosixPath(relative7(projectRoot, file.absolutePath)));
4510
6018
  }
4511
6019
  function transformComponentSource(content, config, iconOptions) {
4512
6020
  let transformed = content;
@@ -4527,13 +6035,13 @@ function getPayloadFileKind(payloadFile) {
4527
6035
  }
4528
6036
  function getPayloadOutputPath(payloadFile, fallbackFileName, config) {
4529
6037
  if (isUtilsHelperFile(payloadFile)) return withSourceExtension(config.utils, "ts");
4530
- if (isOverlayHelperFile(payloadFile)) return toPosixPath(join6(config.components, "overlay.tsx"));
4531
- if (isPrimitivesHelperFile(payloadFile)) return toPosixPath(join6(config.components, "primitives.tsx"));
6038
+ if (isOverlayHelperFile(payloadFile)) return toPosixPath(join10(config.components, "overlay.tsx"));
6039
+ if (isPrimitivesHelperFile(payloadFile)) return toPosixPath(join10(config.components, "primitives.tsx"));
4532
6040
  if (payloadFile.kind === "helper") {
4533
- throw new CliError(`Unsupported registry helper file path: ${payloadFile.path}`);
6041
+ throw new WensityError("path_unsafe", `Unsupported registry helper file path: ${payloadFile.path}`);
4534
6042
  }
4535
6043
  const safePayloadPath = sanitizePayloadPath(payloadFile.path, fallbackFileName);
4536
- return toPosixPath(join6(config.components, safePayloadPath));
6044
+ return toPosixPath(join10(config.components, safePayloadPath));
4537
6045
  }
4538
6046
  function isGraphHelperFile(payloadFile) {
4539
6047
  return isUtilsHelperFile(payloadFile) || isOverlayHelperFile(payloadFile) || isPrimitivesHelperFile(payloadFile);
@@ -4630,7 +6138,7 @@ function normalizePackageName(specifier) {
4630
6138
  function withSourceExtension(path, extension) {
4631
6139
  const directory = parentDirectory(path);
4632
6140
  const fileName = extensionlessPath(path.split("/").at(-1) ?? path);
4633
- return toPosixPath(join6(directory === "." ? "" : directory, `${fileName}.${extension}`));
6141
+ return toPosixPath(join10(directory === "." ? "" : directory, `${fileName}.${extension}`));
4634
6142
  }
4635
6143
 
4636
6144
  // src/commands.ts
@@ -4722,6 +6230,8 @@ async function initCommand(context, parsed) {
4722
6230
  const template = templateId ? getProjectTemplateDefinition(templateId) : void 0;
4723
6231
  const hasPreset = Boolean(presetCode || presetFile);
4724
6232
  const dependencies = ["clsx", "tailwind-merge"];
6233
+ const installOptions = installOptionsFrom(context, parsed);
6234
+ assertProjectRootCandidate(context.cwd, context.env.HOME);
4725
6235
  if (hasPreset) {
4726
6236
  await initWithPreset(context, {
4727
6237
  dryRun,
@@ -4731,19 +6241,26 @@ async function initCommand(context, parsed) {
4731
6241
  presetCode,
4732
6242
  presetFile,
4733
6243
  dependencies,
4734
- template
6244
+ template,
6245
+ installOptions
4735
6246
  });
4736
6247
  return;
4737
6248
  }
4738
6249
  const templateFiles = template ? await scaffoldProjectTemplate(context.cwd, template, { dryRun, force }) : [];
6250
+ const packageManagerFiles = template ? prepareProjectPackageManager(context.cwd, { ...installOptions, dryRun }) : [];
4739
6251
  const config = template ? applyTemplateToConfig(createDefaultConfig(context.cwd, apiUrl), template) : createDefaultConfig(context.cwd, apiUrl);
4740
- const configPath = join7(context.cwd, CONFIG_FILE_NAME);
6252
+ const configPath = join11(context.cwd, CONFIG_FILE_NAME);
4741
6253
  const utilityPath = resolveConfiguredFilePath(context.cwd, config.utils);
4742
6254
  const utilityRelativePath = toPosixPath(config.utils.endsWith(".ts") ? config.utils : `${config.utils}.ts`);
4743
6255
  if (dryRun) {
4744
6256
  printTitle("Project initialization preview");
4745
6257
  print(action("Would write:"));
4746
- print(formatList([...formatTemplateFiles(templateFiles), CONFIG_FILE_NAME, utilityRelativePath]));
6258
+ print(formatList([
6259
+ ...formatTemplateFiles(templateFiles),
6260
+ ...packageManagerFiles,
6261
+ CONFIG_FILE_NAME,
6262
+ utilityRelativePath
6263
+ ]));
4747
6264
  if (template) print(action(`Template: ${template.label}`));
4748
6265
  if (install) print(action(`Would install: ${dependencies.join(" ")}`));
4749
6266
  return;
@@ -4753,14 +6270,24 @@ async function initCommand(context, parsed) {
4753
6270
  await writeTextFile(utilityPath, cnUtilityTemplate());
4754
6271
  }
4755
6272
  if (install) {
4756
- runInstall(context.cwd, missingDependencies(context.cwd, dependencies));
4757
- }
6273
+ if (template) await runProjectInstall(context.cwd, installOptions);
6274
+ await runInstall2(context.cwd, missingDependencies(context.cwd, dependencies), installOptions);
6275
+ }
6276
+ await assertInitPostconditions(context.cwd, [
6277
+ ...templateFiles,
6278
+ ...packageManagerFiles,
6279
+ CONFIG_FILE_NAME,
6280
+ utilityRelativePath
6281
+ ]);
4758
6282
  printTitle("Project initialized");
4759
6283
  print(panel("Created", [
6284
+ ...formatTemplateFiles(templateFiles).map((file) => success(file)),
6285
+ ...packageManagerFiles.map((file) => success(file)),
4760
6286
  success(CONFIG_FILE_NAME),
4761
6287
  success(utilityRelativePath),
4762
6288
  success(`registry: ${apiUrl}`)
4763
6289
  ]));
6290
+ print(action(`Location: ${context.cwd}`));
4764
6291
  print(nextSteps([
4765
6292
  "Browse available components: npx wensity list",
4766
6293
  "Browse premium blocks: npx wensity list --type block",
@@ -4772,18 +6299,22 @@ async function initWithPreset(context, options) {
4772
6299
  presetCode: options.presetCode,
4773
6300
  presetFile: options.presetFile
4774
6301
  });
4775
- const configPath = join7(context.cwd, CONFIG_FILE_NAME);
6302
+ const configPath = join11(context.cwd, CONFIG_FILE_NAME);
4776
6303
  const configExists = await fileExists(configPath);
4777
6304
  const templateFiles = options.template ? await scaffoldProjectTemplate(context.cwd, options.template, {
4778
6305
  dryRun: options.dryRun,
4779
6306
  force: options.force
4780
6307
  }) : [];
6308
+ const packageManagerFiles = options.template ? prepareProjectPackageManager(context.cwd, {
6309
+ ...options.installOptions,
6310
+ dryRun: options.dryRun
6311
+ }) : [];
4781
6312
  let config;
4782
6313
  if (configExists && !options.force) {
4783
6314
  if (options.template) {
4784
6315
  throw new CliError(`${CONFIG_FILE_NAME} already exists. Omit --template for existing projects or use --force.`);
4785
6316
  }
4786
- const loaded = await loadConfig(context.cwd, { apiUrl: options.apiUrl });
6317
+ const loaded = await loadConfig(context.cwd, { apiUrl: options.apiUrl }, { home: context.env.HOME });
4787
6318
  config = {
4788
6319
  ...loaded.config,
4789
6320
  style
@@ -4801,7 +6332,12 @@ async function initWithPreset(context, options) {
4801
6332
  if (options.dryRun) {
4802
6333
  printTitle("Project initialization preview (preset)");
4803
6334
  print(action("Would write:"));
4804
- const planned = [...formatTemplateFiles(templateFiles), CONFIG_FILE_NAME, utilityRelativePath];
6335
+ const planned = [
6336
+ ...formatTemplateFiles(templateFiles),
6337
+ ...packageManagerFiles,
6338
+ CONFIG_FILE_NAME,
6339
+ utilityRelativePath
6340
+ ];
4805
6341
  if (prepared.cssChanged) planned.push(toPosixPath(prepared.cssRelative));
4806
6342
  print(formatList(planned));
4807
6343
  if (options.template) print(action(`Template: ${options.template.label}`));
@@ -4832,11 +6368,25 @@ async function initWithPreset(context, options) {
4832
6368
  }
4833
6369
  });
4834
6370
  if (options.install) {
4835
- runInstall(context.cwd, missingDependencies(context.cwd, installDependencies));
6371
+ if (options.template) await runProjectInstall(context.cwd, options.installOptions);
6372
+ await runInstall2(
6373
+ context.cwd,
6374
+ missingDependencies(context.cwd, installDependencies),
6375
+ options.installOptions
6376
+ );
4836
6377
  }
6378
+ await assertInitPostconditions(context.cwd, [
6379
+ ...templateFiles,
6380
+ ...packageManagerFiles,
6381
+ CONFIG_FILE_NAME,
6382
+ utilityRelativePath,
6383
+ prepared.cssRelative
6384
+ ]);
4837
6385
  printTitle("Project initialized");
4838
6386
  const created = [
4839
6387
  ...options.template ? [success(`template: ${options.template.label}`)] : [],
6388
+ ...formatTemplateFiles(templateFiles).map((file) => success(file)),
6389
+ ...packageManagerFiles.map((file) => success(file)),
4840
6390
  success(CONFIG_FILE_NAME),
4841
6391
  success(utilityRelativePath),
4842
6392
  success(`registry: ${options.apiUrl}`),
@@ -4851,6 +6401,7 @@ async function initWithPreset(context, options) {
4851
6401
  created.push(success(`fonts: ${prepared.fontPlan.packages.join(", ")}`));
4852
6402
  }
4853
6403
  print(panel("Created", created));
6404
+ print(action(`Location: ${context.cwd}`));
4854
6405
  if (prepared.unusedFontWarning) warn(prepared.unusedFontWarning);
4855
6406
  print(nextSteps([
4856
6407
  "Browse available components: npx wensity list",
@@ -4867,7 +6418,7 @@ async function applyCommand(context, parsed) {
4867
6418
  if (!presetCodeInput && !presetFile) {
4868
6419
  throw new CliError("Missing preset. Pass --preset <code> or --preset-file <path>.");
4869
6420
  }
4870
- const loaded = await loadConfig(context.cwd, { registry: registryOverride, apiUrl });
6421
+ const loaded = await loadConfig(context.cwd, { registry: registryOverride, apiUrl }, { home: context.env.HOME });
4871
6422
  if (!loaded.exists) {
4872
6423
  throw new CliError(`${CONFIG_FILE_NAME} not found. Run "wensity init" first, then "wensity apply --preset <code>".`);
4873
6424
  }
@@ -4915,7 +6466,11 @@ async function applyCommand(context, parsed) {
4915
6466
  }
4916
6467
  });
4917
6468
  if (install && installDependencies.length > 0) {
4918
- runInstall(loaded.projectRoot, missingDependencies(loaded.projectRoot, installDependencies));
6469
+ await runInstall2(
6470
+ loaded.projectRoot,
6471
+ missingDependencies(loaded.projectRoot, installDependencies),
6472
+ installOptionsFrom(context, parsed)
6473
+ );
4919
6474
  }
4920
6475
  printTitle("Preset applied");
4921
6476
  const applied = [
@@ -4964,8 +6519,9 @@ async function preparePresetStyleApplication(projectRoot, config, style, operati
4964
6519
  throw new CliError("No Tailwind CSS path configured. Set tailwind.css in wensity.json.");
4965
6520
  }
4966
6521
  const cssAbsolute = resolveInside(projectRoot, cssRelative);
4967
- const existingCss = await fileExists(cssAbsolute) ? await readFile6(cssAbsolute, "utf8") : "";
4968
- const withTokens = upsertPrimitiveCssBlock(existingCss, primitiveCss);
6522
+ const existingCss = await fileExists(cssAbsolute) ? await readFile8(cssAbsolute, "utf8") : "";
6523
+ const withBase = upsertBaseCssBlock(existingCss, buildBaseCssBlock(baseColorFromStyle(style)));
6524
+ const withTokens = upsertPrimitiveCssBlock(withBase, primitiveCss);
4969
6525
  const nextCss = upsertFontImportBlock(withTokens, fontPlan.block);
4970
6526
  const cssChanged = nextCss !== existingCss;
4971
6527
  const installedPackages = readInstalledPackageNames(projectRoot);
@@ -4988,7 +6544,7 @@ async function loginCommand(context, parsed) {
4988
6544
  if (!token) {
4989
6545
  throw new CliError("A token is required. Create one in the Wensity dashboard and run wensity login again.");
4990
6546
  }
4991
- const whoami = await requestJson(joinUrl(apiBaseUrl, "/api/cli/whoami"), { token });
6547
+ const whoami = await requestJson2(joinUrl(apiBaseUrl, "/api/cli/whoami"), { token });
4992
6548
  const path = await writeCredentials(context.env, {
4993
6549
  version: 1,
4994
6550
  apiBaseUrl,
@@ -5017,7 +6573,7 @@ async function logoutCommand(context) {
5017
6573
  }
5018
6574
  async function whoamiCommand(context, parsed) {
5019
6575
  const credentials = await requireCredentials(context.env);
5020
- const whoami = await requestJson(joinUrl(credentials.apiBaseUrl, "/api/cli/whoami"), {
6576
+ const whoami = await requestJson2(joinUrl(credentials.apiBaseUrl, "/api/cli/whoami"), {
5021
6577
  token: credentials.token
5022
6578
  });
5023
6579
  if (hasFlag(parsed, "json")) {
@@ -5031,10 +6587,15 @@ async function listCommand(context, parsed) {
5031
6587
  const resourceType = getResourceType(parsed);
5032
6588
  const apiUrl = getStringOption(parsed, "api-url") ?? context.env.WENSITY_API_URL;
5033
6589
  const registryOverride = getStringOption(parsed, "registry");
5034
- const loaded = await loadConfig(context.cwd, { registry: registryOverride, apiUrl });
5035
- const registry = registryUrlForType(loaded.config.registry, resourceType, registryOverride);
6590
+ const configuredRegistry = await loadConfig(context.cwd, { registry: registryOverride, apiUrl }, { home: context.env.HOME }).then((loaded) => loaded.config.registry).catch((error) => {
6591
+ if (isWensityError(error) && error.code === "project_ambiguous") {
6592
+ return registryOverride ?? createDefaultConfig(context.cwd, apiUrl).registry;
6593
+ }
6594
+ throw error;
6595
+ });
6596
+ const registry = registryUrlForType(configuredRegistry, resourceType, registryOverride);
5036
6597
  if (resourceType === "block") {
5037
- const response2 = await requestJson(registry);
6598
+ const response2 = await requestJson2(registry);
5038
6599
  if (hasFlag(parsed, "json")) {
5039
6600
  printJson(response2);
5040
6601
  return;
@@ -5053,7 +6614,7 @@ async function listCommand(context, parsed) {
5053
6614
  ]));
5054
6615
  return;
5055
6616
  }
5056
- const response = await requestJson(registry);
6617
+ const response = await requestJson2(registry);
5057
6618
  if (hasFlag(parsed, "json")) {
5058
6619
  printJson(response);
5059
6620
  return;
@@ -5095,54 +6656,83 @@ function formatBlockGroup(blocks) {
5095
6656
  }
5096
6657
  async function addCommand(context, parsed, positionals, updateMode) {
5097
6658
  const resourceType = getResourceType(parsed);
5098
- const slug = positionals[0];
5099
- if (!slug) {
5100
- throw new CliError(
5101
- `Missing ${resourceType} slug.
6659
+ const command = updateMode ? "update" : "add";
6660
+ const usage = `Usage: wensity ${command} <slug...>${resourceType === "block" ? " --type block" : ""}`;
6661
+ const slugs = dedupe(positionals);
6662
+ if (slugs.length === 0) {
6663
+ throw new CliError(`Missing ${resourceType} slug.
5102
6664
 
5103
- Usage: wensity ${updateMode ? "update" : "add"} <slug>${resourceType === "block" ? " --type block" : ""}`
5104
- );
6665
+ ${usage}`);
5105
6666
  }
5106
6667
  const apiUrl = getStringOption(parsed, "api-url") ?? context.env.WENSITY_API_URL;
5107
6668
  const registryOverride = getStringOption(parsed, "registry");
5108
- const loaded = await loadConfig(context.cwd, { registry: registryOverride, apiUrl });
6669
+ const loaded = await loadConfig(context.cwd, { registry: registryOverride, apiUrl }, { home: context.env.HOME });
6670
+ if (importAliasesResolve(loaded.projectRoot, loaded.config) === false) {
6671
+ throw new CliError(importAliasBlockMessage(loaded.config));
6672
+ }
5109
6673
  const credentials = await readCredentials(context.env);
5110
6674
  const registry = registryUrlForType(loaded.config.registry, resourceType, registryOverride);
5111
6675
  const force = updateMode || hasFlag(parsed, "force");
5112
- const payload = resourceType === "block" ? await fetchBlockPayload({ slug, registry, credentials }) : await fetchComponentPayload({ slug, registry, credentials });
5113
- const prepared = await prepareInstall(loaded.projectRoot, loaded.config, payload, {
5114
- overwriteExistingDependencyFiles: force
5115
- });
5116
6676
  const dryRun = hasFlag(parsed, "dry-run");
5117
6677
  const install = !hasFlag(parsed, "no-install");
5118
- const plannedInstallFiles = prepared.cssFile ? [...prepared.files, prepared.cssFile] : prepared.files;
5119
- await assertNoConflicts(plannedInstallFiles, force || dryRun);
5120
- const plannedFiles = summarizeFiles(loaded.projectRoot, plannedInstallFiles);
5121
- const missing = missingDependencies(loaded.projectRoot, prepared.dependencies);
6678
+ const payloads = [];
6679
+ for (const slug of slugs) {
6680
+ payloads.push(
6681
+ resourceType === "block" ? await fetchBlockPayload({ slug, registry, credentials }) : await fetchComponentPayload({ slug, registry, credentials })
6682
+ );
6683
+ }
5122
6684
  const style = normalizeProjectStyle(loaded.config.style);
5123
6685
  const fonts = curatedFontsFromStyle(style);
5124
6686
  const installedPackages = readInstalledPackageNames(loaded.projectRoot);
5125
- const oppositeIconWarning = installedPackages && prepared.iconDependencySwap ? warnOppositeIconPackageMessage(style.iconLibrary, installedPackages) : void 0;
5126
6687
  const unusedFontWarning = installedPackages ? warnUnusedFontPackageMessage(fonts.headingFont, fonts.bodyFont, installedPackages) : void 0;
6688
+ const writtenFiles = [];
6689
+ const cssTargets = [];
6690
+ const dependencies = [];
6691
+ const installPlans = [];
6692
+ let iconDependencySwapped = false;
6693
+ for (const payload of payloads) {
6694
+ const prepared = await prepareInstall(loaded.projectRoot, loaded.config, payload, {
6695
+ overwriteExistingDependencyFiles: force
6696
+ });
6697
+ const plannedInstallFiles = prepared.cssFile ? [...prepared.files, prepared.cssFile] : prepared.files;
6698
+ installPlans.push(prepared);
6699
+ writtenFiles.push(...summarizeFiles(loaded.projectRoot, plannedInstallFiles));
6700
+ if (prepared.cssFile) cssTargets.push(prepared.cssFile.relativePath);
6701
+ dependencies.push(...prepared.dependencies);
6702
+ if (prepared.iconDependencySwap) iconDependencySwapped = true;
6703
+ }
6704
+ const allPlannedInstallFiles = installPlans.flatMap(
6705
+ (prepared) => prepared.cssFile ? [...prepared.files, prepared.cssFile] : prepared.files
6706
+ );
6707
+ await assertNoConflicts(allPlannedInstallFiles, force || dryRun);
6708
+ const plannedFiles = dedupe(writtenFiles);
6709
+ const missing = missingDependencies(loaded.projectRoot, dedupe(dependencies));
6710
+ const oppositeIconWarning = installedPackages && iconDependencySwapped ? warnOppositeIconPackageMessage(style.iconLibrary, installedPackages) : void 0;
5127
6711
  if (dryRun) {
5128
6712
  printTitle(updateMode ? "Update preview" : "Install preview");
5129
6713
  print(action("Would write:"));
5130
6714
  print(formatList(plannedFiles));
5131
- if (prepared.cssFile) {
5132
- print(action(`Would update CSS tokens in: ${prepared.cssFile.relativePath}`));
6715
+ for (const target of dedupe(cssTargets)) {
6716
+ print(action(`Would update CSS tokens in: ${target}`));
5133
6717
  }
5134
6718
  if (install && missing.length > 0) print(action(`Would install: ${missing.join(" ")}`));
5135
6719
  if (oppositeIconWarning) warn(oppositeIconWarning);
5136
6720
  if (unusedFontWarning) warn(unusedFontWarning);
5137
6721
  return;
5138
6722
  }
5139
- await writePlannedFiles(plannedInstallFiles);
6723
+ await writeWithRollback(
6724
+ loaded.projectRoot,
6725
+ installPlans.flatMap((prepared) => prepared.cssFile ? [...prepared.files, prepared.cssFile] : prepared.files)
6726
+ );
5140
6727
  if (install) {
5141
- runInstall(loaded.projectRoot, missing);
5142
- }
5143
- printTitle(updateMode ? "Component updated" : "Component installed");
5144
- print(panel(payload.name, [
5145
- success(`${updateMode ? "Updated" : "Added"} ${payload.slug}`),
6728
+ await runInstall2(loaded.projectRoot, missing, installOptionsFrom(context, parsed));
6729
+ }
6730
+ const noun = payloads.length === 1 ? "Component" : "Components";
6731
+ printTitle(updateMode ? `${noun} updated` : `${noun} installed`);
6732
+ print(panel(payloads.map((payload) => payload.name).join(", "), [
6733
+ ...payloads.map(
6734
+ (payload) => success(`${updateMode ? "Updated" : "Added"} ${payload.slug}`)
6735
+ ),
5146
6736
  ...plannedFiles.map((file) => success(file))
5147
6737
  ]));
5148
6738
  if (oppositeIconWarning) warn(oppositeIconWarning);
@@ -5156,62 +6746,99 @@ Usage: wensity ${updateMode ? "update" : "add"} <slug>${resourceType === "block"
5156
6746
  return;
5157
6747
  }
5158
6748
  print(nextSteps([
5159
- "Import the installed file from your Wensity components folder",
6749
+ payloads.length === 1 ? "Import the installed file from your Wensity components folder" : "Import the installed files from your Wensity components folder",
5160
6750
  resourceType === "block" ? "Customize the block copy, colors, and sections" : "Customize props and styling for your app"
5161
6751
  ]));
5162
6752
  }
6753
+ async function writeWithRollback(projectRoot, files) {
6754
+ const byPath = /* @__PURE__ */ new Map();
6755
+ for (const file of files) {
6756
+ byPath.delete(file.absolutePath);
6757
+ byPath.set(file.absolutePath, file);
6758
+ }
6759
+ const result = await apply({
6760
+ projectRoot,
6761
+ planId: `cli_${randomBytes(8).toString("hex")}`,
6762
+ files: [...byPath.values()].map((file) => ({
6763
+ relativePath: toPosixPath(relative8(projectRoot, file.absolutePath)),
6764
+ content: file.content,
6765
+ action: existsSync5(file.absolutePath) ? file.kind === "config" ? "modify" : "overwrite" : "create"
6766
+ }))
6767
+ });
6768
+ if (result.status === "committed") {
6769
+ if (result.cleanupWarning) warn(result.cleanupWarning);
6770
+ return;
6771
+ }
6772
+ const where = result.failedPath ? ` while writing ${result.failedPath}` : "";
6773
+ if (result.status === "rolled_back") {
6774
+ throw new WensityError(
6775
+ result.error.code === "write_failed" ? "write_failed" : result.error.code,
6776
+ `Installation failed${where}; all project-file changes were reverted.
6777
+ ${result.error.message}` + (result.cleanupWarning ? `
6778
+ ${result.cleanupWarning}` : ""),
6779
+ { cause: result.error }
6780
+ );
6781
+ }
6782
+ throw new WensityError(
6783
+ "rollback_failed",
6784
+ [
6785
+ `Installation failed${where}, and some files could not be restored. Nothing was deleted: the backups are in ${result.journalPath}.`,
6786
+ ...result.unrecovered.map((item) => ` - ${item.path}: ${item.recovery}`),
6787
+ "Verify with git diff before removing that directory yourself."
6788
+ ].join("\n"),
6789
+ { cause: result.error }
6790
+ );
6791
+ }
6792
+ function dedupe(values) {
6793
+ return [...new Set(values)];
6794
+ }
5163
6795
  async function fetchComponentPayload(input) {
6796
+ const client = registryClient(input.registry, input.credentials);
5164
6797
  const metadata = await withSpinner(
5165
6798
  `Finding ${input.slug}`,
5166
- () => requestJson(registryMetadataUrl(input.registry, input.slug)),
6799
+ () => client.fetchItemMetadata(input.slug, "component"),
5167
6800
  `Found ${input.slug}`
5168
6801
  );
5169
- if (input.credentials) {
5170
- const credentials = input.credentials;
5171
- const cliEndpoint = metadata.endpoints?.cli ?? `/api/cli/${input.slug}`;
5172
- const endpoint2 = cliEndpoint.startsWith("http") ? cliEndpoint : joinUrl(credentials.apiBaseUrl, cliEndpoint);
5173
- try {
5174
- return await withSpinner(
5175
- `Downloading ${metadata.name}`,
5176
- () => requestJson(endpoint2, { token: credentials.token }),
5177
- `Downloaded ${metadata.name}`
5178
- );
5179
- } catch (error) {
5180
- if (metadata.access === "pro" || !isAuthFailure(error)) throw error;
5181
- warn("Saved Wensity credentials were rejected; falling back to the public free registry payload.");
5182
- }
5183
- }
5184
- if (metadata.access === "pro") {
5185
- throw new CliError(formatProRequiredError(metadata.name, "component"));
6802
+ try {
6803
+ return await client.fetchSourcePayload(input.slug, "component", {
6804
+ metadata,
6805
+ download: (task) => withSpinner(`Downloading ${metadata.name}`, task, `Downloaded ${metadata.name}`),
6806
+ onCredentialsRejected: () => warn("Saved Wensity credentials were rejected; falling back to the public free registry payload.")
6807
+ });
6808
+ } catch (error) {
6809
+ throw proRequiredError(error, metadata.name, "component");
5186
6810
  }
5187
- const publicSourceEndpoint = metadata.endpoints?.registry ?? registrySlugUrl(input.registry, input.slug);
5188
- const endpoint = publicSourceEndpoint.startsWith("http") ? publicSourceEndpoint : joinUrl(registryOrigin(input.registry), publicSourceEndpoint);
5189
- return withSpinner(
5190
- `Downloading ${metadata.name}`,
5191
- () => requestJson(endpoint),
5192
- `Downloaded ${metadata.name}`
5193
- );
5194
- }
5195
- function isAuthFailure(error) {
5196
- return error instanceof CliError && /^(401|403)\b/.test(error.message);
5197
6811
  }
5198
6812
  async function fetchBlockPayload(input) {
6813
+ const client = registryClient(input.registry, input.credentials);
5199
6814
  const metadata = await withSpinner(
5200
6815
  `Finding ${input.slug}`,
5201
- () => requestJson(registryMetadataUrl(input.registry, input.slug)),
6816
+ () => client.fetchItemMetadata(input.slug, "block"),
5202
6817
  `Found ${input.slug}`
5203
6818
  );
5204
- if (!input.credentials) {
5205
- throw new CliError(formatProRequiredError(metadata.name, "block"));
5206
- }
5207
- const credentials = input.credentials;
5208
- const cliEndpoint = metadata.endpoints?.cli ?? `/api/cli/blocks/${input.slug}`;
5209
- const endpoint = cliEndpoint.startsWith("http") ? cliEndpoint : joinUrl(credentials.apiBaseUrl, cliEndpoint);
5210
- return withSpinner(
5211
- `Downloading ${metadata.name}`,
5212
- () => requestJson(endpoint, { token: credentials.token }),
5213
- `Downloaded ${metadata.name}`
5214
- );
6819
+ try {
6820
+ return await client.fetchSourcePayload(input.slug, "block", {
6821
+ metadata,
6822
+ download: (task) => withSpinner(`Downloading ${metadata.name}`, task, `Downloaded ${metadata.name}`)
6823
+ });
6824
+ } catch (error) {
6825
+ throw proRequiredError(error, metadata.name, "block");
6826
+ }
6827
+ }
6828
+ function registryClient(registry, credentials) {
6829
+ return createWensityClient({
6830
+ origin: registryOrigin(registry),
6831
+ registryUrl: registry,
6832
+ blocksRegistryUrl: registry,
6833
+ ...credentials ? { token: credentials.token, apiBaseUrl: credentials.apiBaseUrl } : {},
6834
+ userAgent: `${CLI_NAME}/${CLI_VERSION}`
6835
+ });
6836
+ }
6837
+ function proRequiredError(error, resourceName, resourceType) {
6838
+ if (isWensityError(error) && error.code === "auth_required") {
6839
+ return new CliError(formatProRequiredError(resourceName, resourceType), 1, { cause: error });
6840
+ }
6841
+ return error;
5215
6842
  }
5216
6843
  function getResourceType(parsed) {
5217
6844
  const value = getStringOption(parsed, "type") ?? "component";
@@ -5230,18 +6857,6 @@ async function requireCredentials(env) {
5230
6857
  }
5231
6858
  return credentials;
5232
6859
  }
5233
- function registryMetadataUrl(registry, slug) {
5234
- const url = new URL(registry);
5235
- url.searchParams.set("slug", slug);
5236
- return url.toString();
5237
- }
5238
- function registrySlugUrl(registry, slug) {
5239
- return joinUrl(registry.endsWith("/") ? registry : `${registry}/`, slug);
5240
- }
5241
- function registryOrigin(registry) {
5242
- const url = new URL(registry);
5243
- return `${url.protocol}//${url.host}`;
5244
- }
5245
6860
  function formatHelp() {
5246
6861
  return [
5247
6862
  brandBanner("Own your component source."),
@@ -5269,9 +6884,35 @@ function formatAccountPanel(whoami, savedPath) {
5269
6884
  return panel(isPro ? "Wensity Pro" : "Wensity Free", lines);
5270
6885
  }
5271
6886
  async function writeTextFile(path, content) {
5272
- await mkdir6(dirname7(path), { recursive: true });
6887
+ await mkdir7(dirname8(path), { recursive: true });
5273
6888
  await writeFile6(path, content, "utf8");
5274
6889
  }
6890
+ function installOptionsFrom(context, parsed) {
6891
+ return {
6892
+ packageManager: parsePackageManager(getStringOption(parsed, "package-manager")),
6893
+ env: context.env
6894
+ };
6895
+ }
6896
+ async function assertInitPostconditions(projectRoot, relativePaths) {
6897
+ const missing = [];
6898
+ for (const relativePath of new Set(relativePaths)) {
6899
+ const absolute = join11(projectRoot, relativePath);
6900
+ let size = 0;
6901
+ try {
6902
+ size = (await stat(absolute)).size;
6903
+ } catch {
6904
+ missing.push(relativePath);
6905
+ continue;
6906
+ }
6907
+ if (size === 0) missing.push(relativePath);
6908
+ }
6909
+ if (missing.length > 0) {
6910
+ throw new CliError(
6911
+ `Initialization did not complete. These files are missing or empty in ${projectRoot}:
6912
+ ` + missing.map((file) => ` - ${toPosixPath(file)}`).join("\n")
6913
+ );
6914
+ }
6915
+ }
5275
6916
 
5276
6917
  // src/index.ts
5277
6918
  run({
@@ -5279,10 +6920,11 @@ run({
5279
6920
  cwd: process.cwd(),
5280
6921
  env: process.env
5281
6922
  }).catch((error) => {
5282
- if (error instanceof CliError) {
5283
- process.stderr.write(`${formatCliError(error.message)}
6923
+ const cliError = toCliError(error);
6924
+ if (cliError) {
6925
+ process.stderr.write(`${formatCliError(cliError.message)}
5284
6926
  `);
5285
- process.exitCode = error.exitCode;
6927
+ process.exitCode = cliError.exitCode;
5286
6928
  return;
5287
6929
  }
5288
6930
  const message = error instanceof Error ? error.stack ?? error.message : String(error);