wensity 0.1.12 → 0.1.14

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 (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +6 -1
  3. package/dist/index.js +2006 -647
  4. package/package.json +2 -1
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, stat, 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.12";
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.14";
17
21
  var INIT_PRIMITIVE_CSS_SLUG = "button";
18
22
  var HELP_TEXT = `Wensity CLI ${CLI_VERSION}
19
23
 
@@ -68,85 +72,950 @@ Preset notes:
68
72
  Invalid/expired/unsupported codes exit non-zero with an actionable message
69
73
  `;
70
74
 
71
- // src/errors.ts
72
- var CliError = class extends Error {
73
- exitCode;
74
- constructor(message, exitCode = 1) {
75
- super(message);
76
- this.name = "CliError";
77
- 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;
78
98
  }
79
99
  };
100
+ function isWensityError(error) {
101
+ return error instanceof WensityError;
102
+ }
80
103
  function isNodeError(error) {
81
104
  return error instanceof Error && "code" in error;
82
105
  }
83
106
 
84
- // src/args.ts
85
- function parseArgs(argv) {
86
- const positionals = [];
87
- const options = /* @__PURE__ */ new Map();
88
- for (let index = 0; index < argv.length; index += 1) {
89
- const token = argv[index];
90
- if (!token.startsWith("--")) {
91
- positionals.push(token);
92
- 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);
93
209
  }
94
- if (token === "--") {
95
- positionals.push(...argv.slice(index + 1));
96
- break;
210
+ let text;
211
+ try {
212
+ text = await response.text();
213
+ } catch (error) {
214
+ return transportFailure("Could not read the response from", error);
97
215
  }
98
- const inlineValueIndex = token.indexOf("=");
99
- const rawName = inlineValueIndex > -1 ? token.slice(2, inlineValueIndex) : token.slice(2);
100
- const name = normalizeOptionName(rawName);
101
- if (!name) {
102
- 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
+ };
103
226
  }
104
- if (inlineValueIndex > -1) {
105
- options.set(name, token.slice(inlineValueIndex + 1));
106
- 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;
107
418
  }
108
- const nextToken = argv[index + 1];
109
- if (nextToken && !nextToken.startsWith("--") && expectsValue(name)) {
110
- options.set(name, nextToken);
111
- index += 1;
112
- 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
+ );
113
432
  }
114
- options.set(name, true);
115
433
  }
116
- return { positionals, options };
117
434
  }
118
- function getStringOption(args, name) {
119
- const value = args.options.get(name);
120
- if (value === void 0) return void 0;
121
- if (typeof value === "boolean") {
122
- 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);
123
460
  }
124
- 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
+ };
125
513
  }
126
- function hasFlag(args, name) {
127
- return args.options.get(name) === true;
514
+ function withoutAuthorization(headers) {
515
+ return Object.fromEntries(Object.entries(headers).filter(([name]) => name.toLowerCase() !== "authorization"));
128
516
  }
129
- function expectsValue(name) {
517
+ function isAuthFailure(error) {
518
+ const status = isWensityError(error) ? error.upstream?.status : void 0;
519
+ return status === 401 || status === 403;
520
+ }
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}`;
546
+ }
547
+
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) {
130
669
  return [
131
- "api-url",
132
- "registry",
133
- "token",
134
- "type",
135
- "preset",
136
- "preset-file",
137
- "template",
138
- "package-manager"
139
- ].includes(name);
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
+ ];
140
676
  }
141
- function normalizeOptionName(name) {
142
- if (name === "dry") return "dry-run";
143
- return name;
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
+ }
144
1006
  }
145
1007
 
146
- // src/config.ts
147
- import { existsSync } from "fs";
148
- import { access, mkdir, readFile, writeFile } from "fs/promises";
149
- import { dirname as dirname2, join as join2, relative as relative2 } from "path";
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";
150
1019
 
151
1020
  // ../wensity-preset/src/constants.ts
152
1021
  var ICON_LIBRARIES = ["tabler", "lucide"];
@@ -164,7 +1033,9 @@ var BASE_COLOR_IDS = [
164
1033
  "mauve",
165
1034
  "olive",
166
1035
  "mist",
167
- "black"
1036
+ "black",
1037
+ // Appended, never inserted: preset codes store the list index.
1038
+ "linen"
168
1039
  ];
169
1040
  var THEME_COLOR_IDS = [
170
1041
  "ink",
@@ -236,9 +1107,9 @@ var BODY_FONT_IDS = [
236
1107
  ];
237
1108
  var RADIUS_PRESET_CSS = {
238
1109
  none: "0px",
239
- small: "0.5rem",
240
- medium: "0.875rem",
241
- large: "1.25rem"
1110
+ small: "0.25rem",
1111
+ medium: "0.375rem",
1112
+ large: "0.75rem"
242
1113
  };
243
1114
  var FONT_ID_EXPANSIONS = {
244
1115
  "site:sans": "var(--font-sans)",
@@ -328,7 +1199,7 @@ var DEFAULT_CONTROL_SOLID_DARK = {
328
1199
  };
329
1200
  var DEFAULT_STYLE_CONFIG = {
330
1201
  iconLibrary: "tabler",
331
- radius: "medium",
1202
+ radius: "large",
332
1203
  fonts: { ...DEFAULT_STYLE_FONTS },
333
1204
  colors: {
334
1205
  light: { ...DEFAULT_CONTROL_SOLID_LIGHT },
@@ -353,7 +1224,7 @@ var DEFAULT_STYLE_CONFIG_CANONICAL = {
353
1224
  headingFont: "inter",
354
1225
  bodyFont: "inter",
355
1226
  iconLibrary: "tabler",
356
- radius: "medium"
1227
+ radius: "large"
357
1228
  };
358
1229
  function createDefaultStyleConfigCanonical() {
359
1230
  return {
@@ -1498,6 +2369,7 @@ var TABLER_TO_LUCIDE = {
1498
2369
  IconChevronsRight: "ChevronsRight",
1499
2370
  IconCircleX: "CircleX",
1500
2371
  IconClock: "Clock",
2372
+ IconColorPicker: "Pipette",
1501
2373
  IconCornerDownLeft: "CornerDownLeft",
1502
2374
  IconDots: "Ellipsis",
1503
2375
  IconDownload: "Download",
@@ -1767,6 +2639,26 @@ var BASE_COLOR_PALETTE = {
1767
2639
  textSecondary: "oklch(0.68 0 0)",
1768
2640
  textPlaceholder: "color-mix(in oklab, oklch(0.68 0 0) 78%, transparent)"
1769
2641
  }
2642
+ },
2643
+ linen: {
2644
+ light: {
2645
+ surfaceElevated: "oklch(1 0 0)",
2646
+ surfaceOverlay: "oklch(1 0 0)",
2647
+ surfaceHover: "oklch(0.962 0.004 95)",
2648
+ surfaceActive: "color-mix(in oklab, oklch(0.962 0.004 95) 92%, oklch(0.345 0.01 115) 8%)",
2649
+ borderSubtle: "oklch(0.915 0.006 95)",
2650
+ textSecondary: "oklch(0.55 0.014 105)",
2651
+ textPlaceholder: "color-mix(in oklab, oklch(0.55 0.014 105) 78%, transparent)"
2652
+ },
2653
+ dark: {
2654
+ surfaceElevated: "oklch(0.215 0.003 95)",
2655
+ surfaceOverlay: "oklch(0.25 0.003 95)",
2656
+ surfaceHover: "oklch(0.275 0.004 95)",
2657
+ surfaceActive: "color-mix(in oklab, oklch(0.275 0.004 95) 88%, oklch(0.894 0.01 94) 12%)",
2658
+ borderSubtle: "oklch(0.894 0.01 94 / 9%)",
2659
+ textSecondary: "oklch(0.74 0.016 94)",
2660
+ textPlaceholder: "color-mix(in oklab, oklch(0.74 0.016 94) 78%, transparent)"
2661
+ }
1770
2662
  }
1771
2663
  };
1772
2664
  var THEME_COLOR_PALETTE = {
@@ -2593,99 +3485,31 @@ var BASE_SEMANTIC_THEME_UTILITIES = {
2593
3485
  "--color-surface-muted": "var(--surface-muted)",
2594
3486
  "--color-muted": "var(--muted)",
2595
3487
  "--color-muted-foreground": "var(--muted-foreground)",
2596
- "--color-border": "var(--border)",
2597
- "--color-border-strong": "var(--border-strong)"
2598
- };
2599
- 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 */";
2600
- var BASE_CSS_END = "/* \u2500\u2500 End Wensity Base Tokens \u2500\u2500 */";
2601
- function formatBaseDeclarations(entries, indent = " ") {
2602
- return Object.entries(entries).map(([name, value]) => `${indent}${name}: ${value};`).join("\n");
2603
- }
2604
- function buildBaseCssBlock(baseColor = "inherit") {
2605
- return [
2606
- BASE_CSS_START,
2607
- "@theme inline {",
2608
- formatBaseDeclarations(BASE_SEMANTIC_THEME_UTILITIES),
2609
- "}",
2610
- ":root {",
2611
- formatBaseDeclarations(exportedBaseSemanticCssVars(baseColor, "light")),
2612
- "}",
2613
- ".dark {",
2614
- formatBaseDeclarations(exportedBaseSemanticCssVars(baseColor, "dark")),
2615
- "}",
2616
- BASE_CSS_END
2617
- ].join("\n");
2618
- }
2619
-
2620
- // src/paths.ts
2621
- import { dirname, isAbsolute, relative, resolve, sep } from "path";
2622
- function normalizeUrlOrigin(input) {
2623
- const trimmed = input.trim().replace(/\/+$/, "");
2624
- const withProtocol = /^https?:\/\//i.test(trimmed) ? trimmed : `https://${trimmed}`;
2625
- const url = new URL(withProtocol);
2626
- if (url.protocol !== "https:" && !isLocalHttpUrl(url)) {
2627
- throw new CliError("API URL must use HTTPS, except for localhost development URLs.");
2628
- }
2629
- return url.origin;
2630
- }
2631
- function isLocalHttpUrl(url) {
2632
- if (url.protocol !== "http:") return false;
2633
- return url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "::1";
2634
- }
2635
- function joinUrl(originOrUrl, path) {
2636
- const base = originOrUrl.endsWith("/") ? originOrUrl : `${originOrUrl}/`;
2637
- const normalizedPath = path.startsWith("/") ? path.slice(1) : path;
2638
- return new URL(normalizedPath, base).toString();
2639
- }
2640
- function resolveInside(root, candidate) {
2641
- if (isAbsolute(candidate)) {
2642
- throw new Error(`Path must be project-relative: ${candidate}`);
2643
- }
2644
- const absolutePath = resolve(root, candidate);
2645
- const relativePath = relative(root, absolutePath);
2646
- if (relativePath.startsWith("..") || isAbsolute(relativePath)) {
2647
- throw new Error(`Path escapes the project: ${candidate}`);
2648
- }
2649
- return absolutePath;
2650
- }
2651
- function toPosixPath(input) {
2652
- return input.split(sep).join("/");
2653
- }
2654
- function parentDirectory(input) {
2655
- return dirname(input);
2656
- }
2657
- function stripKnownComponentPrefix(path) {
2658
- const normalized = path.replace(/\\/g, "/").replace(/^\/+/, "");
2659
- const prefixes = ["components/wensity/", "src/components/wensity/"];
2660
- const prefix = prefixes.find((candidate) => normalized.startsWith(candidate));
2661
- return prefix ? normalized.slice(prefix.length) : normalized;
2662
- }
2663
- function sanitizePayloadPath(path, fallbackFileName) {
2664
- const normalized = stripKnownComponentPrefix(path);
2665
- const parts = normalized.split("/").filter(Boolean);
2666
- if (parts.length === 0) return fallbackFileName;
2667
- for (const part of parts) {
2668
- if (part === "." || part === ".." || part.includes("\0")) {
2669
- throw new Error(`Unsafe registry file path: ${path}`);
2670
- }
2671
- }
2672
- const last = parts.at(-1) ?? fallbackFileName;
2673
- if (!/\.(tsx|ts|jsx|js)$/.test(last)) {
2674
- parts[parts.length - 1] = fallbackFileName;
2675
- }
2676
- return parts.join("/");
2677
- }
2678
- function componentFileName(slug) {
2679
- return `${slugToPascalCase(slug)}.tsx`;
2680
- }
2681
- function slugToPascalCase(slug) {
2682
- return slug.split(/[^a-zA-Z0-9]+/).filter(Boolean).map((part) => `${part.charAt(0).toUpperCase()}${part.slice(1)}`).join("");
3488
+ "--color-border": "var(--border)",
3489
+ "--color-border-strong": "var(--border-strong)"
3490
+ };
3491
+ 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 */";
3492
+ var BASE_CSS_END = "/* \u2500\u2500 End Wensity Base Tokens \u2500\u2500 */";
3493
+ function formatBaseDeclarations(entries, indent = " ") {
3494
+ return Object.entries(entries).map(([name, value]) => `${indent}${name}: ${value};`).join("\n");
2683
3495
  }
2684
- function extensionlessPath(path) {
2685
- return path.replace(/\.(tsx|ts|jsx|js)$/, "");
3496
+ function buildBaseCssBlock(baseColor = "inherit") {
3497
+ return [
3498
+ BASE_CSS_START,
3499
+ "@theme inline {",
3500
+ formatBaseDeclarations(BASE_SEMANTIC_THEME_UTILITIES),
3501
+ "}",
3502
+ ":root {",
3503
+ formatBaseDeclarations(exportedBaseSemanticCssVars(baseColor, "light")),
3504
+ "}",
3505
+ ".dark {",
3506
+ formatBaseDeclarations(exportedBaseSemanticCssVars(baseColor, "dark")),
3507
+ "}",
3508
+ BASE_CSS_END
3509
+ ].join("\n");
2686
3510
  }
2687
3511
 
2688
- // src/style-shape.ts
3512
+ // ../install-core/src/style-shape.ts
2689
3513
  function isCanonicalStyleShape(value) {
2690
3514
  if (!value || typeof value !== "object" || Array.isArray(value)) return false;
2691
3515
  const record = value;
@@ -2720,11 +3544,159 @@ function curatedFontsFromStyle(style) {
2720
3544
  return { headingFont: "system", bodyFont: "system" };
2721
3545
  }
2722
3546
 
2723
- // src/config.ts
2724
- async function loadConfig(cwd, overrides = {}) {
2725
- const foundPath = await findUp(cwd, CONFIG_FILE_NAME);
2726
- const projectRoot = foundPath ? dirname2(foundPath) : cwd;
2727
- const configPath = foundPath ?? join2(projectRoot, CONFIG_FILE_NAME);
3547
+ // ../install-core/src/tsconfig-paths.ts
3548
+ import { existsSync, readFileSync as readFileSync2, statSync } from "fs";
3549
+ import { join as join4, posix, relative as relative4 } from "path";
3550
+ var MAX_CONFIG_HOPS = 8;
3551
+ var CONFIG_DIR = "${configDir}";
3552
+ function containedFile(projectRoot, relativePath) {
3553
+ let absolute;
3554
+ try {
3555
+ absolute = resolveInside(projectRoot, relativePath);
3556
+ assertNoSymlinkEscape(projectRoot, absolute);
3557
+ } catch {
3558
+ return void 0;
3559
+ }
3560
+ return existsSync(absolute) ? absolute : void 0;
3561
+ }
3562
+ function readPathMapping(projectRoot) {
3563
+ for (const file of ["tsconfig.json", "jsconfig.json"]) {
3564
+ if (!containedFile(projectRoot, file)) continue;
3565
+ const root = parseConfig(projectRoot, file);
3566
+ const own = resolveConfig(projectRoot, file, 0);
3567
+ if (!root || own === UNKNOWN) return void 0;
3568
+ if (own.paths) return toMapping(own, posix.dirname(file));
3569
+ let unresolvedReference = false;
3570
+ for (const reference of root.references ?? []) {
3571
+ if (typeof reference.path !== "string") continue;
3572
+ const referenced = referencePath(projectRoot, posix.join(posix.dirname(file), reference.path));
3573
+ const resolved = referenced ? resolveConfig(projectRoot, referenced, 0) : UNKNOWN;
3574
+ if (resolved === UNKNOWN) unresolvedReference = true;
3575
+ else if (resolved.paths) return toMapping(resolved, posix.dirname(referenced));
3576
+ }
3577
+ return unresolvedReference ? void 0 : toMapping(own, posix.dirname(file));
3578
+ }
3579
+ return void 0;
3580
+ }
3581
+ function aliasResolvesTo(mapping, alias, target) {
3582
+ const wanted = posix.normalize(stripExtension(target));
3583
+ for (const [key, values] of Object.entries(mapping.paths)) {
3584
+ const wildcard = key.endsWith("/*");
3585
+ const prefix = wildcard ? key.slice(0, -1) : key;
3586
+ if (wildcard ? !alias.startsWith(prefix) : alias !== key) continue;
3587
+ const rest = wildcard ? alias.slice(prefix.length) : "";
3588
+ for (const value of values) {
3589
+ const targetBase = value.endsWith("/*") ? value.slice(0, -1) : value;
3590
+ const candidate = posix.normalize(posix.join(mapping.base, targetBase, rest));
3591
+ if (stripExtension(candidate) === wanted) return true;
3592
+ }
3593
+ }
3594
+ return false;
3595
+ }
3596
+ function atAliasRoot(mapping) {
3597
+ const target = mapping?.paths["@/*"]?.find((value) => value.endsWith("/*"));
3598
+ if (target === void 0) return void 0;
3599
+ const dir = posix.normalize(posix.join(mapping.base, target.slice(0, -2)));
3600
+ if (dir === ".." || dir.startsWith("../") || posix.isAbsolute(dir)) return void 0;
3601
+ if (dir === "node_modules" || dir.startsWith("node_modules/")) return void 0;
3602
+ return dir === "." ? "" : dir;
3603
+ }
3604
+ var UNKNOWN = /* @__PURE__ */ Symbol("unknown");
3605
+ function resolveConfig(projectRoot, file, hops) {
3606
+ const parsed = parseConfig(projectRoot, file);
3607
+ if (!parsed || hops > MAX_CONFIG_HOPS) return UNKNOWN;
3608
+ const dir = posix.dirname(file);
3609
+ const options = parsed.compilerOptions ?? {};
3610
+ let resolved = {};
3611
+ const parents = typeof parsed.extends === "string" ? [parsed.extends] : parsed.extends ?? [];
3612
+ for (const parent of parents) {
3613
+ if (typeof parent !== "string") return UNKNOWN;
3614
+ const parentFile = extendsTarget(projectRoot, dir, parent);
3615
+ const inherited = parentFile ? resolveConfig(projectRoot, parentFile, hops + 1) : UNKNOWN;
3616
+ if (inherited === UNKNOWN) return UNKNOWN;
3617
+ resolved = {
3618
+ baseUrl: inherited.baseUrl ?? resolved.baseUrl,
3619
+ paths: inherited.paths ?? resolved.paths
3620
+ };
3621
+ }
3622
+ if (options.baseUrl !== void 0) {
3623
+ resolved.baseUrl = options.baseUrl.startsWith(CONFIG_DIR) ? options.baseUrl : posix.normalize(posix.join(dir, options.baseUrl));
3624
+ }
3625
+ if (options.paths) resolved.paths = { map: options.paths, dir };
3626
+ return resolved;
3627
+ }
3628
+ function toMapping(resolved, configDir) {
3629
+ const expand = (value) => posix.normalize(posix.join(configDir, value.slice(CONFIG_DIR.length)));
3630
+ const baseUrl = resolved.baseUrl?.startsWith(CONFIG_DIR) ? expand(resolved.baseUrl) : resolved.baseUrl;
3631
+ if (!resolved.paths) return { base: baseUrl ?? ".", paths: {} };
3632
+ const base = baseUrl ?? resolved.paths.dir;
3633
+ const rebase = (value) => {
3634
+ if (!value.startsWith(CONFIG_DIR)) return value;
3635
+ const wildcard = value.endsWith("/*");
3636
+ const target = expand(wildcard ? value.slice(0, -2) : value);
3637
+ const rel = posix.relative(base, target) || ".";
3638
+ return wildcard ? `${rel}/*` : rel;
3639
+ };
3640
+ const paths = Object.fromEntries(Object.entries(resolved.paths.map).map(([key, values]) => [key, values.map(rebase)]));
3641
+ return { base, paths };
3642
+ }
3643
+ function extendsTarget(projectRoot, fromDir, specifier) {
3644
+ if (specifier.startsWith("./") || specifier.startsWith("../") || specifier === "." || specifier === "..") {
3645
+ return configFile(projectRoot, posix.join(fromDir, specifier));
3646
+ }
3647
+ if (posix.isAbsolute(specifier) || /^[A-Za-z]:[\\/]/.test(specifier)) return void 0;
3648
+ const inPackage = posix.join("node_modules", specifier);
3649
+ const direct = configFile(projectRoot, inPackage);
3650
+ if (direct) return direct;
3651
+ const manifest = containedFile(projectRoot, posix.join(inPackage, "package.json"));
3652
+ if (manifest) {
3653
+ try {
3654
+ const field = JSON.parse(readFileSync2(manifest, "utf8")).tsconfig;
3655
+ if (typeof field === "string") {
3656
+ const fromField = configFile(projectRoot, posix.join(inPackage, field));
3657
+ if (fromField) return fromField;
3658
+ }
3659
+ } catch {
3660
+ }
3661
+ }
3662
+ return configFile(projectRoot, posix.join(inPackage, "tsconfig.json"));
3663
+ }
3664
+ function parseConfig(projectRoot, file) {
3665
+ const path = containedFile(projectRoot, file);
3666
+ if (!path) return void 0;
3667
+ try {
3668
+ return JSON.parse(stripJsonComments(readFileSync2(path, "utf8")));
3669
+ } catch {
3670
+ return void 0;
3671
+ }
3672
+ }
3673
+ function configFile(projectRoot, path) {
3674
+ const normalized = posix.normalize(path);
3675
+ for (const candidate of [normalized, `${normalized}.json`]) {
3676
+ const absolute = containedFile(projectRoot, candidate);
3677
+ if (absolute && statSync(absolute).isFile()) return toPosixPath(relative4(projectRoot, absolute));
3678
+ }
3679
+ return void 0;
3680
+ }
3681
+ function referencePath(projectRoot, path) {
3682
+ const absolute = containedFile(projectRoot, path);
3683
+ if (!absolute) return void 0;
3684
+ const file = statSync(absolute).isDirectory() ? join4(absolute, "tsconfig.json") : absolute;
3685
+ const relativeFile = toPosixPath(relative4(projectRoot, file));
3686
+ return containedFile(projectRoot, relativeFile) ? relativeFile : void 0;
3687
+ }
3688
+ function stripJsonComments(text) {
3689
+ return text.replace(/("(?:\\.|[^"\\])*")|\/\*[\s\S]*?\*\/|\/\/[^\n]*/g, (match, string) => string ?? "").replace(/,(\s*[}\]])/g, "$1");
3690
+ }
3691
+ function stripExtension(path) {
3692
+ return path.replace(/\.(tsx|ts|jsx|js)$/, "");
3693
+ }
3694
+
3695
+ // ../install-core/src/config.ts
3696
+ async function loadConfig(cwd, overrides = {}, options = {}) {
3697
+ const { projectRoot, configPath: foundPath } = await locateProject(cwd, options.home);
3698
+ const configPath = foundPath ?? join5(projectRoot, CONFIG_FILE_NAME);
3699
+ if (foundPath && options.rejectEscapingSymlink) assertNoSymlinkEscape(projectRoot, foundPath);
2728
3700
  const defaults = createDefaultConfig(projectRoot, overrides.apiUrl ?? DEFAULT_API_ORIGIN);
2729
3701
  if (!foundPath) {
2730
3702
  return {
@@ -2735,7 +3707,7 @@ async function loadConfig(cwd, overrides = {}) {
2735
3707
  };
2736
3708
  }
2737
3709
  try {
2738
- const parsed = JSON.parse(await readFile(foundPath, "utf8"));
3710
+ const parsed = JSON.parse(await readFile2(foundPath, "utf8"));
2739
3711
  return {
2740
3712
  config: normalizeConfig(
2741
3713
  {
@@ -2757,22 +3729,23 @@ async function loadConfig(cwd, overrides = {}) {
2757
3729
  };
2758
3730
  } catch (error) {
2759
3731
  if (error instanceof SyntaxError) {
2760
- throw new CliError(`${CONFIG_FILE_NAME} contains invalid JSON.`);
3732
+ throw new WensityError("config_invalid", `${CONFIG_FILE_NAME} contains invalid JSON.`);
2761
3733
  }
2762
3734
  if (error instanceof PresetValidationError) {
2763
- throw new CliError(`${CONFIG_FILE_NAME}: ${error.message}`);
3735
+ throw new WensityError("config_invalid", `${CONFIG_FILE_NAME}: ${error.message}`);
2764
3736
  }
2765
3737
  throw error;
2766
3738
  }
2767
3739
  }
2768
3740
  function createDefaultConfig(projectRoot, apiUrl = DEFAULT_API_ORIGIN) {
2769
3741
  const apiOrigin = normalizeUrlOrigin(apiUrl);
3742
+ const sourceRoot = defaultSourceRoot(projectRoot);
2770
3743
  return {
2771
3744
  $schema: joinUrl(apiOrigin, "/schema/wensity.json"),
2772
3745
  version: CONFIG_SCHEMA_VERSION,
2773
3746
  registry: joinUrl(apiOrigin, DEFAULT_REGISTRY_PATH),
2774
- components: DEFAULT_COMPONENTS_DIR,
2775
- utils: DEFAULT_UTILS_PATH,
3747
+ components: posix2.join(sourceRoot, "components/wensity"),
3748
+ utils: posix2.join(sourceRoot, "lib/utils"),
2776
3749
  tailwind: {
2777
3750
  config: detectTailwindConfig(projectRoot),
2778
3751
  css: detectGlobalCss(projectRoot)
@@ -2786,20 +3759,21 @@ function createDefaultConfig(projectRoot, apiUrl = DEFAULT_API_ORIGIN) {
2786
3759
  }
2787
3760
  async function writeConfigFile(path, config, force) {
2788
3761
  if (!force && await fileExists(path)) {
2789
- throw new CliError(`${toPosixPath(relative2(process.cwd(), path))} already exists. Use --force to overwrite it.`);
3762
+ throw new WensityError("file_conflict", `${toPosixPath(relative5(process.cwd(), path))} already exists. Use --force to overwrite it.`);
2790
3763
  }
2791
- await mkdir(dirname2(path), { recursive: true });
3764
+ await mkdir2(dirname3(path), { recursive: true });
2792
3765
  await writeFile(path, `${JSON.stringify(config, null, 2)}
2793
3766
  `, "utf8");
2794
3767
  }
2795
3768
  async function mergeStyleIntoConfigFile(configPath, style) {
2796
- const raw = JSON.parse(await readFile(configPath, "utf8"));
3769
+ const raw = JSON.parse(await readFile2(configPath, "utf8"));
3770
+ const defaults = raw.components && raw.utils ? void 0 : createDefaultConfig(dirname3(configPath));
2797
3771
  const merged = {
2798
3772
  ...raw,
2799
3773
  version: raw.version ?? CONFIG_SCHEMA_VERSION,
2800
3774
  registry: raw.registry ?? "",
2801
- components: raw.components ?? DEFAULT_COMPONENTS_DIR,
2802
- utils: raw.utils ?? DEFAULT_UTILS_PATH,
3775
+ components: raw.components ?? defaults?.components ?? DEFAULT_COMPONENTS_DIR,
3776
+ utils: raw.utils ?? defaults?.utils ?? DEFAULT_UTILS_PATH,
2803
3777
  aliases: {
2804
3778
  components: raw.aliases?.components ?? DEFAULT_COMPONENTS_ALIAS,
2805
3779
  utils: raw.aliases?.utils ?? DEFAULT_UTILS_ALIAS
@@ -2817,7 +3791,7 @@ function resolveConfiguredFilePath(projectRoot, relativePath, extension = "ts")
2817
3791
  return resolveInside(projectRoot, withExtension(relativePath, extension));
2818
3792
  } catch (error) {
2819
3793
  const message = error instanceof Error ? error.message : String(error);
2820
- throw new CliError(message);
3794
+ throw new WensityError("path_unsafe", message);
2821
3795
  }
2822
3796
  }
2823
3797
  async function fileExists(path) {
@@ -2829,16 +3803,52 @@ async function fileExists(path) {
2829
3803
  throw error;
2830
3804
  }
2831
3805
  }
2832
- async function findUp(start, fileName) {
2833
- let current = start;
3806
+ function assertProjectRootCandidate(path, home = homedir()) {
3807
+ const resolved = resolve2(path);
3808
+ if (isFilesystemRoot(resolved)) {
3809
+ throw new WensityError(
3810
+ "project_ambiguous",
3811
+ `${resolved} is a filesystem root, not a project. Run Wensity from your project directory (the one with package.json).`
3812
+ );
3813
+ }
3814
+ if (isHomeDirectory(resolved, home)) {
3815
+ throw new WensityError(
3816
+ "project_ambiguous",
3817
+ "Your home directory is not a project. Run Wensity from your project directory (the one with package.json)."
3818
+ );
3819
+ }
3820
+ }
3821
+ async function locateProject(start, home = homedir()) {
3822
+ assertProjectRootCandidate(start, home);
3823
+ const origin = resolve2(start);
3824
+ let current = origin;
2834
3825
  while (true) {
2835
- const candidate = join2(current, fileName);
2836
- if (await fileExists(candidate)) return candidate;
2837
- const parent = dirname2(current);
2838
- if (parent === current) return void 0;
3826
+ const candidate = join5(current, CONFIG_FILE_NAME);
3827
+ if (await fileExists(candidate)) return { projectRoot: current, configPath: candidate };
3828
+ if (await fileExists(join5(current, "package.json"))) return { projectRoot: current, configPath: void 0 };
3829
+ const parent = dirname3(current);
3830
+ if (isFilesystemRoot(parent) || isHomeDirectory(parent, home)) return { projectRoot: origin, configPath: void 0 };
2839
3831
  current = parent;
2840
3832
  }
2841
3833
  }
3834
+ function isFilesystemRoot(path) {
3835
+ return dirname3(path) === path;
3836
+ }
3837
+ function isHomeDirectory(path, home) {
3838
+ if (!home) return false;
3839
+ if (resolve2(path) === resolve2(home)) return true;
3840
+ try {
3841
+ return realpathSync3(path) === realpathSync3(home);
3842
+ } catch {
3843
+ return false;
3844
+ }
3845
+ }
3846
+ function defaultSourceRoot(projectRoot) {
3847
+ const fromAlias = atAliasRoot(readPathMapping(projectRoot));
3848
+ if (fromAlias !== void 0) return fromAlias;
3849
+ const src = join5(projectRoot, "src");
3850
+ return existsSync2(src) && statSync2(src).isDirectory() ? "src" : "";
3851
+ }
2842
3852
  function normalizeConfig(config, overrides) {
2843
3853
  const apiOrigin = overrides.apiUrl ? normalizeUrlOrigin(overrides.apiUrl) : void 0;
2844
3854
  try {
@@ -2858,14 +3868,14 @@ function normalizeConfig(config, overrides) {
2858
3868
  };
2859
3869
  } catch (error) {
2860
3870
  if (error instanceof PresetValidationError) {
2861
- throw new CliError(`${CONFIG_FILE_NAME}: ${error.message}`);
3871
+ throw new WensityError("config_invalid", `${CONFIG_FILE_NAME}: ${error.message}`);
2862
3872
  }
2863
3873
  throw error;
2864
3874
  }
2865
3875
  }
2866
3876
  function detectTailwindConfig(projectRoot) {
2867
3877
  const candidates = ["tailwind.config.ts", "tailwind.config.js", "tailwind.config.mjs"];
2868
- return candidates.find((candidate) => existsSync(join2(projectRoot, candidate))) ?? "tailwind.config.ts";
3878
+ return candidates.find((candidate) => existsSync2(join5(projectRoot, candidate))) ?? "tailwind.config.ts";
2869
3879
  }
2870
3880
  function detectGlobalCss(projectRoot) {
2871
3881
  const candidates = [
@@ -2875,230 +3885,356 @@ function detectGlobalCss(projectRoot) {
2875
3885
  "src/index.css",
2876
3886
  "styles/globals.css"
2877
3887
  ];
2878
- return candidates.find((candidate) => existsSync(join2(projectRoot, candidate))) ?? "src/app/globals.css";
3888
+ return candidates.find((candidate) => existsSync2(join5(projectRoot, candidate))) ?? "src/app/globals.css";
2879
3889
  }
2880
3890
  function withExtension(path, extension) {
2881
3891
  if (/\.(ts|tsx|js|jsx)$/.test(path)) return path;
2882
3892
  return `${path}.${extension}`;
2883
3893
  }
2884
3894
 
2885
- // src/credentials.ts
2886
- import { mkdir as mkdir2, readFile as readFile2, rename, rm, writeFile as writeFile2, chmod } from "fs/promises";
2887
- import { dirname as dirname3, join as join3 } from "path";
2888
- import { randomUUID } from "crypto";
2889
- import { stdin, stdout } from "process";
2890
- function credentialsPath(env) {
2891
- if (env.WENSITY_CREDENTIALS_FILE) return env.WENSITY_CREDENTIALS_FILE;
2892
- const configHome = env.WENSITY_CONFIG_HOME ?? env.XDG_CONFIG_HOME ?? join3(requiredHome(env), ".config");
2893
- return join3(configHome, "wensity", "credentials.json");
3895
+ // ../install-core/src/package-manager.ts
3896
+ import { accessSync, constants as fsConstants2, existsSync as existsSync3, readFileSync as readFileSync3, statSync as statSync3, writeFileSync } from "fs";
3897
+ import { spawn, spawnSync } from "child_process";
3898
+ import { join as join6, posix as posix3, win32 } from "path";
3899
+ var PACKAGE_MANAGERS = ["npm", "pnpm", "yarn", "bun"];
3900
+ function parsePackageManager(value) {
3901
+ if (!value) return void 0;
3902
+ const normalized = value.trim().toLowerCase();
3903
+ if (isPackageManager(normalized)) return normalized;
3904
+ throw new WensityError(
3905
+ "unsupported_package_manager",
3906
+ `Unsupported package manager "${value}". Use one of: ${PACKAGE_MANAGERS.join(", ")}.`
3907
+ );
2894
3908
  }
2895
- async function readCredentials(env) {
2896
- const path = credentialsPath(env);
2897
- try {
2898
- const content = await readFile2(path, "utf8");
2899
- const parsed = JSON.parse(content);
2900
- if (parsed.version !== 1 || !parsed.token || !parsed.apiBaseUrl) {
2901
- throw new CliError(`Credentials at ${path} are not valid. Run wensity login again.`);
2902
- }
2903
- return parsed;
2904
- } catch (error) {
2905
- if (isNodeError(error) && error.code === "ENOENT") return void 0;
2906
- if (error instanceof SyntaxError) {
2907
- throw new CliError(`Credentials at ${path} are not valid JSON. Run wensity login again.`);
2908
- }
2909
- throw error;
2910
- }
3909
+ function packageManagerFromUserAgent(env = process.env) {
3910
+ const agent = env.npm_config_user_agent;
3911
+ if (!agent) return void 0;
3912
+ const first = agent.trim().split(/\s+/)[0] ?? "";
3913
+ const name = first.split("/")[0]?.toLowerCase();
3914
+ return name && isPackageManager(name) ? name : void 0;
2911
3915
  }
2912
- async function writeCredentials(env, credentials) {
2913
- const path = credentialsPath(env);
2914
- const directory = dirname3(path);
2915
- const temporaryPath = join3(directory, `.credentials.${randomUUID()}.tmp`);
2916
- await mkdir2(directory, { recursive: true, mode: 448 });
2917
- await writeFile2(temporaryPath, `${JSON.stringify(credentials, null, 2)}
2918
- `, {
2919
- encoding: "utf8",
2920
- mode: 384
2921
- });
2922
- await rename(temporaryPath, path);
2923
- await chmod(path, 384);
2924
- return path;
3916
+ function detectPackageManager(projectRoot, env = process.env) {
3917
+ if (existsSync3(join6(projectRoot, "pnpm-lock.yaml"))) return "pnpm";
3918
+ if (existsSync3(join6(projectRoot, "bun.lockb")) || existsSync3(join6(projectRoot, "bun.lock"))) return "bun";
3919
+ if (existsSync3(join6(projectRoot, "yarn.lock"))) return "yarn";
3920
+ if (existsSync3(join6(projectRoot, "package-lock.json"))) return "npm";
3921
+ return packageManagerFromUserAgent(env) ?? "npm";
2925
3922
  }
2926
- async function deleteCredentials(env) {
2927
- const path = credentialsPath(env);
2928
- try {
2929
- await rm(path, { force: false });
2930
- return true;
2931
- } catch (error) {
2932
- if (isNodeError(error) && error.code === "ENOENT") return false;
2933
- throw error;
2934
- }
3923
+ function isPackageManager(value) {
3924
+ return PACKAGE_MANAGERS.includes(value);
2935
3925
  }
2936
- async function readTokenInteractively(prompt = "Wensity API token: ") {
2937
- if (!stdin.isTTY) {
2938
- const chunks = [];
2939
- for await (const chunk of stdin) {
2940
- chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
2941
- }
2942
- return Buffer.concat(chunks).toString("utf8").trim();
3926
+ function missingDependencies(projectRoot, dependencies) {
3927
+ const installed = readInstalledPackageNames(projectRoot);
3928
+ if (!installed) return dependencies;
3929
+ return dependencies.filter((dependency) => !installed.has(dependency));
3930
+ }
3931
+ function readInstalledPackageNames(projectRoot) {
3932
+ const packageJsonPath = join6(projectRoot, "package.json");
3933
+ if (!existsSync3(packageJsonPath)) return void 0;
3934
+ const packageJson2 = JSON.parse(readFileSync3(packageJsonPath, "utf8"));
3935
+ return new Set(
3936
+ Object.keys({
3937
+ ...packageJson2.dependencies,
3938
+ ...packageJson2.devDependencies,
3939
+ ...packageJson2.peerDependencies,
3940
+ ...packageJson2.optionalDependencies
3941
+ })
3942
+ );
3943
+ }
3944
+ var YARN_CONFIG_FILE = ".yarnrc.yml";
3945
+ function prepareProjectPackageManager(projectRoot, options = {}) {
3946
+ if (resolvePackageManager(projectRoot, options) !== "yarn") return [];
3947
+ const configPath = join6(projectRoot, YARN_CONFIG_FILE);
3948
+ if (existsSync3(configPath)) return [];
3949
+ if (!options.dryRun) writeFileSync(configPath, "nodeLinker: node-modules\n", "utf8");
3950
+ return [YARN_CONFIG_FILE];
3951
+ }
3952
+ async function runProjectInstall(projectRoot, options = {}) {
3953
+ const packageJsonPath = join6(projectRoot, "package.json");
3954
+ if (!existsSync3(packageJsonPath)) return;
3955
+ const packageManager = resolvePackageManager(projectRoot, options);
3956
+ await runPackageManager(projectRoot, packageManager, ["install"], options);
3957
+ }
3958
+ function resolvePackageManager(projectRoot, options = {}) {
3959
+ return options.packageManager ?? detectPackageManager(projectRoot, options.env);
3960
+ }
3961
+ async function runInstall(projectRoot, dependencies, options = {}) {
3962
+ if (dependencies.length === 0) return;
3963
+ const packageJsonPath = join6(projectRoot, "package.json");
3964
+ if (!existsSync3(packageJsonPath)) {
3965
+ (options.onWarning ?? writeWarning)(
3966
+ `No package.json found. Install these dependencies manually: ${dependencies.join(" ")}`
3967
+ );
3968
+ return;
2943
3969
  }
2944
- return new Promise((resolveToken, rejectToken) => {
2945
- const input = stdin;
2946
- const previousRawMode = input.isRaw;
2947
- let token = "";
2948
- function cleanup() {
2949
- input.off("data", onData);
2950
- if (typeof previousRawMode === "boolean") {
2951
- input.setRawMode(previousRawMode);
2952
- }
2953
- input.pause();
2954
- }
2955
- function onData(chunk) {
2956
- const value = chunk.toString("utf8");
2957
- if (value === "") {
2958
- cleanup();
2959
- stdout.write("\n");
2960
- rejectToken(new CliError("Login cancelled.", 130));
2961
- return;
2962
- }
2963
- if (value === "\r" || value === "\n" || value === "\r\n") {
2964
- cleanup();
2965
- stdout.write("\n");
2966
- resolveToken(token.trim());
2967
- return;
2968
- }
2969
- if (value === "\x7F") {
2970
- token = token.slice(0, -1);
2971
- return;
2972
- }
2973
- token += value;
2974
- }
2975
- stdout.write(prompt);
2976
- input.setRawMode(true);
2977
- input.resume();
2978
- input.on("data", onData);
2979
- });
3970
+ const packageManager = resolvePackageManager(projectRoot, options);
3971
+ await runPackageManager(projectRoot, packageManager, installArgs(packageManager, dependencies), options);
2980
3972
  }
2981
- function requiredHome(env) {
2982
- if (env.HOME) return env.HOME;
2983
- throw new CliError("HOME is not set, so the CLI cannot locate ~/.config/wensity.");
3973
+ async function runPackageManager(projectRoot, packageManager, args, options) {
3974
+ const outcome = await runPackageManagerCommand(projectRoot, packageManager, args, {
3975
+ output: "inherit",
3976
+ forwardSignals: true,
3977
+ ...options.spawnImpl ? { spawnImpl: options.spawnImpl } : {}
3978
+ });
3979
+ throwForFailure(packageManager, args, outcome);
2984
3980
  }
2985
-
2986
- // src/font-install.ts
2987
- var FONT_CSS_START_MARKER = "/* \u2500\u2500 Wensity Fonts";
2988
- var FONT_CSS_END_MARKER = "/* \u2500\u2500 End Wensity Fonts \u2500\u2500 */";
2989
- function planFontInstall(headingFont, bodyFont) {
2990
- const packages = [];
2991
- const importLines = [];
2992
- const seenPackages = /* @__PURE__ */ new Set();
2993
- const seenImports = /* @__PURE__ */ new Set();
2994
- const pushFont = (meta) => {
2995
- if (!meta) return;
2996
- if (!seenPackages.has(meta.packageName)) {
2997
- seenPackages.add(meta.packageName);
2998
- packages.push(meta.packageName);
2999
- }
3000
- for (const specifier of curatedFontCssImportSpecifiers(meta)) {
3001
- const line = `@import "${specifier}";`;
3002
- if (!seenImports.has(line)) {
3003
- seenImports.add(line);
3004
- importLines.push(line);
3005
- }
3006
- }
3007
- };
3008
- if (headingFont !== "system") {
3009
- pushFont(HEADING_FONT_PACKAGES[headingFont]);
3981
+ function throwForFailure(packageManager, args, outcome) {
3982
+ if (outcome.spawnError) {
3983
+ throw new WensityError("dependency_install_failed", `Could not run ${packageManager}: ${outcome.spawnError}`);
3010
3984
  }
3011
- if (bodyFont !== "system") {
3012
- pushFont(BODY_FONT_PACKAGES[bodyFont]);
3985
+ if (outcome.timedOut) {
3986
+ throw new WensityError(
3987
+ "dependency_install_failed",
3988
+ `${packageManager} ${args.join(" ")} did not finish within ${PACKAGE_MANAGER_TIMEOUT_MS / 1e3} s and was stopped.`
3989
+ );
3013
3990
  }
3014
- packages.sort();
3015
- if (importLines.length === 0) {
3016
- return { packages: [], importLines: [], block: null };
3991
+ if (outcome.exitCode !== 0) {
3992
+ throw new WensityError("dependency_install_failed", `${packageManager} ${args.join(" ")} failed with exit code ${outcome.exitCode ?? 1}.`);
3017
3993
  }
3018
- const block = [
3019
- `${FONT_CSS_START_MARKER} \u2500\u2500 */`,
3020
- ...importLines,
3021
- FONT_CSS_END_MARKER
3022
- ].join("\n");
3023
- return { packages, importLines, block };
3024
3994
  }
3025
- function upsertFontImportBlock(existingCss, block) {
3026
- const start = existingCss.indexOf(FONT_CSS_START_MARKER);
3027
- const end = existingCss.indexOf(FONT_CSS_END_MARKER);
3028
- if (start !== -1 || end !== -1) {
3029
- if (start === -1 || end === -1 || end <= start) {
3030
- throw new CliError(
3031
- "Configured Tailwind CSS has a partial Wensity Fonts block. Remove the broken markers and re-run install."
3032
- );
3033
- }
3034
- const before2 = existingCss.slice(0, start).replace(/\s*$/, "");
3035
- const after2 = existingCss.slice(end + FONT_CSS_END_MARKER.length).replace(/^\s*/, "");
3036
- if (!block) {
3037
- const joined = [before2, after2].filter((part) => part.length > 0).join("\n\n");
3038
- return joined ? `${joined.replace(/\n{3,}/g, "\n\n")}
3039
- ` : "";
3995
+ var PACKAGE_MANAGER_TIMEOUT_MS = 18e4;
3996
+ var PACKAGE_NAME = /^(@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/;
3997
+ var SUBCOMMANDS = /* @__PURE__ */ new Set(["add", "install"]);
3998
+ var WINDOWS_UNSAFE = /["%!^&|<>()\r\n\t\u0000-\u001f\u007f]/;
3999
+ function assertValidPackageName(name) {
4000
+ if (typeof name !== "string" || name.length > 214 || !PACKAGE_NAME.test(name) || /(^|\/|^@)[-._]/.test(name)) {
4001
+ throw new WensityError("dependency_install_failed", `Refusing to install an invalid package name: ${JSON.stringify(String(name).slice(0, 80))}`);
4002
+ }
4003
+ }
4004
+ var WINDOWS_JS_ENTRIES = {
4005
+ npm: ["node_modules/npm/bin/npm-cli.js"],
4006
+ pnpm: ["node_modules/pnpm/bin/pnpm.cjs", "node_modules/pnpm/bin/pnpm.mjs"],
4007
+ yarn: ["node_modules/yarn/bin/yarn.js"],
4008
+ bun: []
4009
+ };
4010
+ function buildPackageManagerInvocation(packageManager, args, options = {}) {
4011
+ if (!PACKAGE_MANAGERS.includes(packageManager)) parsePackageManager(packageManager);
4012
+ const [subcommand, ...names] = args;
4013
+ if (!subcommand || !SUBCOMMANDS.has(subcommand)) {
4014
+ throw new WensityError("dependency_install_failed", `Refusing to run ${packageManager} with an unexpected subcommand.`);
4015
+ }
4016
+ for (const name of names) assertValidPackageName(name);
4017
+ const platform = options.platform ?? process.platform;
4018
+ const env = options.env ?? process.env;
4019
+ const display = [packageManager, ...args].join(" ");
4020
+ const isFile = options.isFile ?? defaultIsFile;
4021
+ if (platform !== "win32") {
4022
+ const isExecutable = options.isExecutable ?? defaultIsExecutable;
4023
+ const dirs = (env.PATH ?? "").split(posix3.delimiter).filter(Boolean);
4024
+ const resolved = dirs.map((dir) => posix3.join(dir, packageManager)).find(isExecutable);
4025
+ if (!resolved) throw notFound(packageManager);
4026
+ return { shape: "posix", command: resolved, args, windowsVerbatimArguments: false, display };
4027
+ }
4028
+ const pathValue = env.PATH ?? env.Path ?? Object.entries(env).find(([key]) => key.toLowerCase() === "path")?.[1] ?? "";
4029
+ const extensions = (env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").map((ext) => ext.toLowerCase()).filter(Boolean);
4030
+ for (const dir of pathValue.split(win32.delimiter).filter(Boolean)) {
4031
+ for (const extension of extensions) {
4032
+ const candidate = win32.join(dir, `${packageManager}${extension}`);
4033
+ if (!isFile(candidate)) continue;
4034
+ if (extension === ".exe" || extension === ".com") {
4035
+ return { shape: "windows-exe", command: candidate, args, windowsVerbatimArguments: false, display };
4036
+ }
4037
+ if (extension !== ".cmd" && extension !== ".bat") continue;
4038
+ const entry = WINDOWS_JS_ENTRIES[packageManager].map((relative9) => win32.join(dir, relative9)).find(isFile);
4039
+ if (entry) {
4040
+ return { shape: "windows-node", command: options.nodePath ?? process.execPath, args: [entry, ...args], windowsVerbatimArguments: false, display };
4041
+ }
4042
+ const cmd = resolveCmdExe(env);
4043
+ return {
4044
+ shape: "windows-cmd",
4045
+ command: cmd,
4046
+ args: ["/d", "/s", "/c", `"${quoteWindowsCommandLine([candidate, ...args])}"`],
4047
+ windowsVerbatimArguments: true,
4048
+ display
4049
+ };
3040
4050
  }
3041
- const normalizedBlock2 = block.trim();
3042
- const prefix2 = before2 ? `${before2}
3043
-
3044
- ` : "";
3045
- const suffix2 = after2 ? `
3046
-
3047
- ${after2}` : "\n";
3048
- return `${prefix2}${normalizedBlock2}${suffix2}`.replace(/\n{3,}/g, "\n\n");
3049
4051
  }
3050
- if (!block) return existingCss;
3051
- const normalizedBlock = block.trim();
3052
- const insertAt = findFontBlockInsertIndex(existingCss);
3053
- const before = existingCss.slice(0, insertAt).replace(/\s*$/, "");
3054
- const after = existingCss.slice(insertAt).replace(/^\s*/, "");
3055
- const prefix = before ? `${before}
3056
-
3057
- ` : "";
3058
- const suffix = after ? `
3059
-
3060
- ${after}` : "\n";
3061
- return `${prefix}${normalizedBlock}${suffix}`.replace(/\n{3,}/g, "\n\n");
4052
+ throw notFound(packageManager);
3062
4053
  }
3063
- function warnUnusedFontPackageMessage(headingFont, bodyFont, installedPackages) {
3064
- const needed = new Set(planFontInstall(headingFont, bodyFont).packages);
3065
- const curated = [
3066
- ...Object.values(HEADING_FONT_PACKAGES).map((entry) => entry.packageName),
3067
- ...Object.values(BODY_FONT_PACKAGES).map((entry) => entry.packageName)
3068
- ];
3069
- const leftover = curated.filter(
3070
- (pkg) => installedPackages.has(pkg) && !needed.has(pkg)
3071
- );
3072
- if (leftover.length === 0) return void 0;
3073
- 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 quoteWindowsCommandLine(values) {
4055
+ return values.map((value) => {
4056
+ if (value.length === 0 || WINDOWS_UNSAFE.test(value)) {
4057
+ throw new WensityError("dependency_install_failed", `Refusing to build a command line from an unsafe value: ${JSON.stringify(value.slice(0, 80))}`);
4058
+ }
4059
+ return /\s/.test(value) ? `"${value}"` : value;
4060
+ }).join(" ");
3074
4061
  }
3075
- function findFontBlockInsertIndex(css) {
3076
- let index = 0;
3077
- const length = css.length;
3078
- while (index < length) {
3079
- while (index < length && /\s/.test(css[index])) index += 1;
3080
- if (index >= length) break;
3081
- if (css.startsWith("/*", index)) {
3082
- const end = css.indexOf("*/", index + 2);
3083
- if (end === -1) break;
3084
- index = end + 2;
3085
- continue;
4062
+ function resolveCmdExe(env) {
4063
+ const fromEnv = env.ComSpec ?? env.COMSPEC;
4064
+ if (fromEnv && win32.isAbsolute(fromEnv) && win32.basename(fromEnv).toLowerCase() === "cmd.exe" && !WINDOWS_UNSAFE.test(fromEnv)) {
4065
+ return fromEnv;
4066
+ }
4067
+ const systemRoot = env.SystemRoot ?? env.SYSTEMROOT ?? "C:\\Windows";
4068
+ if (!win32.isAbsolute(systemRoot) || WINDOWS_UNSAFE.test(systemRoot)) {
4069
+ throw new WensityError("dependency_install_failed", "Could not locate cmd.exe safely.");
4070
+ }
4071
+ return win32.join(systemRoot, "System32", "cmd.exe");
4072
+ }
4073
+ function notFound(packageManager) {
4074
+ return new WensityError("dependency_install_failed", `Could not run ${packageManager}: it was not found on PATH.`);
4075
+ }
4076
+ function defaultIsExecutable(path) {
4077
+ try {
4078
+ accessSync(path, fsConstants2.X_OK);
4079
+ return statSync3(path).isFile();
4080
+ } catch {
4081
+ return false;
4082
+ }
4083
+ }
4084
+ function defaultIsFile(path) {
4085
+ try {
4086
+ return statSync3(path).isFile();
4087
+ } catch {
4088
+ return false;
4089
+ }
4090
+ }
4091
+ var OUTPUT_KEEP = 256 * 1024;
4092
+ var TREE_EXIT_WAIT_MS = 5e3;
4093
+ function runPackageManagerCommand(projectRoot, packageManager, args, options = {}) {
4094
+ const invocation = buildPackageManagerInvocation(packageManager, args, options);
4095
+ const platform = options.platform ?? process.platform;
4096
+ const inherit = options.output === "inherit";
4097
+ const ownGroup = !inherit && platform !== "win32";
4098
+ const timeoutMs = options.timeoutMs ?? (inherit ? void 0 : PACKAGE_MANAGER_TIMEOUT_MS);
4099
+ const started = Date.now();
4100
+ return new Promise((resolve3) => {
4101
+ let stdout2 = "";
4102
+ let stderr = "";
4103
+ let timedOut = false;
4104
+ const child = (options.spawnImpl ?? spawn)(invocation.command, invocation.args, {
4105
+ cwd: projectRoot,
4106
+ env: options.spawnEnv ?? process.env,
4107
+ shell: false,
4108
+ stdio: inherit ? "inherit" : ["ignore", "pipe", "pipe"],
4109
+ windowsHide: true,
4110
+ windowsVerbatimArguments: invocation.windowsVerbatimArguments,
4111
+ detached: ownGroup
4112
+ });
4113
+ const killTree = () => killProcessTree(child.pid, platform, options.env ?? process.env, spawnSync, ownGroup);
4114
+ const signalHandlers = [];
4115
+ if (options.forwardSignals) {
4116
+ for (const signal of ["SIGINT", "SIGTERM"]) {
4117
+ const handler = () => {
4118
+ killTree();
4119
+ removeSignalHandlers();
4120
+ process.kill(process.pid, signal);
4121
+ };
4122
+ signalHandlers.push([signal, handler]);
4123
+ process.once(signal, handler);
4124
+ }
3086
4125
  }
3087
- if (css.startsWith("//", index)) {
3088
- const end = css.indexOf("\n", index);
3089
- if (end === -1) return length;
3090
- index = end + 1;
3091
- continue;
4126
+ const removeSignalHandlers = () => {
4127
+ for (const [signal, handler] of signalHandlers.splice(0)) process.removeListener(signal, handler);
4128
+ };
4129
+ const timer = timeoutMs === void 0 ? void 0 : setTimeout(() => {
4130
+ timedOut = true;
4131
+ killTree();
4132
+ }, timeoutMs);
4133
+ child.stdout?.on("data", (chunk) => {
4134
+ stdout2 = (stdout2 + chunk.toString("utf8")).slice(-OUTPUT_KEEP);
4135
+ });
4136
+ child.stderr?.on("data", (chunk) => {
4137
+ stderr = (stderr + chunk.toString("utf8")).slice(-OUTPUT_KEEP);
4138
+ });
4139
+ let settled = false;
4140
+ const finish = async (exitCode, spawnError) => {
4141
+ if (settled) return;
4142
+ settled = true;
4143
+ clearTimeout(timer);
4144
+ if (ownGroup && child.pid !== void 0 && groupAlive(child.pid)) {
4145
+ killTree();
4146
+ await waitForGroupExit(child.pid);
4147
+ }
4148
+ removeSignalHandlers();
4149
+ child.stdout?.destroy();
4150
+ child.stderr?.destroy();
4151
+ resolve3({ invocation, exitCode, stdout: stdout2, stderr, durationMs: Date.now() - started, timedOut, ...spawnError ? { spawnError } : {} });
4152
+ };
4153
+ child.on("error", (error) => void finish(null, error.message));
4154
+ child.on("exit", (code) => void finish(code));
4155
+ });
4156
+ }
4157
+ function killProcessTree(pid, platform, env, run2 = spawnSync, ownGroup = true) {
4158
+ if (pid === void 0) return;
4159
+ try {
4160
+ if (platform !== "win32" && !ownGroup) {
4161
+ for (const member of [...descendantsOf(pid, run2).reverse(), pid]) {
4162
+ try {
4163
+ process.kill(member, "SIGKILL");
4164
+ } catch {
4165
+ }
4166
+ }
4167
+ return;
3092
4168
  }
3093
- if (css.startsWith("@charset", index) || css.startsWith("@import", index)) {
3094
- const end = css.indexOf(";", index);
3095
- if (end === -1) break;
3096
- index = end + 1;
3097
- continue;
4169
+ if (platform === "win32") {
4170
+ const systemRoot = env.SystemRoot ?? env.SYSTEMROOT ?? "C:\\Windows";
4171
+ run2(win32.join(systemRoot, "System32", "taskkill.exe"), ["/PID", String(pid), "/T", "/F"], { shell: false, stdio: "ignore", windowsHide: true });
4172
+ } else {
4173
+ process.kill(-pid, "SIGKILL");
3098
4174
  }
3099
- break;
4175
+ } catch {
3100
4176
  }
3101
- return index;
4177
+ }
4178
+ function descendantsOf(pid, run2) {
4179
+ const listing = run2("ps", ["-A", "-o", "pid=,ppid="], { shell: false, encoding: "utf8" });
4180
+ const children = /* @__PURE__ */ new Map();
4181
+ for (const line of String(listing.stdout ?? "").split("\n")) {
4182
+ const [child, parent] = line.trim().split(/\s+/).map(Number);
4183
+ if (!child || !parent) continue;
4184
+ children.set(parent, [...children.get(parent) ?? [], child]);
4185
+ }
4186
+ const found = [];
4187
+ const queue = [...children.get(pid) ?? []];
4188
+ while (queue.length > 0) {
4189
+ const next = queue.shift();
4190
+ found.push(next);
4191
+ queue.push(...children.get(next) ?? []);
4192
+ }
4193
+ return found;
4194
+ }
4195
+ function groupAlive(pid) {
4196
+ try {
4197
+ process.kill(-pid, 0);
4198
+ return true;
4199
+ } catch (error) {
4200
+ return error.code === "EPERM";
4201
+ }
4202
+ }
4203
+ async function waitForGroupExit(pid) {
4204
+ const deadline = Date.now() + TREE_EXIT_WAIT_MS;
4205
+ while (groupAlive(pid) && Date.now() < deadline) {
4206
+ await new Promise((resolve3) => setTimeout(resolve3, 20));
4207
+ }
4208
+ }
4209
+ function installArgs(packageManager, dependencies) {
4210
+ switch (packageManager) {
4211
+ case "pnpm":
4212
+ return ["add", ...dependencies];
4213
+ case "yarn":
4214
+ return ["add", ...dependencies];
4215
+ case "bun":
4216
+ return ["add", ...dependencies];
4217
+ case "npm":
4218
+ return ["install", ...dependencies];
4219
+ }
4220
+ }
4221
+ function writeWarning(message) {
4222
+ process.stderr.write(`Warning: ${message}
4223
+ `);
4224
+ }
4225
+
4226
+ // ../install-core/src/project.ts
4227
+ 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.';
4228
+ function importAliasesResolve(projectRoot, config) {
4229
+ const mapping = readPathMapping(projectRoot);
4230
+ if (!mapping) return null;
4231
+ return aliasResolvesTo(mapping, config.aliases.components, config.components) && aliasResolvesTo(mapping, config.aliases.utils, config.utils);
4232
+ }
4233
+ function importAliasMismatchMessage(config) {
4234
+ 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.`;
4235
+ }
4236
+ function importAliasBlockMessage(config) {
4237
+ return `${importAliasMismatchMessage(config)} ${IMPORT_ALIAS_REMEDY}`;
3102
4238
  }
3103
4239
 
3104
4240
  // src/output.ts
@@ -3272,54 +4408,334 @@ function centerLine(line) {
3272
4408
  return `${" ".repeat(padding)}${line}`;
3273
4409
  }
3274
4410
 
3275
- // src/http.ts
3276
- async function requestJson(url, options = {}) {
3277
- const headers = new Headers(options.headers);
3278
- headers.set("accept", "application/json");
3279
- headers.set("user-agent", `${CLI_NAME}/${CLI_VERSION}`);
3280
- if (options.body && !headers.has("content-type")) {
3281
- headers.set("content-type", "application/json");
3282
- }
3283
- if (options.token) {
3284
- headers.set("authorization", `Bearer ${options.token}`);
4411
+ // src/errors.ts
4412
+ var PRICING_URL = "https://ui.wensity.com/pricing";
4413
+ var CliError = class _CliError extends Error {
4414
+ exitCode;
4415
+ /** The install-core error this one renders, when there is one. */
4416
+ wensityError;
4417
+ constructor(message, exitCode = 1, options = {}) {
4418
+ super(message, options.cause ? { cause: options.cause } : void 0);
4419
+ this.name = "CliError";
4420
+ this.exitCode = exitCode;
4421
+ if (options.cause) this.wensityError = options.cause;
4422
+ }
4423
+ /**
4424
+ * install-core throws WensityError wherever this code used to throw CliError,
4425
+ * so `instanceof CliError` keeps meaning "a user-facing CLI failure" for both.
4426
+ * Render one with `toCliError` to get the exit code and CLI wording.
4427
+ */
4428
+ static [Symbol.hasInstance](value) {
4429
+ return isWensityError(value) || Function.prototype[Symbol.hasInstance].call(_CliError, value);
4430
+ }
4431
+ static fromWensityError(error) {
4432
+ return new _CliError(renderWensityErrorMessage(error), 1, { cause: error });
4433
+ }
4434
+ };
4435
+ function renderWensityErrorMessage(error) {
4436
+ if (error.code === "entitlement_required") {
4437
+ return `${error.message}
4438
+ Upgrade here: ${pricingBlue(PRICING_URL)}`;
4439
+ }
4440
+ return error.message;
4441
+ }
4442
+ function toCliError(error) {
4443
+ if (isWensityError(error)) return CliError.fromWensityError(error);
4444
+ return error instanceof CliError ? error : void 0;
4445
+ }
4446
+
4447
+ // src/args.ts
4448
+ function parseArgs(argv) {
4449
+ const positionals = [];
4450
+ const options = /* @__PURE__ */ new Map();
4451
+ for (let index = 0; index < argv.length; index += 1) {
4452
+ const token = argv[index];
4453
+ if (!token.startsWith("--")) {
4454
+ positionals.push(token);
4455
+ continue;
4456
+ }
4457
+ if (token === "--") {
4458
+ positionals.push(...argv.slice(index + 1));
4459
+ break;
4460
+ }
4461
+ const inlineValueIndex = token.indexOf("=");
4462
+ const rawName = inlineValueIndex > -1 ? token.slice(2, inlineValueIndex) : token.slice(2);
4463
+ const name = normalizeOptionName(rawName);
4464
+ if (!name) {
4465
+ throw new CliError(`Invalid option: ${token}`);
4466
+ }
4467
+ if (inlineValueIndex > -1) {
4468
+ options.set(name, token.slice(inlineValueIndex + 1));
4469
+ continue;
4470
+ }
4471
+ const nextToken = argv[index + 1];
4472
+ if (nextToken && !nextToken.startsWith("--") && expectsValue(name)) {
4473
+ options.set(name, nextToken);
4474
+ index += 1;
4475
+ continue;
4476
+ }
4477
+ options.set(name, true);
4478
+ }
4479
+ return { positionals, options };
4480
+ }
4481
+ function getStringOption(args, name) {
4482
+ const value = args.options.get(name);
4483
+ if (value === void 0) return void 0;
4484
+ if (typeof value === "boolean") {
4485
+ throw new CliError(`--${name} expects a value.`);
4486
+ }
4487
+ return value;
4488
+ }
4489
+ function hasFlag(args, name) {
4490
+ return args.options.get(name) === true;
4491
+ }
4492
+ function expectsValue(name) {
4493
+ return [
4494
+ "api-url",
4495
+ "registry",
4496
+ "token",
4497
+ "type",
4498
+ "preset",
4499
+ "preset-file",
4500
+ "template",
4501
+ "package-manager"
4502
+ ].includes(name);
4503
+ }
4504
+ function normalizeOptionName(name) {
4505
+ if (name === "dry") return "dry-run";
4506
+ return name;
4507
+ }
4508
+
4509
+ // src/credentials.ts
4510
+ import { stdin, stdout } from "process";
4511
+
4512
+ // ../install-core/src/credentials.ts
4513
+ import { mkdir as mkdir3, readFile as readFile4, rename as rename2, rm as rm2, writeFile as writeFile2, chmod as chmod2 } from "fs/promises";
4514
+ import { dirname as dirname4, join as join8 } from "path";
4515
+ import { randomUUID } from "crypto";
4516
+ function credentialsPath(env) {
4517
+ if (env.WENSITY_CREDENTIALS_FILE) return env.WENSITY_CREDENTIALS_FILE;
4518
+ const configHome = env.WENSITY_CONFIG_HOME ?? env.XDG_CONFIG_HOME ?? join8(requiredHome(env), ".config");
4519
+ return join8(configHome, "wensity", "credentials.json");
4520
+ }
4521
+ async function readCredentials(env) {
4522
+ const path = credentialsPath(env);
4523
+ try {
4524
+ const content = await readFile4(path, "utf8");
4525
+ const parsed = JSON.parse(content);
4526
+ if (parsed.version !== 1 || !parsed.token || !parsed.apiBaseUrl) {
4527
+ throw new WensityError("auth_invalid", `Credentials at ${path} are not valid. Run wensity login again.`);
4528
+ }
4529
+ return parsed;
4530
+ } catch (error) {
4531
+ if (isNodeError(error) && error.code === "ENOENT") return void 0;
4532
+ if (error instanceof SyntaxError) {
4533
+ throw new WensityError("auth_invalid", `Credentials at ${path} are not valid JSON. Run wensity login again.`);
4534
+ }
4535
+ throw error;
4536
+ }
4537
+ }
4538
+ async function writeCredentials(env, credentials) {
4539
+ const path = credentialsPath(env);
4540
+ const directory = dirname4(path);
4541
+ const temporaryPath = join8(directory, `.credentials.${randomUUID()}.tmp`);
4542
+ await mkdir3(directory, { recursive: true, mode: 448 });
4543
+ await writeFile2(temporaryPath, `${JSON.stringify(credentials, null, 2)}
4544
+ `, {
4545
+ encoding: "utf8",
4546
+ mode: 384
4547
+ });
4548
+ await rename2(temporaryPath, path);
4549
+ await chmod2(path, 384);
4550
+ return path;
4551
+ }
4552
+ async function deleteCredentials(env) {
4553
+ const path = credentialsPath(env);
4554
+ try {
4555
+ await rm2(path, { force: false });
4556
+ return true;
4557
+ } catch (error) {
4558
+ if (isNodeError(error) && error.code === "ENOENT") return false;
4559
+ throw error;
4560
+ }
4561
+ }
4562
+ function requiredHome(env) {
4563
+ if (env.HOME) return env.HOME;
4564
+ throw new WensityError("config_missing", "HOME is not set, so the CLI cannot locate ~/.config/wensity.");
4565
+ }
4566
+
4567
+ // src/credentials.ts
4568
+ async function readTokenInteractively(prompt = "Wensity API token: ") {
4569
+ if (!stdin.isTTY) {
4570
+ const chunks = [];
4571
+ for await (const chunk of stdin) {
4572
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
4573
+ }
4574
+ return Buffer.concat(chunks).toString("utf8").trim();
4575
+ }
4576
+ return new Promise((resolveToken, rejectToken) => {
4577
+ const input = stdin;
4578
+ const previousRawMode = input.isRaw;
4579
+ let token = "";
4580
+ function cleanup() {
4581
+ input.off("data", onData);
4582
+ if (typeof previousRawMode === "boolean") {
4583
+ input.setRawMode(previousRawMode);
4584
+ }
4585
+ input.pause();
4586
+ }
4587
+ function onData(chunk) {
4588
+ const value = chunk.toString("utf8");
4589
+ if (value === "") {
4590
+ cleanup();
4591
+ stdout.write("\n");
4592
+ rejectToken(new CliError("Login cancelled.", 130));
4593
+ return;
4594
+ }
4595
+ if (value === "\r" || value === "\n" || value === "\r\n") {
4596
+ cleanup();
4597
+ stdout.write("\n");
4598
+ resolveToken(token.trim());
4599
+ return;
4600
+ }
4601
+ if (value === "\x7F") {
4602
+ token = token.slice(0, -1);
4603
+ return;
4604
+ }
4605
+ token += value;
4606
+ }
4607
+ stdout.write(prompt);
4608
+ input.setRawMode(true);
4609
+ input.resume();
4610
+ input.on("data", onData);
4611
+ });
4612
+ }
4613
+
4614
+ // ../install-core/src/font-install.ts
4615
+ var FONT_CSS_START_MARKER = "/* \u2500\u2500 Wensity Fonts";
4616
+ var FONT_CSS_END_MARKER = "/* \u2500\u2500 End Wensity Fonts \u2500\u2500 */";
4617
+ function planFontInstall(headingFont, bodyFont) {
4618
+ const packages = [];
4619
+ const importLines = [];
4620
+ const seenPackages = /* @__PURE__ */ new Set();
4621
+ const seenImports = /* @__PURE__ */ new Set();
4622
+ const pushFont = (meta) => {
4623
+ if (!meta) return;
4624
+ if (!seenPackages.has(meta.packageName)) {
4625
+ seenPackages.add(meta.packageName);
4626
+ packages.push(meta.packageName);
4627
+ }
4628
+ for (const specifier of curatedFontCssImportSpecifiers(meta)) {
4629
+ const line = `@import "${specifier}";`;
4630
+ if (!seenImports.has(line)) {
4631
+ seenImports.add(line);
4632
+ importLines.push(line);
4633
+ }
4634
+ }
4635
+ };
4636
+ if (headingFont !== "system") {
4637
+ pushFont(HEADING_FONT_PACKAGES[headingFont]);
3285
4638
  }
3286
- let response;
3287
- try {
3288
- response = await fetch(url, { ...options, headers });
3289
- } catch (error) {
3290
- const message = error instanceof Error ? error.message : "Unknown network error";
3291
- throw new CliError(`Could not reach ${url}: ${message}`);
4639
+ if (bodyFont !== "system") {
4640
+ pushFont(BODY_FONT_PACKAGES[bodyFont]);
3292
4641
  }
3293
- const text = await response.text();
3294
- const payload = parseJson(text);
3295
- if (!response.ok) {
3296
- const message = formatErrorMessage(payload) ?? response.statusText;
3297
- throw new CliError(`${response.status} ${message}`);
4642
+ packages.sort();
4643
+ if (importLines.length === 0) {
4644
+ return { packages: [], importLines: [], block: null };
3298
4645
  }
3299
- return payload;
4646
+ const block = [
4647
+ `${FONT_CSS_START_MARKER} \u2500\u2500 */`,
4648
+ ...importLines,
4649
+ FONT_CSS_END_MARKER
4650
+ ].join("\n");
4651
+ return { packages, importLines, block };
3300
4652
  }
3301
- function parseJson(text) {
3302
- if (!text.trim()) return null;
3303
- try {
3304
- return JSON.parse(text);
3305
- } catch {
3306
- throw new CliError("Registry returned invalid JSON.");
4653
+ function upsertFontImportBlock(existingCss, block) {
4654
+ const start = existingCss.indexOf(FONT_CSS_START_MARKER);
4655
+ const end = existingCss.indexOf(FONT_CSS_END_MARKER);
4656
+ if (start !== -1 || end !== -1) {
4657
+ if (start === -1 || end === -1 || end <= start) {
4658
+ throw new WensityError(
4659
+ "config_invalid",
4660
+ "Configured Tailwind CSS has a partial Wensity Fonts block. Remove the broken markers and re-run install."
4661
+ );
4662
+ }
4663
+ const before2 = existingCss.slice(0, start).replace(/\s*$/, "");
4664
+ const after2 = existingCss.slice(end + FONT_CSS_END_MARKER.length).replace(/^\s*/, "");
4665
+ if (!block) {
4666
+ const joined = [before2, after2].filter((part) => part.length > 0).join("\n\n");
4667
+ return joined ? `${joined.replace(/\n{3,}/g, "\n\n")}
4668
+ ` : "";
4669
+ }
4670
+ const normalizedBlock2 = block.trim();
4671
+ const prefix2 = before2 ? `${before2}
4672
+
4673
+ ` : "";
4674
+ const suffix2 = after2 ? `
4675
+
4676
+ ${after2}` : "\n";
4677
+ return `${prefix2}${normalizedBlock2}${suffix2}`.replace(/\n{3,}/g, "\n\n");
3307
4678
  }
4679
+ if (!block) return existingCss;
4680
+ const normalizedBlock = block.trim();
4681
+ const insertAt = findFontBlockInsertIndex(existingCss);
4682
+ const before = existingCss.slice(0, insertAt).replace(/\s*$/, "");
4683
+ const after = existingCss.slice(insertAt).replace(/^\s*/, "");
4684
+ const prefix = before ? `${before}
4685
+
4686
+ ` : "";
4687
+ const suffix = after ? `
4688
+
4689
+ ${after}` : "\n";
4690
+ return `${prefix}${normalizedBlock}${suffix}`.replace(/\n{3,}/g, "\n\n");
4691
+ }
4692
+ function warnUnusedFontPackageMessage(headingFont, bodyFont, installedPackages) {
4693
+ const needed = new Set(planFontInstall(headingFont, bodyFont).packages);
4694
+ const curated = [
4695
+ ...Object.values(HEADING_FONT_PACKAGES).map((entry) => entry.packageName),
4696
+ ...Object.values(BODY_FONT_PACKAGES).map((entry) => entry.packageName)
4697
+ ];
4698
+ const leftover = curated.filter(
4699
+ (pkg) => installedPackages.has(pkg) && !needed.has(pkg)
4700
+ );
4701
+ if (leftover.length === 0) return void 0;
4702
+ 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).`;
3308
4703
  }
3309
- function formatErrorMessage(payload) {
3310
- if (!payload || typeof payload !== "object") return void 0;
3311
- const record = payload;
3312
- const code = typeof record.error === "string" ? record.error : void 0;
3313
- const message = typeof record.message === "string" ? record.message : code;
3314
- if (code === "pro_license_required") {
3315
- const base = message ?? "This component requires Wensity Pro.";
3316
- return `${base}
3317
- Upgrade here: ${pricingBlue("https://ui.wensity.com/pricing")}`;
4704
+ function findFontBlockInsertIndex(css) {
4705
+ let index = 0;
4706
+ const length = css.length;
4707
+ while (index < length) {
4708
+ while (index < length && /\s/.test(css[index])) index += 1;
4709
+ if (index >= length) break;
4710
+ if (css.startsWith("/*", index)) {
4711
+ const end = css.indexOf("*/", index + 2);
4712
+ if (end === -1) break;
4713
+ index = end + 2;
4714
+ continue;
4715
+ }
4716
+ if (css.startsWith("//", index)) {
4717
+ const end = css.indexOf("\n", index);
4718
+ if (end === -1) return length;
4719
+ index = end + 1;
4720
+ continue;
4721
+ }
4722
+ if (css.startsWith("@charset", index) || css.startsWith("@import", index)) {
4723
+ const end = css.indexOf(";", index);
4724
+ if (end === -1) break;
4725
+ index = end + 1;
4726
+ continue;
4727
+ }
4728
+ break;
3318
4729
  }
3319
- return message;
4730
+ return index;
4731
+ }
4732
+
4733
+ // src/http.ts
4734
+ function requestJson2(url, options = {}) {
4735
+ return requestJson(url, { userAgent: `${CLI_NAME}/${CLI_VERSION}`, ...options });
3320
4736
  }
3321
4737
 
3322
- // src/icon-transform.ts
4738
+ // ../install-core/src/icon-transform.ts
3323
4739
  var UI_PRIMITIVES_CATEGORY = "UI Primitives";
3324
4740
  var TABLER_ICONS_PACKAGE = "@tabler/icons-react";
3325
4741
  var LUCIDE_ICONS_PACKAGE = "lucide-react";
@@ -3363,7 +4779,7 @@ function createLucideAdapter() {
3363
4779
  return resolveLucideExport(tablerName);
3364
4780
  } catch (error) {
3365
4781
  const message = error instanceof Error ? error.message : String(error);
3366
- throw new CliError(`${context}: ${message}`);
4782
+ throw new WensityError("source_integrity_failed", `${context}: ${message}`);
3367
4783
  }
3368
4784
  },
3369
4785
  strokePropTo: "strokeWidth"
@@ -3477,13 +4893,15 @@ function rewriteTablerImportsToTarget(content, adapter, context) {
3477
4893
  for (const spec of specs) {
3478
4894
  if (spec.isTypeOnly || typePrefix.includes("type")) {
3479
4895
  if (spec.imported === "Icon" || isIconTransformExclusion(spec.imported)) {
3480
- throw new CliError(
4896
+ throw new WensityError(
4897
+ "source_integrity_failed",
3481
4898
  `${context}: unexpected type-only icon import "${spec.imported}" from ${TABLER_ICONS_PACKAGE}.`
3482
4899
  );
3483
4900
  }
3484
4901
  }
3485
4902
  if (isIconTransformExclusion(spec.imported)) {
3486
- throw new CliError(
4903
+ throw new WensityError(
4904
+ "source_integrity_failed",
3487
4905
  `${context}: "${spec.imported}" is excluded from the icon map (Phase 0 internal geometry). It must not appear as a library import.`
3488
4906
  );
3489
4907
  }
@@ -3506,12 +4924,14 @@ function rewriteTablerImportsToTarget(content, adapter, context) {
3506
4924
  }
3507
4925
  );
3508
4926
  if (hasPackageImport(next, TABLER_ICONS_PACKAGE)) {
3509
- throw new CliError(
4927
+ throw new WensityError(
4928
+ "source_integrity_failed",
3510
4929
  `${context}: failed to rewrite all ${TABLER_ICONS_PACKAGE} imports for ${adapter.id}.`
3511
4930
  );
3512
4931
  }
3513
4932
  if (/(?:import|export)\s+\*\s+as\s+\w+\s+from\s+["']@tabler\/icons-react["']/.test(content)) {
3514
- throw new CliError(
4933
+ throw new WensityError(
4934
+ "source_integrity_failed",
3515
4935
  `${context}: namespace imports from ${TABLER_ICONS_PACKAGE} are not supported. Use named imports.`
3516
4936
  );
3517
4937
  }
@@ -3523,7 +4943,8 @@ function assertNoUnmappedTablerImports(content, context) {
3523
4943
  const specs = parseNamedImportClause(match[1] ?? "");
3524
4944
  for (const spec of specs) {
3525
4945
  if (isIconTransformExclusion(spec.imported)) {
3526
- throw new CliError(
4946
+ throw new WensityError(
4947
+ "source_integrity_failed",
3527
4948
  `${context}: "${spec.imported}" must not appear as a library import (Phase 0 exclusion).`
3528
4949
  );
3529
4950
  }
@@ -3576,108 +4997,12 @@ function rewriteStrokeProps(content, localBindings) {
3576
4997
  }
3577
4998
 
3578
4999
  // src/package-manager.ts
3579
- import { existsSync as existsSync2, readFileSync, writeFileSync } from "fs";
3580
- import { spawnSync } from "child_process";
3581
- import { join as join4 } from "path";
3582
- var PACKAGE_MANAGERS = ["npm", "pnpm", "yarn", "bun"];
3583
- function parsePackageManager(value) {
3584
- if (!value) return void 0;
3585
- const normalized = value.trim().toLowerCase();
3586
- if (isPackageManager(normalized)) return normalized;
3587
- throw new CliError(
3588
- `Unsupported package manager "${value}". Use one of: ${PACKAGE_MANAGERS.join(", ")}.`
3589
- );
3590
- }
3591
- function packageManagerFromUserAgent(env = process.env) {
3592
- const agent = env.npm_config_user_agent;
3593
- if (!agent) return void 0;
3594
- const first = agent.trim().split(/\s+/)[0] ?? "";
3595
- const name = first.split("/")[0]?.toLowerCase();
3596
- return name && isPackageManager(name) ? name : void 0;
3597
- }
3598
- function detectPackageManager(projectRoot, env = process.env) {
3599
- if (existsSync2(join4(projectRoot, "pnpm-lock.yaml"))) return "pnpm";
3600
- if (existsSync2(join4(projectRoot, "bun.lockb")) || existsSync2(join4(projectRoot, "bun.lock"))) return "bun";
3601
- if (existsSync2(join4(projectRoot, "yarn.lock"))) return "yarn";
3602
- if (existsSync2(join4(projectRoot, "package-lock.json"))) return "npm";
3603
- return packageManagerFromUserAgent(env) ?? "npm";
3604
- }
3605
- function isPackageManager(value) {
3606
- return PACKAGE_MANAGERS.includes(value);
3607
- }
3608
- function missingDependencies(projectRoot, dependencies) {
3609
- const installed = readInstalledPackageNames(projectRoot);
3610
- if (!installed) return dependencies;
3611
- return dependencies.filter((dependency) => !installed.has(dependency));
3612
- }
3613
- function readInstalledPackageNames(projectRoot) {
3614
- const packageJsonPath = join4(projectRoot, "package.json");
3615
- if (!existsSync2(packageJsonPath)) return void 0;
3616
- const packageJson2 = JSON.parse(readFileSync(packageJsonPath, "utf8"));
3617
- return new Set(
3618
- Object.keys({
3619
- ...packageJson2.dependencies,
3620
- ...packageJson2.devDependencies,
3621
- ...packageJson2.peerDependencies,
3622
- ...packageJson2.optionalDependencies
3623
- })
3624
- );
3625
- }
3626
- var YARN_CONFIG_FILE = ".yarnrc.yml";
3627
- function prepareProjectPackageManager(projectRoot, options = {}) {
3628
- if (resolvePackageManager(projectRoot, options) !== "yarn") return [];
3629
- const configPath = join4(projectRoot, YARN_CONFIG_FILE);
3630
- if (existsSync2(configPath)) return [];
3631
- if (!options.dryRun) writeFileSync(configPath, "nodeLinker: node-modules\n", "utf8");
3632
- return [YARN_CONFIG_FILE];
3633
- }
3634
- function runProjectInstall(projectRoot, options = {}) {
3635
- const packageJsonPath = join4(projectRoot, "package.json");
3636
- if (!existsSync2(packageJsonPath)) return;
3637
- const packageManager = resolvePackageManager(projectRoot, options);
3638
- runPackageManager(projectRoot, packageManager, ["install"]);
3639
- }
3640
- function resolvePackageManager(projectRoot, options = {}) {
3641
- return options.packageManager ?? detectPackageManager(projectRoot, options.env);
3642
- }
3643
- function runInstall(projectRoot, dependencies, options = {}) {
3644
- if (dependencies.length === 0) return;
3645
- const packageJsonPath = join4(projectRoot, "package.json");
3646
- if (!existsSync2(packageJsonPath)) {
3647
- warn(`No package.json found. Install these dependencies manually: ${dependencies.join(" ")}`);
3648
- return;
3649
- }
3650
- const packageManager = resolvePackageManager(projectRoot, options);
3651
- runPackageManager(projectRoot, packageManager, installArgs(packageManager, dependencies));
3652
- }
3653
- function runPackageManager(projectRoot, packageManager, args) {
3654
- const result = spawnSync(packageManager, args, {
3655
- cwd: projectRoot,
3656
- stdio: "inherit",
3657
- shell: process.platform === "win32"
3658
- });
3659
- if (result.error) {
3660
- throw new CliError(`Could not run ${packageManager}: ${result.error.message}`);
3661
- }
3662
- if (result.status !== 0) {
3663
- throw new CliError(`${packageManager} ${args.join(" ")} failed with exit code ${result.status ?? 1}.`);
3664
- }
3665
- }
3666
- function installArgs(packageManager, dependencies) {
3667
- switch (packageManager) {
3668
- case "pnpm":
3669
- return ["add", ...dependencies];
3670
- case "yarn":
3671
- return ["add", ...dependencies];
3672
- case "bun":
3673
- return ["add", ...dependencies];
3674
- case "npm":
3675
- return ["install", ...dependencies];
3676
- }
5000
+ function runInstall2(projectRoot, dependencies, options = {}) {
5001
+ return runInstall(projectRoot, dependencies, { onWarning: warn, ...options });
3677
5002
  }
3678
5003
 
3679
5004
  // src/preset.ts
3680
- import { readFile as readFile3 } from "fs/promises";
5005
+ import { readFile as readFile5 } from "fs/promises";
3681
5006
  async function resolvePresetStyle(input) {
3682
5007
  if (input.presetCode && input.presetFile) {
3683
5008
  throw new CliError("Use either --preset or --preset-file, not both.");
@@ -3685,7 +5010,7 @@ async function resolvePresetStyle(input) {
3685
5010
  let code = input.presetCode;
3686
5011
  if (input.presetFile) {
3687
5012
  try {
3688
- const raw = await readFile3(input.presetFile, "utf8");
5013
+ const raw = await readFile5(input.presetFile, "utf8");
3689
5014
  const trimmed = raw.trim();
3690
5015
  if (trimmed.startsWith("{")) {
3691
5016
  const parsed = JSON.parse(trimmed);
@@ -3885,18 +5210,18 @@ function formatTokenPanel(summary) {
3885
5210
  }
3886
5211
 
3887
5212
  // src/preset-init-writes.ts
3888
- import { mkdir as mkdir3, writeFile as writeFile3 } from "fs/promises";
3889
- import { dirname as dirname4 } from "path";
5213
+ import { mkdir as mkdir4, writeFile as writeFile3 } from "fs/promises";
5214
+ import { dirname as dirname5 } from "path";
3890
5215
  async function commitPresetInitWrites(input) {
3891
5216
  let cssWritten = false;
3892
5217
  try {
3893
5218
  if (input.cssChanged) {
3894
- await mkdir3(dirname4(input.cssAbsolute), { recursive: true });
5219
+ await mkdir4(dirname5(input.cssAbsolute), { recursive: true });
3895
5220
  await writeFile3(input.cssAbsolute, input.nextCss, "utf8");
3896
5221
  cssWritten = true;
3897
5222
  }
3898
5223
  if (input.writeUtility) {
3899
- await mkdir3(dirname4(input.utilityAbsolutePath), { recursive: true });
5224
+ await mkdir4(dirname5(input.utilityAbsolutePath), { recursive: true });
3900
5225
  await writeFile3(input.utilityAbsolutePath, input.utilityContent, "utf8");
3901
5226
  }
3902
5227
  await input.commitConfig();
@@ -3912,8 +5237,8 @@ async function commitPresetInitWrites(input) {
3912
5237
  }
3913
5238
 
3914
5239
  // src/project-templates.ts
3915
- import { mkdir as mkdir4, readFile as readFile4, writeFile as writeFile4 } from "fs/promises";
3916
- import { dirname as dirname5, join as join5 } from "path";
5240
+ import { mkdir as mkdir5, readFile as readFile6, writeFile as writeFile4 } from "fs/promises";
5241
+ import { dirname as dirname6, join as join9 } from "path";
3917
5242
  function templateCss() {
3918
5243
  return `@import "tailwindcss";
3919
5244
 
@@ -3937,6 +5262,8 @@ function getProjectTemplateDefinition(id) {
3937
5262
  function applyTemplateToConfig(config, template) {
3938
5263
  return {
3939
5264
  ...config,
5265
+ components: "src/components/wensity",
5266
+ utils: "src/lib/utils",
3940
5267
  tailwind: {
3941
5268
  ...config.tailwind,
3942
5269
  css: template.cssPath
@@ -3948,7 +5275,7 @@ async function scaffoldProjectTemplate(projectRoot, template, options) {
3948
5275
  if (!options.force) {
3949
5276
  const conflicts = [];
3950
5277
  for (const file of template.files) {
3951
- if (await fileExists(join5(projectRoot, file.path))) {
5278
+ if (await fileExists(join9(projectRoot, file.path))) {
3952
5279
  conflicts.push(file.path);
3953
5280
  }
3954
5281
  }
@@ -3965,10 +5292,10 @@ async function scaffoldProjectTemplate(projectRoot, template, options) {
3965
5292
  return planned;
3966
5293
  }
3967
5294
  async function writeTemplateFile(projectRoot, file) {
3968
- const absolute = join5(projectRoot, file.path);
3969
- await mkdir4(dirname5(absolute), { recursive: true });
5295
+ const absolute = join9(projectRoot, file.path);
5296
+ await mkdir5(dirname6(absolute), { recursive: true });
3970
5297
  if (file.path === "package.json" && await fileExists(absolute)) {
3971
- const existing = JSON.parse(await readFile4(absolute, "utf8"));
5298
+ const existing = JSON.parse(await readFile6(absolute, "utf8"));
3972
5299
  const next = JSON.parse(file.content);
3973
5300
  await writeFile4(
3974
5301
  absolute,
@@ -4291,7 +5618,7 @@ function formatTemplateFiles(files) {
4291
5618
  return files.map(toPosixPath);
4292
5619
  }
4293
5620
 
4294
- // src/style-css.ts
5621
+ // ../install-core/src/style-css.ts
4295
5622
  var ROOT_SELECTOR = ":root";
4296
5623
  var DARK_SELECTOR = ".dark";
4297
5624
  var SHARED_TOKEN_KEYS = ["radius", "fontSans", "fontDisplay", "fontMono"];
@@ -4375,11 +5702,11 @@ function escapeRegExp(value) {
4375
5702
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4376
5703
  }
4377
5704
 
4378
- // src/component-install.ts
4379
- import { mkdir as mkdir5, readFile as readFile5, writeFile as writeFile5 } from "fs/promises";
4380
- import { dirname as dirname6, join as join6, relative as relative3 } from "path";
5705
+ // ../install-core/src/component-install.ts
5706
+ import { mkdir as mkdir6, readFile as readFile7, writeFile as writeFile5 } from "fs/promises";
5707
+ import { dirname as dirname7, join as join10, relative as relative7 } from "path";
4381
5708
 
4382
- // src/templates.ts
5709
+ // ../install-core/src/templates.ts
4383
5710
  function cnUtilityTemplate() {
4384
5711
  return `import { clsx, type ClassValue } from "clsx";
4385
5712
  import { twMerge } from "tailwind-merge";
@@ -4532,7 +5859,13 @@ export function WensitySecondaryButton({
4532
5859
  `;
4533
5860
  }
4534
5861
 
4535
- // src/component-install.ts
5862
+ // ../install-core/src/component-install.ts
5863
+ function safeTarget(projectRoot, relativePath) {
5864
+ assertTargetPathLength(relativePath);
5865
+ const absolutePath = resolveInside(projectRoot, relativePath);
5866
+ assertNoSymlinkEscape(projectRoot, absolutePath);
5867
+ return absolutePath;
5868
+ }
4536
5869
  var ALWAYS_PRESENT_PACKAGES = /* @__PURE__ */ new Set(["react", "react-dom", "next"]);
4537
5870
  var OVERLAY_HELPER_PATH = "components/wensity/overlay.tsx";
4538
5871
  var PRIMITIVE_CSS_START_MARKER = "/* \u2500\u2500 Wensity Primitive Tokens";
@@ -4554,7 +5887,7 @@ async function prepareInstall(projectRoot, config, payload, options = {}) {
4554
5887
  for (const [index, payloadFile] of payload.files.entries()) {
4555
5888
  const payloadFileKind = getPayloadFileKind(payloadFile);
4556
5889
  const relativeOutputPath = getPayloadOutputPath(payloadFile, fallbackFileName, config);
4557
- const absoluteOutputPath = resolveInside(projectRoot, relativeOutputPath);
5890
+ const absoluteOutputPath = safeTarget(projectRoot, relativeOutputPath);
4558
5891
  if (!payloadHelperNeeds) mergeHelperNeeds(inferredHelperNeeds, inferHelperNeeds(payloadFile.content));
4559
5892
  const transformed = transformComponentSource(payloadFile.content, config, {
4560
5893
  isPrimitiveCategory,
@@ -4577,9 +5910,9 @@ async function prepareInstall(projectRoot, config, payload, options = {}) {
4577
5910
  const helperNeeds = payloadHelperNeeds ?? inferredHelperNeeds;
4578
5911
  if (!hasGraphHelperFiles && helperNeeds.utils) {
4579
5912
  const utilityPath = withSourceExtension(config.utils, "ts");
4580
- if (!await fileExists(resolveInside(projectRoot, utilityPath))) {
5913
+ if (!await fileExists(safeTarget(projectRoot, utilityPath))) {
4581
5914
  files.push({
4582
- absolutePath: resolveInside(projectRoot, utilityPath),
5915
+ absolutePath: safeTarget(projectRoot, utilityPath),
4583
5916
  relativePath: utilityPath,
4584
5917
  content: cnUtilityTemplate(),
4585
5918
  kind: "helper"
@@ -4587,11 +5920,11 @@ async function prepareInstall(projectRoot, config, payload, options = {}) {
4587
5920
  }
4588
5921
  }
4589
5922
  if (!hasGraphHelperFiles && helperNeeds.primitives) {
4590
- const primitivesPath = toPosixPath(join6(config.components, "primitives.tsx"));
4591
- if (!await fileExists(resolveInside(projectRoot, primitivesPath))) {
5923
+ const primitivesPath = toPosixPath(join10(config.components, "primitives.tsx"));
5924
+ if (!await fileExists(safeTarget(projectRoot, primitivesPath))) {
4592
5925
  const primitives = primitivesTemplate(config.aliases.utils);
4593
5926
  files.push({
4594
- absolutePath: resolveInside(projectRoot, primitivesPath),
5927
+ absolutePath: safeTarget(projectRoot, primitivesPath),
4595
5928
  relativePath: primitivesPath,
4596
5929
  content: primitives,
4597
5930
  kind: "helper"
@@ -4627,8 +5960,8 @@ async function planPrimitiveCssFile(projectRoot, config, payload) {
4627
5960
  primitiveCss = applyStyleToPrimitiveCss(primitiveCss, style).trim();
4628
5961
  const cssRelative = config.tailwind?.css;
4629
5962
  if (!cssRelative) return void 0;
4630
- const absolutePath = resolveInside(projectRoot, cssRelative);
4631
- const existing = await fileExists(absolutePath) ? await readFile5(absolutePath, "utf8") : "";
5963
+ const absolutePath = safeTarget(projectRoot, cssRelative);
5964
+ const existing = await fileExists(absolutePath) ? await readFile7(absolutePath, "utf8") : "";
4632
5965
  const withBase = upsertBaseCssBlock(existing, buildBaseCssBlock(baseColorFromStyle(style)));
4633
5966
  const withTokens = upsertPrimitiveCssBlock(withBase, primitiveCss);
4634
5967
  const fonts = curatedFontsFromStyle(style);
@@ -4667,7 +6000,8 @@ function upsertDelimitedBlock(existingCss, block, options) {
4667
6000
  return `${before}${normalizedBlock}${after}`.replace(/\n{3,}/g, "\n\n");
4668
6001
  }
4669
6002
  if (start !== -1 || end !== -1) {
4670
- throw new CliError(
6003
+ throw new WensityError(
6004
+ "config_invalid",
4671
6005
  `Configured Tailwind CSS has a partial ${options.label} block. Remove the broken markers and re-run install.`
4672
6006
  );
4673
6007
  }
@@ -4697,18 +6031,12 @@ async function assertNoConflicts(files, force) {
4697
6031
  if (await fileExists(file.absolutePath)) conflicts.push(file.relativePath);
4698
6032
  }
4699
6033
  if (conflicts.length > 0) {
4700
- throw new CliError(`Refusing to overwrite existing files. Use --force to replace:
6034
+ throw new WensityError("file_conflict", `Refusing to overwrite existing files. Use --force to replace:
4701
6035
  ${conflicts.map((path) => ` - ${path}`).join("\n")}`);
4702
6036
  }
4703
6037
  }
4704
- async function writePlannedFiles(files) {
4705
- for (const file of files) {
4706
- await mkdir5(dirname6(file.absolutePath), { recursive: true });
4707
- await writeFile5(file.absolutePath, file.content, "utf8");
4708
- }
4709
- }
4710
6038
  function summarizeFiles(projectRoot, files) {
4711
- return files.map((file) => toPosixPath(relative3(projectRoot, file.absolutePath)));
6039
+ return files.map((file) => toPosixPath(relative7(projectRoot, file.absolutePath)));
4712
6040
  }
4713
6041
  function transformComponentSource(content, config, iconOptions) {
4714
6042
  let transformed = content;
@@ -4729,13 +6057,13 @@ function getPayloadFileKind(payloadFile) {
4729
6057
  }
4730
6058
  function getPayloadOutputPath(payloadFile, fallbackFileName, config) {
4731
6059
  if (isUtilsHelperFile(payloadFile)) return withSourceExtension(config.utils, "ts");
4732
- if (isOverlayHelperFile(payloadFile)) return toPosixPath(join6(config.components, "overlay.tsx"));
4733
- if (isPrimitivesHelperFile(payloadFile)) return toPosixPath(join6(config.components, "primitives.tsx"));
6060
+ if (isOverlayHelperFile(payloadFile)) return toPosixPath(join10(config.components, "overlay.tsx"));
6061
+ if (isPrimitivesHelperFile(payloadFile)) return toPosixPath(join10(config.components, "primitives.tsx"));
4734
6062
  if (payloadFile.kind === "helper") {
4735
- throw new CliError(`Unsupported registry helper file path: ${payloadFile.path}`);
6063
+ throw new WensityError("path_unsafe", `Unsupported registry helper file path: ${payloadFile.path}`);
4736
6064
  }
4737
6065
  const safePayloadPath = sanitizePayloadPath(payloadFile.path, fallbackFileName);
4738
- return toPosixPath(join6(config.components, safePayloadPath));
6066
+ return toPosixPath(join10(config.components, safePayloadPath));
4739
6067
  }
4740
6068
  function isGraphHelperFile(payloadFile) {
4741
6069
  return isUtilsHelperFile(payloadFile) || isOverlayHelperFile(payloadFile) || isPrimitivesHelperFile(payloadFile);
@@ -4809,7 +6137,7 @@ function collectDependencies(input) {
4809
6137
  function parseImports(source) {
4810
6138
  const imports = [];
4811
6139
  const patterns = [
4812
- /(?:import|export)\s+(?:type\s+)?(?:[\s\S]*?\s+from\s*)?["']([^"']+)["']/g,
6140
+ /(?:import|export)\s+(?:type\s+)?(?:[^;'"`]*?\s+from\s*)?["']([^"']+)["']/g,
4813
6141
  /import\(\s*["']([^"']+)["']\s*\)/g
4814
6142
  ];
4815
6143
  for (const pattern of patterns) {
@@ -4832,7 +6160,7 @@ function normalizePackageName(specifier) {
4832
6160
  function withSourceExtension(path, extension) {
4833
6161
  const directory = parentDirectory(path);
4834
6162
  const fileName = extensionlessPath(path.split("/").at(-1) ?? path);
4835
- return toPosixPath(join6(directory === "." ? "" : directory, `${fileName}.${extension}`));
6163
+ return toPosixPath(join10(directory === "." ? "" : directory, `${fileName}.${extension}`));
4836
6164
  }
4837
6165
 
4838
6166
  // src/commands.ts
@@ -4925,6 +6253,7 @@ async function initCommand(context, parsed) {
4925
6253
  const hasPreset = Boolean(presetCode || presetFile);
4926
6254
  const dependencies = ["clsx", "tailwind-merge"];
4927
6255
  const installOptions = installOptionsFrom(context, parsed);
6256
+ assertProjectRootCandidate(context.cwd, context.env.HOME);
4928
6257
  if (hasPreset) {
4929
6258
  await initWithPreset(context, {
4930
6259
  dryRun,
@@ -4942,7 +6271,7 @@ async function initCommand(context, parsed) {
4942
6271
  const templateFiles = template ? await scaffoldProjectTemplate(context.cwd, template, { dryRun, force }) : [];
4943
6272
  const packageManagerFiles = template ? prepareProjectPackageManager(context.cwd, { ...installOptions, dryRun }) : [];
4944
6273
  const config = template ? applyTemplateToConfig(createDefaultConfig(context.cwd, apiUrl), template) : createDefaultConfig(context.cwd, apiUrl);
4945
- const configPath = join7(context.cwd, CONFIG_FILE_NAME);
6274
+ const configPath = join11(context.cwd, CONFIG_FILE_NAME);
4946
6275
  const utilityPath = resolveConfiguredFilePath(context.cwd, config.utils);
4947
6276
  const utilityRelativePath = toPosixPath(config.utils.endsWith(".ts") ? config.utils : `${config.utils}.ts`);
4948
6277
  if (dryRun) {
@@ -4963,8 +6292,8 @@ async function initCommand(context, parsed) {
4963
6292
  await writeTextFile(utilityPath, cnUtilityTemplate());
4964
6293
  }
4965
6294
  if (install) {
4966
- if (template) runProjectInstall(context.cwd, installOptions);
4967
- runInstall(context.cwd, missingDependencies(context.cwd, dependencies), installOptions);
6295
+ if (template) await runProjectInstall(context.cwd, installOptions);
6296
+ await runInstall2(context.cwd, missingDependencies(context.cwd, dependencies), installOptions);
4968
6297
  }
4969
6298
  await assertInitPostconditions(context.cwd, [
4970
6299
  ...templateFiles,
@@ -4992,7 +6321,7 @@ async function initWithPreset(context, options) {
4992
6321
  presetCode: options.presetCode,
4993
6322
  presetFile: options.presetFile
4994
6323
  });
4995
- const configPath = join7(context.cwd, CONFIG_FILE_NAME);
6324
+ const configPath = join11(context.cwd, CONFIG_FILE_NAME);
4996
6325
  const configExists = await fileExists(configPath);
4997
6326
  const templateFiles = options.template ? await scaffoldProjectTemplate(context.cwd, options.template, {
4998
6327
  dryRun: options.dryRun,
@@ -5007,7 +6336,7 @@ async function initWithPreset(context, options) {
5007
6336
  if (options.template) {
5008
6337
  throw new CliError(`${CONFIG_FILE_NAME} already exists. Omit --template for existing projects or use --force.`);
5009
6338
  }
5010
- const loaded = await loadConfig(context.cwd, { apiUrl: options.apiUrl });
6339
+ const loaded = await loadConfig(context.cwd, { apiUrl: options.apiUrl }, { home: context.env.HOME });
5011
6340
  config = {
5012
6341
  ...loaded.config,
5013
6342
  style
@@ -5061,8 +6390,8 @@ async function initWithPreset(context, options) {
5061
6390
  }
5062
6391
  });
5063
6392
  if (options.install) {
5064
- if (options.template) runProjectInstall(context.cwd, options.installOptions);
5065
- runInstall(
6393
+ if (options.template) await runProjectInstall(context.cwd, options.installOptions);
6394
+ await runInstall2(
5066
6395
  context.cwd,
5067
6396
  missingDependencies(context.cwd, installDependencies),
5068
6397
  options.installOptions
@@ -5111,7 +6440,7 @@ async function applyCommand(context, parsed) {
5111
6440
  if (!presetCodeInput && !presetFile) {
5112
6441
  throw new CliError("Missing preset. Pass --preset <code> or --preset-file <path>.");
5113
6442
  }
5114
- const loaded = await loadConfig(context.cwd, { registry: registryOverride, apiUrl });
6443
+ const loaded = await loadConfig(context.cwd, { registry: registryOverride, apiUrl }, { home: context.env.HOME });
5115
6444
  if (!loaded.exists) {
5116
6445
  throw new CliError(`${CONFIG_FILE_NAME} not found. Run "wensity init" first, then "wensity apply --preset <code>".`);
5117
6446
  }
@@ -5159,7 +6488,7 @@ async function applyCommand(context, parsed) {
5159
6488
  }
5160
6489
  });
5161
6490
  if (install && installDependencies.length > 0) {
5162
- runInstall(
6491
+ await runInstall2(
5163
6492
  loaded.projectRoot,
5164
6493
  missingDependencies(loaded.projectRoot, installDependencies),
5165
6494
  installOptionsFrom(context, parsed)
@@ -5212,7 +6541,7 @@ async function preparePresetStyleApplication(projectRoot, config, style, operati
5212
6541
  throw new CliError("No Tailwind CSS path configured. Set tailwind.css in wensity.json.");
5213
6542
  }
5214
6543
  const cssAbsolute = resolveInside(projectRoot, cssRelative);
5215
- const existingCss = await fileExists(cssAbsolute) ? await readFile6(cssAbsolute, "utf8") : "";
6544
+ const existingCss = await fileExists(cssAbsolute) ? await readFile8(cssAbsolute, "utf8") : "";
5216
6545
  const withBase = upsertBaseCssBlock(existingCss, buildBaseCssBlock(baseColorFromStyle(style)));
5217
6546
  const withTokens = upsertPrimitiveCssBlock(withBase, primitiveCss);
5218
6547
  const nextCss = upsertFontImportBlock(withTokens, fontPlan.block);
@@ -5237,7 +6566,7 @@ async function loginCommand(context, parsed) {
5237
6566
  if (!token) {
5238
6567
  throw new CliError("A token is required. Create one in the Wensity dashboard and run wensity login again.");
5239
6568
  }
5240
- const whoami = await requestJson(joinUrl(apiBaseUrl, "/api/cli/whoami"), { token });
6569
+ const whoami = await requestJson2(joinUrl(apiBaseUrl, "/api/cli/whoami"), { token });
5241
6570
  const path = await writeCredentials(context.env, {
5242
6571
  version: 1,
5243
6572
  apiBaseUrl,
@@ -5266,7 +6595,7 @@ async function logoutCommand(context) {
5266
6595
  }
5267
6596
  async function whoamiCommand(context, parsed) {
5268
6597
  const credentials = await requireCredentials(context.env);
5269
- const whoami = await requestJson(joinUrl(credentials.apiBaseUrl, "/api/cli/whoami"), {
6598
+ const whoami = await requestJson2(joinUrl(credentials.apiBaseUrl, "/api/cli/whoami"), {
5270
6599
  token: credentials.token
5271
6600
  });
5272
6601
  if (hasFlag(parsed, "json")) {
@@ -5280,10 +6609,15 @@ async function listCommand(context, parsed) {
5280
6609
  const resourceType = getResourceType(parsed);
5281
6610
  const apiUrl = getStringOption(parsed, "api-url") ?? context.env.WENSITY_API_URL;
5282
6611
  const registryOverride = getStringOption(parsed, "registry");
5283
- const loaded = await loadConfig(context.cwd, { registry: registryOverride, apiUrl });
5284
- const registry = registryUrlForType(loaded.config.registry, resourceType, registryOverride);
6612
+ const configuredRegistry = await loadConfig(context.cwd, { registry: registryOverride, apiUrl }, { home: context.env.HOME }).then((loaded) => loaded.config.registry).catch((error) => {
6613
+ if (isWensityError(error) && error.code === "project_ambiguous") {
6614
+ return registryOverride ?? createDefaultConfig(context.cwd, apiUrl).registry;
6615
+ }
6616
+ throw error;
6617
+ });
6618
+ const registry = registryUrlForType(configuredRegistry, resourceType, registryOverride);
5285
6619
  if (resourceType === "block") {
5286
- const response2 = await requestJson(registry);
6620
+ const response2 = await requestJson2(registry);
5287
6621
  if (hasFlag(parsed, "json")) {
5288
6622
  printJson(response2);
5289
6623
  return;
@@ -5302,7 +6636,7 @@ async function listCommand(context, parsed) {
5302
6636
  ]));
5303
6637
  return;
5304
6638
  }
5305
- const response = await requestJson(registry);
6639
+ const response = await requestJson2(registry);
5306
6640
  if (hasFlag(parsed, "json")) {
5307
6641
  printJson(response);
5308
6642
  return;
@@ -5354,7 +6688,10 @@ ${usage}`);
5354
6688
  }
5355
6689
  const apiUrl = getStringOption(parsed, "api-url") ?? context.env.WENSITY_API_URL;
5356
6690
  const registryOverride = getStringOption(parsed, "registry");
5357
- const loaded = await loadConfig(context.cwd, { registry: registryOverride, apiUrl });
6691
+ const loaded = await loadConfig(context.cwd, { registry: registryOverride, apiUrl }, { home: context.env.HOME });
6692
+ if (importAliasesResolve(loaded.projectRoot, loaded.config) === false) {
6693
+ throw new CliError(importAliasBlockMessage(loaded.config));
6694
+ }
5358
6695
  const credentials = await readCredentials(context.env);
5359
6696
  const registry = registryUrlForType(loaded.config.registry, resourceType, registryOverride);
5360
6697
  const force = updateMode || hasFlag(parsed, "force");
@@ -5405,13 +6742,12 @@ ${usage}`);
5405
6742
  if (unusedFontWarning) warn(unusedFontWarning);
5406
6743
  return;
5407
6744
  }
5408
- for (const prepared of installPlans) {
5409
- await writePlannedFiles(
5410
- prepared.cssFile ? [...prepared.files, prepared.cssFile] : prepared.files
5411
- );
5412
- }
6745
+ await writeWithRollback(
6746
+ loaded.projectRoot,
6747
+ installPlans.flatMap((prepared) => prepared.cssFile ? [...prepared.files, prepared.cssFile] : prepared.files)
6748
+ );
5413
6749
  if (install) {
5414
- runInstall(loaded.projectRoot, missing, installOptionsFrom(context, parsed));
6750
+ await runInstall2(loaded.projectRoot, missing, installOptionsFrom(context, parsed));
5415
6751
  }
5416
6752
  const noun = payloads.length === 1 ? "Component" : "Components";
5417
6753
  printTitle(updateMode ? `${noun} updated` : `${noun} installed`);
@@ -5436,61 +6772,95 @@ ${usage}`);
5436
6772
  resourceType === "block" ? "Customize the block copy, colors, and sections" : "Customize props and styling for your app"
5437
6773
  ]));
5438
6774
  }
6775
+ async function writeWithRollback(projectRoot, files) {
6776
+ const byPath = /* @__PURE__ */ new Map();
6777
+ for (const file of files) {
6778
+ byPath.delete(file.absolutePath);
6779
+ byPath.set(file.absolutePath, file);
6780
+ }
6781
+ const result = await apply({
6782
+ projectRoot,
6783
+ planId: `cli_${randomBytes(8).toString("hex")}`,
6784
+ files: [...byPath.values()].map((file) => ({
6785
+ relativePath: toPosixPath(relative8(projectRoot, file.absolutePath)),
6786
+ content: file.content,
6787
+ action: existsSync5(file.absolutePath) ? file.kind === "config" ? "modify" : "overwrite" : "create"
6788
+ }))
6789
+ });
6790
+ if (result.status === "committed") {
6791
+ if (result.cleanupWarning) warn(result.cleanupWarning);
6792
+ return;
6793
+ }
6794
+ const where = result.failedPath ? ` while writing ${result.failedPath}` : "";
6795
+ if (result.status === "rolled_back") {
6796
+ throw new WensityError(
6797
+ result.error.code === "write_failed" ? "write_failed" : result.error.code,
6798
+ `Installation failed${where}; all project-file changes were reverted.
6799
+ ${result.error.message}` + (result.cleanupWarning ? `
6800
+ ${result.cleanupWarning}` : ""),
6801
+ { cause: result.error }
6802
+ );
6803
+ }
6804
+ throw new WensityError(
6805
+ "rollback_failed",
6806
+ [
6807
+ `Installation failed${where}, and some files could not be restored. Nothing was deleted: the backups are in ${result.journalPath}.`,
6808
+ ...result.unrecovered.map((item) => ` - ${item.path}: ${item.recovery}`),
6809
+ "Verify with git diff before removing that directory yourself."
6810
+ ].join("\n"),
6811
+ { cause: result.error }
6812
+ );
6813
+ }
5439
6814
  function dedupe(values) {
5440
6815
  return [...new Set(values)];
5441
6816
  }
5442
6817
  async function fetchComponentPayload(input) {
6818
+ const client = registryClient(input.registry, input.credentials);
5443
6819
  const metadata = await withSpinner(
5444
6820
  `Finding ${input.slug}`,
5445
- () => requestJson(registryMetadataUrl(input.registry, input.slug)),
6821
+ () => client.fetchItemMetadata(input.slug, "component"),
5446
6822
  `Found ${input.slug}`
5447
6823
  );
5448
- if (input.credentials) {
5449
- const credentials = input.credentials;
5450
- const cliEndpoint = metadata.endpoints?.cli ?? `/api/cli/${input.slug}`;
5451
- const endpoint2 = cliEndpoint.startsWith("http") ? cliEndpoint : joinUrl(credentials.apiBaseUrl, cliEndpoint);
5452
- try {
5453
- return await withSpinner(
5454
- `Downloading ${metadata.name}`,
5455
- () => requestJson(endpoint2, { token: credentials.token }),
5456
- `Downloaded ${metadata.name}`
5457
- );
5458
- } catch (error) {
5459
- if (metadata.access === "pro" || !isAuthFailure(error)) throw error;
5460
- warn("Saved Wensity credentials were rejected; falling back to the public free registry payload.");
5461
- }
5462
- }
5463
- if (metadata.access === "pro") {
5464
- throw new CliError(formatProRequiredError(metadata.name, "component"));
6824
+ try {
6825
+ return await client.fetchSourcePayload(input.slug, "component", {
6826
+ metadata,
6827
+ download: (task) => withSpinner(`Downloading ${metadata.name}`, task, `Downloaded ${metadata.name}`),
6828
+ onCredentialsRejected: () => warn("Saved Wensity credentials were rejected; falling back to the public free registry payload.")
6829
+ });
6830
+ } catch (error) {
6831
+ throw proRequiredError(error, metadata.name, "component");
5465
6832
  }
5466
- const publicSourceEndpoint = metadata.endpoints?.registry ?? registrySlugUrl(input.registry, input.slug);
5467
- const endpoint = publicSourceEndpoint.startsWith("http") ? publicSourceEndpoint : joinUrl(registryOrigin(input.registry), publicSourceEndpoint);
5468
- return withSpinner(
5469
- `Downloading ${metadata.name}`,
5470
- () => requestJson(endpoint),
5471
- `Downloaded ${metadata.name}`
5472
- );
5473
- }
5474
- function isAuthFailure(error) {
5475
- return error instanceof CliError && /^(401|403)\b/.test(error.message);
5476
6833
  }
5477
6834
  async function fetchBlockPayload(input) {
6835
+ const client = registryClient(input.registry, input.credentials);
5478
6836
  const metadata = await withSpinner(
5479
6837
  `Finding ${input.slug}`,
5480
- () => requestJson(registryMetadataUrl(input.registry, input.slug)),
6838
+ () => client.fetchItemMetadata(input.slug, "block"),
5481
6839
  `Found ${input.slug}`
5482
6840
  );
5483
- if (!input.credentials) {
5484
- throw new CliError(formatProRequiredError(metadata.name, "block"));
5485
- }
5486
- const credentials = input.credentials;
5487
- const cliEndpoint = metadata.endpoints?.cli ?? `/api/cli/blocks/${input.slug}`;
5488
- const endpoint = cliEndpoint.startsWith("http") ? cliEndpoint : joinUrl(credentials.apiBaseUrl, cliEndpoint);
5489
- return withSpinner(
5490
- `Downloading ${metadata.name}`,
5491
- () => requestJson(endpoint, { token: credentials.token }),
5492
- `Downloaded ${metadata.name}`
5493
- );
6841
+ try {
6842
+ return await client.fetchSourcePayload(input.slug, "block", {
6843
+ metadata,
6844
+ download: (task) => withSpinner(`Downloading ${metadata.name}`, task, `Downloaded ${metadata.name}`)
6845
+ });
6846
+ } catch (error) {
6847
+ throw proRequiredError(error, metadata.name, "block");
6848
+ }
6849
+ }
6850
+ function registryClient(registry, credentials) {
6851
+ return createWensityClient({
6852
+ origin: registryOrigin(registry),
6853
+ registryUrl: registry,
6854
+ blocksRegistryUrl: registry,
6855
+ ...credentials ? { token: credentials.token, apiBaseUrl: credentials.apiBaseUrl } : {},
6856
+ userAgent: `${CLI_NAME}/${CLI_VERSION}`
6857
+ });
6858
+ }
6859
+ function proRequiredError(error, resourceName, resourceType) {
6860
+ if (isWensityError(error) && error.code === "auth_required") {
6861
+ return new CliError(formatProRequiredError(resourceName, resourceType), 1, { cause: error });
6862
+ }
6863
+ return error;
5494
6864
  }
5495
6865
  function getResourceType(parsed) {
5496
6866
  const value = getStringOption(parsed, "type") ?? "component";
@@ -5509,18 +6879,6 @@ async function requireCredentials(env) {
5509
6879
  }
5510
6880
  return credentials;
5511
6881
  }
5512
- function registryMetadataUrl(registry, slug) {
5513
- const url = new URL(registry);
5514
- url.searchParams.set("slug", slug);
5515
- return url.toString();
5516
- }
5517
- function registrySlugUrl(registry, slug) {
5518
- return joinUrl(registry.endsWith("/") ? registry : `${registry}/`, slug);
5519
- }
5520
- function registryOrigin(registry) {
5521
- const url = new URL(registry);
5522
- return `${url.protocol}//${url.host}`;
5523
- }
5524
6882
  function formatHelp() {
5525
6883
  return [
5526
6884
  brandBanner("Own your component source."),
@@ -5548,7 +6906,7 @@ function formatAccountPanel(whoami, savedPath) {
5548
6906
  return panel(isPro ? "Wensity Pro" : "Wensity Free", lines);
5549
6907
  }
5550
6908
  async function writeTextFile(path, content) {
5551
- await mkdir6(dirname7(path), { recursive: true });
6909
+ await mkdir7(dirname8(path), { recursive: true });
5552
6910
  await writeFile6(path, content, "utf8");
5553
6911
  }
5554
6912
  function installOptionsFrom(context, parsed) {
@@ -5560,7 +6918,7 @@ function installOptionsFrom(context, parsed) {
5560
6918
  async function assertInitPostconditions(projectRoot, relativePaths) {
5561
6919
  const missing = [];
5562
6920
  for (const relativePath of new Set(relativePaths)) {
5563
- const absolute = join7(projectRoot, relativePath);
6921
+ const absolute = join11(projectRoot, relativePath);
5564
6922
  let size = 0;
5565
6923
  try {
5566
6924
  size = (await stat(absolute)).size;
@@ -5584,10 +6942,11 @@ run({
5584
6942
  cwd: process.cwd(),
5585
6943
  env: process.env
5586
6944
  }).catch((error) => {
5587
- if (error instanceof CliError) {
5588
- process.stderr.write(`${formatCliError(error.message)}
6945
+ const cliError = toCliError(error);
6946
+ if (cliError) {
6947
+ process.stderr.write(`${formatCliError(cliError.message)}
5589
6948
  `);
5590
- process.exitCode = error.exitCode;
6949
+ process.exitCode = cliError.exitCode;
5591
6950
  return;
5592
6951
  }
5593
6952
  const message = error instanceof Error ? error.stack ?? error.message : String(error);