wensity 0.1.12 → 0.1.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +6 -1
- package/dist/index.js +1977 -640
- 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 {
|
|
5
|
-
import {
|
|
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.13";
|
|
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
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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/
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
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
|
|
127
|
-
return
|
|
514
|
+
function withoutAuthorization(headers) {
|
|
515
|
+
return Object.fromEntries(Object.entries(headers).filter(([name]) => name.toLowerCase() !== "authorization"));
|
|
128
516
|
}
|
|
129
|
-
function
|
|
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
|
-
|
|
132
|
-
"
|
|
133
|
-
"
|
|
134
|
-
|
|
135
|
-
"
|
|
136
|
-
|
|
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
|
|
142
|
-
|
|
143
|
-
|
|
677
|
+
function assertNoIncompleteJournal(projectRoot) {
|
|
678
|
+
const [first] = findIncompleteJournals(projectRoot);
|
|
679
|
+
if (first) throw recoveryRequired(first);
|
|
144
680
|
}
|
|
145
681
|
|
|
146
|
-
// src/
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
682
|
+
// ../install-core/src/apply.ts
|
|
683
|
+
var WINDOWS_RETRY_CODES = /* @__PURE__ */ new Set(["EPERM", "EBUSY", "EACCES"]);
|
|
684
|
+
var WINDOWS_PLACEMENT_ATTEMPTS = 5;
|
|
685
|
+
function sha256(data) {
|
|
686
|
+
return createHash("sha256").update(data).digest("hex");
|
|
687
|
+
}
|
|
688
|
+
async function apply(options) {
|
|
689
|
+
const { projectRoot, planId, files } = options;
|
|
690
|
+
const hooks = options.testHooks ?? {};
|
|
691
|
+
const platform = options.platform ?? process.platform;
|
|
692
|
+
const emit = (event) => hooks.onEvent?.(event);
|
|
693
|
+
const fault = (at, index = 0) => {
|
|
694
|
+
if (hooks.fault && hooks.fault.at === at && (hooks.fault.file ?? 0) === index) {
|
|
695
|
+
throw new WensityError("write_failed", `Injected fault at ${at}.`);
|
|
696
|
+
}
|
|
697
|
+
};
|
|
698
|
+
if (!/^[A-Za-z0-9_-]{1,80}$/.test(planId)) throw new WensityError("path_unsafe", `Invalid journal name: ${planId}`);
|
|
699
|
+
assertNoIncompleteJournal(projectRoot);
|
|
700
|
+
const targets = files.map((file) => {
|
|
701
|
+
assertTargetPathLength(file.relativePath);
|
|
702
|
+
const absolutePath = resolveInside(projectRoot, file.relativePath);
|
|
703
|
+
assertNoSymlinkEscape(projectRoot, absolutePath);
|
|
704
|
+
if (file.content.includes("\0")) {
|
|
705
|
+
throw new WensityError("source_integrity_failed", `${file.relativePath} contains a NUL byte; Wensity only installs text source.`);
|
|
706
|
+
}
|
|
707
|
+
const stat2 = lstatOrUndefined2(absolutePath);
|
|
708
|
+
if (stat2 && (stat2.isSymbolicLink() || !stat2.isFile())) {
|
|
709
|
+
throw new WensityError("path_unsafe", `${file.relativePath} exists but is not a regular file (a symlink or directory). Nothing was written.`);
|
|
710
|
+
}
|
|
711
|
+
if (file.action === "create" && stat2) {
|
|
712
|
+
throw new WensityError("plan_stale", `${file.relativePath} appeared after planning.`, {
|
|
713
|
+
details: { reason: "precondition_mismatch", path: file.relativePath, expected: "absent", found: "present" }
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
if (file.action !== "create" && !stat2) {
|
|
717
|
+
throw new WensityError("plan_stale", `${file.relativePath} was removed after planning.`, {
|
|
718
|
+
details: { reason: "precondition_mismatch", path: file.relativePath, expected: "present", found: "absent" }
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
return { ...file, absolutePath, existed: Boolean(stat2), mode: stat2 ? stat2.mode & 511 : void 0 };
|
|
722
|
+
});
|
|
723
|
+
const workDir = ensureWorkDir(projectRoot);
|
|
724
|
+
const journalDir = join3(workDir, planId);
|
|
725
|
+
try {
|
|
726
|
+
mkdirSync2(journalDir, { mode: 448 });
|
|
727
|
+
} catch (error) {
|
|
728
|
+
if (isNodeError(error) && error.code === "EEXIST") {
|
|
729
|
+
throw new WensityError("path_unsafe", `${WORK_DIR_NAME}/${planId} already exists; it was not reused.`);
|
|
730
|
+
}
|
|
731
|
+
throw error;
|
|
732
|
+
}
|
|
733
|
+
const journalPath = toPosixPath(relative3(projectRoot, journalDir));
|
|
734
|
+
const journalFile = join3(journalDir, JOURNAL_FILE);
|
|
735
|
+
const entries = targets.map((target) => ({
|
|
736
|
+
relativePath: target.relativePath,
|
|
737
|
+
existed: target.existed,
|
|
738
|
+
expectedSha256: sha256(target.content),
|
|
739
|
+
action: target.action
|
|
740
|
+
}));
|
|
741
|
+
const writeJournal = async (status, step) => {
|
|
742
|
+
await writeSynced(journalFile, `${JSON.stringify({ version: 1, planId, status, step, entries }, null, 2)}
|
|
743
|
+
`, 384, true);
|
|
744
|
+
await syncDirectory(journalDir);
|
|
745
|
+
emit({ type: "journal", status, step });
|
|
746
|
+
};
|
|
747
|
+
const attempted = targets.map(() => false);
|
|
748
|
+
const placed = targets.map(() => void 0);
|
|
749
|
+
const createdDirs = [];
|
|
750
|
+
let failedPath;
|
|
751
|
+
try {
|
|
752
|
+
await mkdir(join3(journalDir, "backup"), { mode: 448 });
|
|
753
|
+
await mkdir(join3(journalDir, "staged"), { mode: 448 });
|
|
754
|
+
for (const [index, target] of targets.entries()) {
|
|
755
|
+
failedPath = target.relativePath;
|
|
756
|
+
if (target.existed) {
|
|
757
|
+
const backup = `backup/${index}`;
|
|
758
|
+
await copyFile(target.absolutePath, join3(journalDir, backup), fsConstants.COPYFILE_EXCL);
|
|
759
|
+
entries[index].backup = backup;
|
|
760
|
+
entries[index].sha256Before = sha256(await readFile(join3(journalDir, backup)));
|
|
761
|
+
entries[index].mode = target.mode;
|
|
762
|
+
emit({ type: "backup", path: target.relativePath });
|
|
763
|
+
}
|
|
764
|
+
fault("backup", index);
|
|
765
|
+
}
|
|
766
|
+
failedPath = void 0;
|
|
767
|
+
await writeJournal("in_progress", "backed_up");
|
|
768
|
+
fault("journal");
|
|
769
|
+
await writeJournal("in_progress", "staging");
|
|
770
|
+
for (const [index, target] of targets.entries()) {
|
|
771
|
+
failedPath = target.relativePath;
|
|
772
|
+
await writeSynced(join3(journalDir, "staged", String(index)), target.content, target.mode ?? 420);
|
|
773
|
+
emit({ type: "stage", path: target.relativePath });
|
|
774
|
+
fault("stage", index);
|
|
775
|
+
}
|
|
776
|
+
await writeJournal("in_progress", "placing");
|
|
777
|
+
for (const [index, target] of targets.entries()) {
|
|
778
|
+
failedPath = target.relativePath;
|
|
779
|
+
await makeParents(projectRoot, dirname2(target.absolutePath), createdDirs);
|
|
780
|
+
attempted[index] = true;
|
|
781
|
+
placed[index] = await place(join3(journalDir, "staged", String(index)), target, platform, hooks, emit, projectRoot);
|
|
782
|
+
assertPlaced(projectRoot, target, placed[index]);
|
|
783
|
+
fault("place", index);
|
|
784
|
+
const onDisk = sha256(await readFile(target.absolutePath));
|
|
785
|
+
emit({ type: "verify", path: target.relativePath });
|
|
786
|
+
if (onDisk !== entries[index].expectedSha256) {
|
|
787
|
+
throw new WensityError("source_integrity_failed", `${target.relativePath} did not verify after writing (hash mismatch).`, {
|
|
788
|
+
details: { reason: "post_write_hash_mismatch", path: target.relativePath }
|
|
789
|
+
});
|
|
790
|
+
}
|
|
791
|
+
fault("verify", index);
|
|
792
|
+
}
|
|
793
|
+
} catch (cause) {
|
|
794
|
+
const error = isWensityError(cause) ? cause : new WensityError("write_failed", `Writing ${failedPath ?? "the project files"} failed: ${cause.message}`, { cause });
|
|
795
|
+
return rollback({ projectRoot, journalDir, journalPath, targets, entries, attempted, placed, createdDirs, writeJournal, error, failedPath, emit });
|
|
796
|
+
}
|
|
797
|
+
try {
|
|
798
|
+
fault("commit");
|
|
799
|
+
await writeJournal("committed", "done");
|
|
800
|
+
} catch (cause) {
|
|
801
|
+
const error = isWensityError(cause) ? cause : new WensityError("write_failed", `Recording the commit failed: ${cause.message}`, { cause });
|
|
802
|
+
return rollback({ projectRoot, journalDir, journalPath, targets, entries, attempted, placed, createdDirs, writeJournal, error, failedPath: void 0, emit });
|
|
803
|
+
}
|
|
804
|
+
let cleanupWarning;
|
|
805
|
+
try {
|
|
806
|
+
await rm(journalDir, { recursive: true, force: true });
|
|
807
|
+
removeWorkDirIfEmpty(projectRoot);
|
|
808
|
+
} catch (error) {
|
|
809
|
+
cleanupWarning = `The install committed, but ${journalPath} could not be removed (${error.message}). It is marked committed and can be deleted.`;
|
|
810
|
+
}
|
|
811
|
+
return {
|
|
812
|
+
status: "committed",
|
|
813
|
+
...cleanupWarning ? { cleanupWarning } : {},
|
|
814
|
+
changes: targets.map((target, index) => ({
|
|
815
|
+
path: target.relativePath,
|
|
816
|
+
action: target.action === "create" ? "created" : target.action === "modify" ? "modified" : "overwritten",
|
|
817
|
+
bytes: Buffer.byteLength(target.content),
|
|
818
|
+
sha256: entries[index].expectedSha256
|
|
819
|
+
}))
|
|
820
|
+
};
|
|
821
|
+
}
|
|
822
|
+
async function rollback(input) {
|
|
823
|
+
const { journalDir, targets, entries, attempted, placed } = input;
|
|
824
|
+
const restored = [];
|
|
825
|
+
const removed = [];
|
|
826
|
+
const unrecovered = [];
|
|
827
|
+
for (let index = targets.length - 1; index >= 0; index -= 1) {
|
|
828
|
+
if (!attempted[index]) continue;
|
|
829
|
+
const target = targets[index];
|
|
830
|
+
const entry = entries[index];
|
|
831
|
+
try {
|
|
832
|
+
const backupBytes = entry.existed ? await readFile(join3(journalDir, entry.backup)) : void 0;
|
|
833
|
+
input.emit({ type: "rollback", path: target.relativePath });
|
|
834
|
+
const outcome = rollbackOneSync(input.projectRoot, target, entry, placed[index], backupBytes);
|
|
835
|
+
if (outcome === "restored") restored.push(target.relativePath);
|
|
836
|
+
else removed.push(target.relativePath);
|
|
837
|
+
} catch (error) {
|
|
838
|
+
unrecovered.push({
|
|
839
|
+
path: target.relativePath,
|
|
840
|
+
recovery: entry.existed ? `Copy ${input.journalPath}/${entry.backup} back to ${target.relativePath} (${error.message})` : `Delete ${target.relativePath} if you did not create it yourself (${error.message})`
|
|
841
|
+
});
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
for (const dir of [...input.createdDirs].reverse()) {
|
|
845
|
+
try {
|
|
846
|
+
assertNoSymlinkEscape(input.projectRoot, dir);
|
|
847
|
+
const stat2 = lstatSync3(dir);
|
|
848
|
+
if (stat2.isDirectory() && !stat2.isSymbolicLink()) rmdirSync2(dir);
|
|
849
|
+
} catch {
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
if (unrecovered.length > 0) {
|
|
853
|
+
await input.writeJournal("rollback_failed", "rollback").catch(() => void 0);
|
|
854
|
+
return {
|
|
855
|
+
status: "rollback_failed",
|
|
856
|
+
...input.failedPath ? { failedPath: input.failedPath } : {},
|
|
857
|
+
journalPath: input.journalPath,
|
|
858
|
+
restored,
|
|
859
|
+
removed,
|
|
860
|
+
unrecovered,
|
|
861
|
+
error: input.error
|
|
862
|
+
};
|
|
863
|
+
}
|
|
864
|
+
let cleanupWarning;
|
|
865
|
+
try {
|
|
866
|
+
await input.writeJournal("rolled_back", "done");
|
|
867
|
+
} catch (error) {
|
|
868
|
+
cleanupWarning = `Every file was restored, but ${input.journalPath} could not be marked rolled back (${error.message}).`;
|
|
869
|
+
}
|
|
870
|
+
try {
|
|
871
|
+
await rm(journalDir, { recursive: true, force: true });
|
|
872
|
+
removeWorkDirIfEmpty(input.projectRoot);
|
|
873
|
+
} catch (error) {
|
|
874
|
+
cleanupWarning = `Every file was restored, but ${input.journalPath} could not be removed (${error.message}); it may block the next install until you delete it.`;
|
|
875
|
+
}
|
|
876
|
+
return {
|
|
877
|
+
status: "rolled_back",
|
|
878
|
+
...input.failedPath ? { failedPath: input.failedPath } : {},
|
|
879
|
+
restored,
|
|
880
|
+
removed,
|
|
881
|
+
error: input.error,
|
|
882
|
+
...cleanupWarning ? { cleanupWarning } : {}
|
|
883
|
+
};
|
|
884
|
+
}
|
|
885
|
+
function identityOf(stat2) {
|
|
886
|
+
return { dev: stat2.dev, ino: stat2.ino };
|
|
887
|
+
}
|
|
888
|
+
function sameFile(a, b) {
|
|
889
|
+
return a.dev === b.dev && a.ino === b.ino;
|
|
890
|
+
}
|
|
891
|
+
function unsafe2(relativePath, why) {
|
|
892
|
+
return new WensityError("path_unsafe", `${relativePath} ${why}; it was not touched.`);
|
|
893
|
+
}
|
|
894
|
+
function rollbackOneSync(projectRoot, target, entry, placedFile, backupBytes) {
|
|
895
|
+
const { relativePath, absolutePath } = target;
|
|
896
|
+
if (!placedFile) return entry.existed ? "restored" : "removed";
|
|
897
|
+
assertNoSymlinkEscape(projectRoot, absolutePath);
|
|
898
|
+
const current = lstatOrUndefinedBig(absolutePath);
|
|
899
|
+
if (current && (current.isSymbolicLink() || !current.isFile())) throw unsafe2(relativePath, "was replaced by a symlink or non-file");
|
|
900
|
+
if (!current) throw unsafe2(relativePath, "was deleted after it was written");
|
|
901
|
+
if (!sameFile(identityOf(current), placedFile)) throw unsafe2(relativePath, "was replaced by another file after it was written");
|
|
902
|
+
if (!entry.existed) {
|
|
903
|
+
unlinkSync(absolutePath);
|
|
904
|
+
return "removed";
|
|
905
|
+
}
|
|
906
|
+
if (sha256(backupBytes) !== entry.sha256Before) throw new Error("the backup no longer matches its recorded hash; it was not written back");
|
|
907
|
+
const fd = openSync(absolutePath, fsConstants.O_WRONLY | (fsConstants.O_NOFOLLOW ?? 0));
|
|
908
|
+
try {
|
|
909
|
+
const opened = identityOf(fstatSync(fd, { bigint: true }));
|
|
910
|
+
assertNoSymlinkEscape(projectRoot, absolutePath);
|
|
911
|
+
const again = lstatOrUndefinedBig(absolutePath);
|
|
912
|
+
if (!sameFile(opened, placedFile) || !again || !sameFile(identityOf(again), placedFile)) {
|
|
913
|
+
throw unsafe2(relativePath, "moved while it was being restored");
|
|
914
|
+
}
|
|
915
|
+
ftruncateSync(fd, 0);
|
|
916
|
+
let offset = 0;
|
|
917
|
+
while (offset < backupBytes.length) offset += writeSync(fd, backupBytes, offset, backupBytes.length - offset, offset);
|
|
918
|
+
fsyncSync(fd);
|
|
919
|
+
if (entry.mode !== void 0) fchmodSync(fd, entry.mode);
|
|
920
|
+
} finally {
|
|
921
|
+
closeSync(fd);
|
|
922
|
+
}
|
|
923
|
+
return "restored";
|
|
924
|
+
}
|
|
925
|
+
function lstatOrUndefinedBig(path) {
|
|
926
|
+
try {
|
|
927
|
+
return lstatSync3(path, { bigint: true });
|
|
928
|
+
} catch (error) {
|
|
929
|
+
if (isNodeError(error) && error.code === "ENOENT") return void 0;
|
|
930
|
+
throw error;
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
async function place(staged, target, platform, hooks, emit, projectRoot) {
|
|
934
|
+
const stagedFile = identityOf(lstatSync3(staged, { bigint: true }));
|
|
935
|
+
const attempts = platform === "win32" && target.existed ? WINDOWS_PLACEMENT_ATTEMPTS : 1;
|
|
936
|
+
for (let attempt = 1; ; attempt += 1) {
|
|
937
|
+
emit({ type: "place", path: target.relativePath, attempt });
|
|
938
|
+
try {
|
|
939
|
+
if (hooks.rename) {
|
|
940
|
+
assertNoSymlinkEscape(projectRoot, target.absolutePath);
|
|
941
|
+
await hooks.rename(staged, target.absolutePath);
|
|
942
|
+
} else {
|
|
943
|
+
assertNoSymlinkEscape(projectRoot, target.absolutePath);
|
|
944
|
+
renameSync(staged, target.absolutePath);
|
|
945
|
+
}
|
|
946
|
+
break;
|
|
947
|
+
} catch (error) {
|
|
948
|
+
const retryable = isNodeError(error) && WINDOWS_RETRY_CODES.has(error.code ?? "");
|
|
949
|
+
if (!retryable || attempt >= attempts) throw error;
|
|
950
|
+
await new Promise((resolve3) => setTimeout(resolve3, 25 * 2 ** (attempt - 1)));
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
return stagedFile;
|
|
954
|
+
}
|
|
955
|
+
function assertPlaced(projectRoot, target, placedFile) {
|
|
956
|
+
assertNoSymlinkEscape(projectRoot, target.absolutePath);
|
|
957
|
+
const now = lstatOrUndefinedBig(target.absolutePath);
|
|
958
|
+
if (!now || now.isSymbolicLink() || !sameFile(identityOf(now), placedFile)) {
|
|
959
|
+
throw new WensityError("path_unsafe", `${target.relativePath} is not the file that was just written; the install is being rolled back.`);
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
async function makeParents(projectRoot, dir, created) {
|
|
963
|
+
const missing = [];
|
|
964
|
+
for (let current = dir; current !== projectRoot && !lstatOrUndefined2(current); current = dirname2(current)) {
|
|
965
|
+
missing.unshift(current);
|
|
966
|
+
}
|
|
967
|
+
for (const path of missing) {
|
|
968
|
+
await mkdir(path);
|
|
969
|
+
created.push(path);
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
async function writeSynced(path, data, mode, replace = false) {
|
|
973
|
+
if (replace) {
|
|
974
|
+
const temp = `${path}.next`;
|
|
975
|
+
await writeSynced(temp, data, mode);
|
|
976
|
+
await rename(temp, path);
|
|
977
|
+
return;
|
|
978
|
+
}
|
|
979
|
+
const handle = await open(path, "wx", mode);
|
|
980
|
+
try {
|
|
981
|
+
await handle.writeFile(data, "utf8");
|
|
982
|
+
await handle.sync();
|
|
983
|
+
} finally {
|
|
984
|
+
await handle.close();
|
|
985
|
+
}
|
|
986
|
+
await chmod(path, mode);
|
|
987
|
+
}
|
|
988
|
+
async function syncDirectory(dir) {
|
|
989
|
+
try {
|
|
990
|
+
const handle = await open(dir, "r");
|
|
991
|
+
try {
|
|
992
|
+
await handle.sync();
|
|
993
|
+
} finally {
|
|
994
|
+
await handle.close();
|
|
995
|
+
}
|
|
996
|
+
} catch {
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
function lstatOrUndefined2(path) {
|
|
1000
|
+
try {
|
|
1001
|
+
return lstatSync3(path);
|
|
1002
|
+
} catch (error) {
|
|
1003
|
+
if (isNodeError(error) && error.code === "ENOENT") return void 0;
|
|
1004
|
+
throw error;
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
// ../install-core/src/project.ts
|
|
1009
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
1010
|
+
import { existsSync as existsSync4, lstatSync as lstatSync4, readdirSync as readdirSync2, readFileSync as readFileSync4, statSync as statSync4 } from "fs";
|
|
1011
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
1012
|
+
import { join as join7, relative as relative6 } from "path";
|
|
1013
|
+
|
|
1014
|
+
// ../install-core/src/config.ts
|
|
1015
|
+
import { existsSync as existsSync2, realpathSync as realpathSync3, statSync as statSync2 } from "fs";
|
|
1016
|
+
import { access, mkdir as mkdir2, readFile as readFile2, writeFile } from "fs/promises";
|
|
1017
|
+
import { homedir } from "os";
|
|
1018
|
+
import { dirname as dirname3, join as join5, posix as posix2, relative as relative5, resolve as resolve2 } from "path";
|
|
150
1019
|
|
|
151
1020
|
// ../wensity-preset/src/constants.ts
|
|
152
1021
|
var ICON_LIBRARIES = ["tabler", "lucide"];
|
|
@@ -1498,6 +2367,7 @@ var TABLER_TO_LUCIDE = {
|
|
|
1498
2367
|
IconChevronsRight: "ChevronsRight",
|
|
1499
2368
|
IconCircleX: "CircleX",
|
|
1500
2369
|
IconClock: "Clock",
|
|
2370
|
+
IconColorPicker: "Pipette",
|
|
1501
2371
|
IconCornerDownLeft: "CornerDownLeft",
|
|
1502
2372
|
IconDots: "Ellipsis",
|
|
1503
2373
|
IconDownload: "Download",
|
|
@@ -2593,99 +3463,31 @@ var BASE_SEMANTIC_THEME_UTILITIES = {
|
|
|
2593
3463
|
"--color-surface-muted": "var(--surface-muted)",
|
|
2594
3464
|
"--color-muted": "var(--muted)",
|
|
2595
3465
|
"--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("");
|
|
3466
|
+
"--color-border": "var(--border)",
|
|
3467
|
+
"--color-border-strong": "var(--border-strong)"
|
|
3468
|
+
};
|
|
3469
|
+
var BASE_CSS_START = "/* \u2500\u2500 Wensity Base Tokens \u2014 generated by the Wensity CLI.\n Edit these values to retheme your app; re-running `wensity init --preset`\n replaces this block. \u2500\u2500 */";
|
|
3470
|
+
var BASE_CSS_END = "/* \u2500\u2500 End Wensity Base Tokens \u2500\u2500 */";
|
|
3471
|
+
function formatBaseDeclarations(entries, indent = " ") {
|
|
3472
|
+
return Object.entries(entries).map(([name, value]) => `${indent}${name}: ${value};`).join("\n");
|
|
2683
3473
|
}
|
|
2684
|
-
function
|
|
2685
|
-
return
|
|
3474
|
+
function buildBaseCssBlock(baseColor = "inherit") {
|
|
3475
|
+
return [
|
|
3476
|
+
BASE_CSS_START,
|
|
3477
|
+
"@theme inline {",
|
|
3478
|
+
formatBaseDeclarations(BASE_SEMANTIC_THEME_UTILITIES),
|
|
3479
|
+
"}",
|
|
3480
|
+
":root {",
|
|
3481
|
+
formatBaseDeclarations(exportedBaseSemanticCssVars(baseColor, "light")),
|
|
3482
|
+
"}",
|
|
3483
|
+
".dark {",
|
|
3484
|
+
formatBaseDeclarations(exportedBaseSemanticCssVars(baseColor, "dark")),
|
|
3485
|
+
"}",
|
|
3486
|
+
BASE_CSS_END
|
|
3487
|
+
].join("\n");
|
|
2686
3488
|
}
|
|
2687
3489
|
|
|
2688
|
-
// src/style-shape.ts
|
|
3490
|
+
// ../install-core/src/style-shape.ts
|
|
2689
3491
|
function isCanonicalStyleShape(value) {
|
|
2690
3492
|
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
2691
3493
|
const record = value;
|
|
@@ -2720,11 +3522,159 @@ function curatedFontsFromStyle(style) {
|
|
|
2720
3522
|
return { headingFont: "system", bodyFont: "system" };
|
|
2721
3523
|
}
|
|
2722
3524
|
|
|
2723
|
-
// src/
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
3525
|
+
// ../install-core/src/tsconfig-paths.ts
|
|
3526
|
+
import { existsSync, readFileSync as readFileSync2, statSync } from "fs";
|
|
3527
|
+
import { join as join4, posix, relative as relative4 } from "path";
|
|
3528
|
+
var MAX_CONFIG_HOPS = 8;
|
|
3529
|
+
var CONFIG_DIR = "${configDir}";
|
|
3530
|
+
function containedFile(projectRoot, relativePath) {
|
|
3531
|
+
let absolute;
|
|
3532
|
+
try {
|
|
3533
|
+
absolute = resolveInside(projectRoot, relativePath);
|
|
3534
|
+
assertNoSymlinkEscape(projectRoot, absolute);
|
|
3535
|
+
} catch {
|
|
3536
|
+
return void 0;
|
|
3537
|
+
}
|
|
3538
|
+
return existsSync(absolute) ? absolute : void 0;
|
|
3539
|
+
}
|
|
3540
|
+
function readPathMapping(projectRoot) {
|
|
3541
|
+
for (const file of ["tsconfig.json", "jsconfig.json"]) {
|
|
3542
|
+
if (!containedFile(projectRoot, file)) continue;
|
|
3543
|
+
const root = parseConfig(projectRoot, file);
|
|
3544
|
+
const own = resolveConfig(projectRoot, file, 0);
|
|
3545
|
+
if (!root || own === UNKNOWN) return void 0;
|
|
3546
|
+
if (own.paths) return toMapping(own, posix.dirname(file));
|
|
3547
|
+
let unresolvedReference = false;
|
|
3548
|
+
for (const reference of root.references ?? []) {
|
|
3549
|
+
if (typeof reference.path !== "string") continue;
|
|
3550
|
+
const referenced = referencePath(projectRoot, posix.join(posix.dirname(file), reference.path));
|
|
3551
|
+
const resolved = referenced ? resolveConfig(projectRoot, referenced, 0) : UNKNOWN;
|
|
3552
|
+
if (resolved === UNKNOWN) unresolvedReference = true;
|
|
3553
|
+
else if (resolved.paths) return toMapping(resolved, posix.dirname(referenced));
|
|
3554
|
+
}
|
|
3555
|
+
return unresolvedReference ? void 0 : toMapping(own, posix.dirname(file));
|
|
3556
|
+
}
|
|
3557
|
+
return void 0;
|
|
3558
|
+
}
|
|
3559
|
+
function aliasResolvesTo(mapping, alias, target) {
|
|
3560
|
+
const wanted = posix.normalize(stripExtension(target));
|
|
3561
|
+
for (const [key, values] of Object.entries(mapping.paths)) {
|
|
3562
|
+
const wildcard = key.endsWith("/*");
|
|
3563
|
+
const prefix = wildcard ? key.slice(0, -1) : key;
|
|
3564
|
+
if (wildcard ? !alias.startsWith(prefix) : alias !== key) continue;
|
|
3565
|
+
const rest = wildcard ? alias.slice(prefix.length) : "";
|
|
3566
|
+
for (const value of values) {
|
|
3567
|
+
const targetBase = value.endsWith("/*") ? value.slice(0, -1) : value;
|
|
3568
|
+
const candidate = posix.normalize(posix.join(mapping.base, targetBase, rest));
|
|
3569
|
+
if (stripExtension(candidate) === wanted) return true;
|
|
3570
|
+
}
|
|
3571
|
+
}
|
|
3572
|
+
return false;
|
|
3573
|
+
}
|
|
3574
|
+
function atAliasRoot(mapping) {
|
|
3575
|
+
const target = mapping?.paths["@/*"]?.find((value) => value.endsWith("/*"));
|
|
3576
|
+
if (target === void 0) return void 0;
|
|
3577
|
+
const dir = posix.normalize(posix.join(mapping.base, target.slice(0, -2)));
|
|
3578
|
+
if (dir === ".." || dir.startsWith("../") || posix.isAbsolute(dir)) return void 0;
|
|
3579
|
+
if (dir === "node_modules" || dir.startsWith("node_modules/")) return void 0;
|
|
3580
|
+
return dir === "." ? "" : dir;
|
|
3581
|
+
}
|
|
3582
|
+
var UNKNOWN = /* @__PURE__ */ Symbol("unknown");
|
|
3583
|
+
function resolveConfig(projectRoot, file, hops) {
|
|
3584
|
+
const parsed = parseConfig(projectRoot, file);
|
|
3585
|
+
if (!parsed || hops > MAX_CONFIG_HOPS) return UNKNOWN;
|
|
3586
|
+
const dir = posix.dirname(file);
|
|
3587
|
+
const options = parsed.compilerOptions ?? {};
|
|
3588
|
+
let resolved = {};
|
|
3589
|
+
const parents = typeof parsed.extends === "string" ? [parsed.extends] : parsed.extends ?? [];
|
|
3590
|
+
for (const parent of parents) {
|
|
3591
|
+
if (typeof parent !== "string") return UNKNOWN;
|
|
3592
|
+
const parentFile = extendsTarget(projectRoot, dir, parent);
|
|
3593
|
+
const inherited = parentFile ? resolveConfig(projectRoot, parentFile, hops + 1) : UNKNOWN;
|
|
3594
|
+
if (inherited === UNKNOWN) return UNKNOWN;
|
|
3595
|
+
resolved = {
|
|
3596
|
+
baseUrl: inherited.baseUrl ?? resolved.baseUrl,
|
|
3597
|
+
paths: inherited.paths ?? resolved.paths
|
|
3598
|
+
};
|
|
3599
|
+
}
|
|
3600
|
+
if (options.baseUrl !== void 0) {
|
|
3601
|
+
resolved.baseUrl = options.baseUrl.startsWith(CONFIG_DIR) ? options.baseUrl : posix.normalize(posix.join(dir, options.baseUrl));
|
|
3602
|
+
}
|
|
3603
|
+
if (options.paths) resolved.paths = { map: options.paths, dir };
|
|
3604
|
+
return resolved;
|
|
3605
|
+
}
|
|
3606
|
+
function toMapping(resolved, configDir) {
|
|
3607
|
+
const expand = (value) => posix.normalize(posix.join(configDir, value.slice(CONFIG_DIR.length)));
|
|
3608
|
+
const baseUrl = resolved.baseUrl?.startsWith(CONFIG_DIR) ? expand(resolved.baseUrl) : resolved.baseUrl;
|
|
3609
|
+
if (!resolved.paths) return { base: baseUrl ?? ".", paths: {} };
|
|
3610
|
+
const base = baseUrl ?? resolved.paths.dir;
|
|
3611
|
+
const rebase = (value) => {
|
|
3612
|
+
if (!value.startsWith(CONFIG_DIR)) return value;
|
|
3613
|
+
const wildcard = value.endsWith("/*");
|
|
3614
|
+
const target = expand(wildcard ? value.slice(0, -2) : value);
|
|
3615
|
+
const rel = posix.relative(base, target) || ".";
|
|
3616
|
+
return wildcard ? `${rel}/*` : rel;
|
|
3617
|
+
};
|
|
3618
|
+
const paths = Object.fromEntries(Object.entries(resolved.paths.map).map(([key, values]) => [key, values.map(rebase)]));
|
|
3619
|
+
return { base, paths };
|
|
3620
|
+
}
|
|
3621
|
+
function extendsTarget(projectRoot, fromDir, specifier) {
|
|
3622
|
+
if (specifier.startsWith("./") || specifier.startsWith("../") || specifier === "." || specifier === "..") {
|
|
3623
|
+
return configFile(projectRoot, posix.join(fromDir, specifier));
|
|
3624
|
+
}
|
|
3625
|
+
if (posix.isAbsolute(specifier) || /^[A-Za-z]:[\\/]/.test(specifier)) return void 0;
|
|
3626
|
+
const inPackage = posix.join("node_modules", specifier);
|
|
3627
|
+
const direct = configFile(projectRoot, inPackage);
|
|
3628
|
+
if (direct) return direct;
|
|
3629
|
+
const manifest = containedFile(projectRoot, posix.join(inPackage, "package.json"));
|
|
3630
|
+
if (manifest) {
|
|
3631
|
+
try {
|
|
3632
|
+
const field = JSON.parse(readFileSync2(manifest, "utf8")).tsconfig;
|
|
3633
|
+
if (typeof field === "string") {
|
|
3634
|
+
const fromField = configFile(projectRoot, posix.join(inPackage, field));
|
|
3635
|
+
if (fromField) return fromField;
|
|
3636
|
+
}
|
|
3637
|
+
} catch {
|
|
3638
|
+
}
|
|
3639
|
+
}
|
|
3640
|
+
return configFile(projectRoot, posix.join(inPackage, "tsconfig.json"));
|
|
3641
|
+
}
|
|
3642
|
+
function parseConfig(projectRoot, file) {
|
|
3643
|
+
const path = containedFile(projectRoot, file);
|
|
3644
|
+
if (!path) return void 0;
|
|
3645
|
+
try {
|
|
3646
|
+
return JSON.parse(stripJsonComments(readFileSync2(path, "utf8")));
|
|
3647
|
+
} catch {
|
|
3648
|
+
return void 0;
|
|
3649
|
+
}
|
|
3650
|
+
}
|
|
3651
|
+
function configFile(projectRoot, path) {
|
|
3652
|
+
const normalized = posix.normalize(path);
|
|
3653
|
+
for (const candidate of [normalized, `${normalized}.json`]) {
|
|
3654
|
+
const absolute = containedFile(projectRoot, candidate);
|
|
3655
|
+
if (absolute && statSync(absolute).isFile()) return toPosixPath(relative4(projectRoot, absolute));
|
|
3656
|
+
}
|
|
3657
|
+
return void 0;
|
|
3658
|
+
}
|
|
3659
|
+
function referencePath(projectRoot, path) {
|
|
3660
|
+
const absolute = containedFile(projectRoot, path);
|
|
3661
|
+
if (!absolute) return void 0;
|
|
3662
|
+
const file = statSync(absolute).isDirectory() ? join4(absolute, "tsconfig.json") : absolute;
|
|
3663
|
+
const relativeFile = toPosixPath(relative4(projectRoot, file));
|
|
3664
|
+
return containedFile(projectRoot, relativeFile) ? relativeFile : void 0;
|
|
3665
|
+
}
|
|
3666
|
+
function stripJsonComments(text) {
|
|
3667
|
+
return text.replace(/("(?:\\.|[^"\\])*")|\/\*[\s\S]*?\*\/|\/\/[^\n]*/g, (match, string) => string ?? "").replace(/,(\s*[}\]])/g, "$1");
|
|
3668
|
+
}
|
|
3669
|
+
function stripExtension(path) {
|
|
3670
|
+
return path.replace(/\.(tsx|ts|jsx|js)$/, "");
|
|
3671
|
+
}
|
|
3672
|
+
|
|
3673
|
+
// ../install-core/src/config.ts
|
|
3674
|
+
async function loadConfig(cwd, overrides = {}, options = {}) {
|
|
3675
|
+
const { projectRoot, configPath: foundPath } = await locateProject(cwd, options.home);
|
|
3676
|
+
const configPath = foundPath ?? join5(projectRoot, CONFIG_FILE_NAME);
|
|
3677
|
+
if (foundPath && options.rejectEscapingSymlink) assertNoSymlinkEscape(projectRoot, foundPath);
|
|
2728
3678
|
const defaults = createDefaultConfig(projectRoot, overrides.apiUrl ?? DEFAULT_API_ORIGIN);
|
|
2729
3679
|
if (!foundPath) {
|
|
2730
3680
|
return {
|
|
@@ -2735,7 +3685,7 @@ async function loadConfig(cwd, overrides = {}) {
|
|
|
2735
3685
|
};
|
|
2736
3686
|
}
|
|
2737
3687
|
try {
|
|
2738
|
-
const parsed = JSON.parse(await
|
|
3688
|
+
const parsed = JSON.parse(await readFile2(foundPath, "utf8"));
|
|
2739
3689
|
return {
|
|
2740
3690
|
config: normalizeConfig(
|
|
2741
3691
|
{
|
|
@@ -2757,22 +3707,23 @@ async function loadConfig(cwd, overrides = {}) {
|
|
|
2757
3707
|
};
|
|
2758
3708
|
} catch (error) {
|
|
2759
3709
|
if (error instanceof SyntaxError) {
|
|
2760
|
-
throw new
|
|
3710
|
+
throw new WensityError("config_invalid", `${CONFIG_FILE_NAME} contains invalid JSON.`);
|
|
2761
3711
|
}
|
|
2762
3712
|
if (error instanceof PresetValidationError) {
|
|
2763
|
-
throw new
|
|
3713
|
+
throw new WensityError("config_invalid", `${CONFIG_FILE_NAME}: ${error.message}`);
|
|
2764
3714
|
}
|
|
2765
3715
|
throw error;
|
|
2766
3716
|
}
|
|
2767
3717
|
}
|
|
2768
3718
|
function createDefaultConfig(projectRoot, apiUrl = DEFAULT_API_ORIGIN) {
|
|
2769
3719
|
const apiOrigin = normalizeUrlOrigin(apiUrl);
|
|
3720
|
+
const sourceRoot = defaultSourceRoot(projectRoot);
|
|
2770
3721
|
return {
|
|
2771
3722
|
$schema: joinUrl(apiOrigin, "/schema/wensity.json"),
|
|
2772
3723
|
version: CONFIG_SCHEMA_VERSION,
|
|
2773
3724
|
registry: joinUrl(apiOrigin, DEFAULT_REGISTRY_PATH),
|
|
2774
|
-
components:
|
|
2775
|
-
utils:
|
|
3725
|
+
components: posix2.join(sourceRoot, "components/wensity"),
|
|
3726
|
+
utils: posix2.join(sourceRoot, "lib/utils"),
|
|
2776
3727
|
tailwind: {
|
|
2777
3728
|
config: detectTailwindConfig(projectRoot),
|
|
2778
3729
|
css: detectGlobalCss(projectRoot)
|
|
@@ -2786,20 +3737,21 @@ function createDefaultConfig(projectRoot, apiUrl = DEFAULT_API_ORIGIN) {
|
|
|
2786
3737
|
}
|
|
2787
3738
|
async function writeConfigFile(path, config, force) {
|
|
2788
3739
|
if (!force && await fileExists(path)) {
|
|
2789
|
-
throw new
|
|
3740
|
+
throw new WensityError("file_conflict", `${toPosixPath(relative5(process.cwd(), path))} already exists. Use --force to overwrite it.`);
|
|
2790
3741
|
}
|
|
2791
|
-
await
|
|
3742
|
+
await mkdir2(dirname3(path), { recursive: true });
|
|
2792
3743
|
await writeFile(path, `${JSON.stringify(config, null, 2)}
|
|
2793
3744
|
`, "utf8");
|
|
2794
3745
|
}
|
|
2795
3746
|
async function mergeStyleIntoConfigFile(configPath, style) {
|
|
2796
|
-
const raw = JSON.parse(await
|
|
3747
|
+
const raw = JSON.parse(await readFile2(configPath, "utf8"));
|
|
3748
|
+
const defaults = raw.components && raw.utils ? void 0 : createDefaultConfig(dirname3(configPath));
|
|
2797
3749
|
const merged = {
|
|
2798
3750
|
...raw,
|
|
2799
3751
|
version: raw.version ?? CONFIG_SCHEMA_VERSION,
|
|
2800
3752
|
registry: raw.registry ?? "",
|
|
2801
|
-
components: raw.components ?? DEFAULT_COMPONENTS_DIR,
|
|
2802
|
-
utils: raw.utils ?? DEFAULT_UTILS_PATH,
|
|
3753
|
+
components: raw.components ?? defaults?.components ?? DEFAULT_COMPONENTS_DIR,
|
|
3754
|
+
utils: raw.utils ?? defaults?.utils ?? DEFAULT_UTILS_PATH,
|
|
2803
3755
|
aliases: {
|
|
2804
3756
|
components: raw.aliases?.components ?? DEFAULT_COMPONENTS_ALIAS,
|
|
2805
3757
|
utils: raw.aliases?.utils ?? DEFAULT_UTILS_ALIAS
|
|
@@ -2817,7 +3769,7 @@ function resolveConfiguredFilePath(projectRoot, relativePath, extension = "ts")
|
|
|
2817
3769
|
return resolveInside(projectRoot, withExtension(relativePath, extension));
|
|
2818
3770
|
} catch (error) {
|
|
2819
3771
|
const message = error instanceof Error ? error.message : String(error);
|
|
2820
|
-
throw new
|
|
3772
|
+
throw new WensityError("path_unsafe", message);
|
|
2821
3773
|
}
|
|
2822
3774
|
}
|
|
2823
3775
|
async function fileExists(path) {
|
|
@@ -2829,16 +3781,52 @@ async function fileExists(path) {
|
|
|
2829
3781
|
throw error;
|
|
2830
3782
|
}
|
|
2831
3783
|
}
|
|
2832
|
-
|
|
2833
|
-
|
|
3784
|
+
function assertProjectRootCandidate(path, home = homedir()) {
|
|
3785
|
+
const resolved = resolve2(path);
|
|
3786
|
+
if (isFilesystemRoot(resolved)) {
|
|
3787
|
+
throw new WensityError(
|
|
3788
|
+
"project_ambiguous",
|
|
3789
|
+
`${resolved} is a filesystem root, not a project. Run Wensity from your project directory (the one with package.json).`
|
|
3790
|
+
);
|
|
3791
|
+
}
|
|
3792
|
+
if (isHomeDirectory(resolved, home)) {
|
|
3793
|
+
throw new WensityError(
|
|
3794
|
+
"project_ambiguous",
|
|
3795
|
+
"Your home directory is not a project. Run Wensity from your project directory (the one with package.json)."
|
|
3796
|
+
);
|
|
3797
|
+
}
|
|
3798
|
+
}
|
|
3799
|
+
async function locateProject(start, home = homedir()) {
|
|
3800
|
+
assertProjectRootCandidate(start, home);
|
|
3801
|
+
const origin = resolve2(start);
|
|
3802
|
+
let current = origin;
|
|
2834
3803
|
while (true) {
|
|
2835
|
-
const candidate =
|
|
2836
|
-
if (await fileExists(candidate)) return candidate;
|
|
2837
|
-
|
|
2838
|
-
|
|
3804
|
+
const candidate = join5(current, CONFIG_FILE_NAME);
|
|
3805
|
+
if (await fileExists(candidate)) return { projectRoot: current, configPath: candidate };
|
|
3806
|
+
if (await fileExists(join5(current, "package.json"))) return { projectRoot: current, configPath: void 0 };
|
|
3807
|
+
const parent = dirname3(current);
|
|
3808
|
+
if (isFilesystemRoot(parent) || isHomeDirectory(parent, home)) return { projectRoot: origin, configPath: void 0 };
|
|
2839
3809
|
current = parent;
|
|
2840
3810
|
}
|
|
2841
3811
|
}
|
|
3812
|
+
function isFilesystemRoot(path) {
|
|
3813
|
+
return dirname3(path) === path;
|
|
3814
|
+
}
|
|
3815
|
+
function isHomeDirectory(path, home) {
|
|
3816
|
+
if (!home) return false;
|
|
3817
|
+
if (resolve2(path) === resolve2(home)) return true;
|
|
3818
|
+
try {
|
|
3819
|
+
return realpathSync3(path) === realpathSync3(home);
|
|
3820
|
+
} catch {
|
|
3821
|
+
return false;
|
|
3822
|
+
}
|
|
3823
|
+
}
|
|
3824
|
+
function defaultSourceRoot(projectRoot) {
|
|
3825
|
+
const fromAlias = atAliasRoot(readPathMapping(projectRoot));
|
|
3826
|
+
if (fromAlias !== void 0) return fromAlias;
|
|
3827
|
+
const src = join5(projectRoot, "src");
|
|
3828
|
+
return existsSync2(src) && statSync2(src).isDirectory() ? "src" : "";
|
|
3829
|
+
}
|
|
2842
3830
|
function normalizeConfig(config, overrides) {
|
|
2843
3831
|
const apiOrigin = overrides.apiUrl ? normalizeUrlOrigin(overrides.apiUrl) : void 0;
|
|
2844
3832
|
try {
|
|
@@ -2858,14 +3846,14 @@ function normalizeConfig(config, overrides) {
|
|
|
2858
3846
|
};
|
|
2859
3847
|
} catch (error) {
|
|
2860
3848
|
if (error instanceof PresetValidationError) {
|
|
2861
|
-
throw new
|
|
3849
|
+
throw new WensityError("config_invalid", `${CONFIG_FILE_NAME}: ${error.message}`);
|
|
2862
3850
|
}
|
|
2863
3851
|
throw error;
|
|
2864
3852
|
}
|
|
2865
3853
|
}
|
|
2866
3854
|
function detectTailwindConfig(projectRoot) {
|
|
2867
3855
|
const candidates = ["tailwind.config.ts", "tailwind.config.js", "tailwind.config.mjs"];
|
|
2868
|
-
return candidates.find((candidate) =>
|
|
3856
|
+
return candidates.find((candidate) => existsSync2(join5(projectRoot, candidate))) ?? "tailwind.config.ts";
|
|
2869
3857
|
}
|
|
2870
3858
|
function detectGlobalCss(projectRoot) {
|
|
2871
3859
|
const candidates = [
|
|
@@ -2875,230 +3863,356 @@ function detectGlobalCss(projectRoot) {
|
|
|
2875
3863
|
"src/index.css",
|
|
2876
3864
|
"styles/globals.css"
|
|
2877
3865
|
];
|
|
2878
|
-
return candidates.find((candidate) =>
|
|
3866
|
+
return candidates.find((candidate) => existsSync2(join5(projectRoot, candidate))) ?? "src/app/globals.css";
|
|
2879
3867
|
}
|
|
2880
3868
|
function withExtension(path, extension) {
|
|
2881
3869
|
if (/\.(ts|tsx|js|jsx)$/.test(path)) return path;
|
|
2882
3870
|
return `${path}.${extension}`;
|
|
2883
3871
|
}
|
|
2884
3872
|
|
|
2885
|
-
// src/
|
|
2886
|
-
import {
|
|
2887
|
-
import {
|
|
2888
|
-
import {
|
|
2889
|
-
|
|
2890
|
-
function
|
|
2891
|
-
if (
|
|
2892
|
-
const
|
|
2893
|
-
|
|
3873
|
+
// ../install-core/src/package-manager.ts
|
|
3874
|
+
import { accessSync, constants as fsConstants2, existsSync as existsSync3, readFileSync as readFileSync3, statSync as statSync3, writeFileSync } from "fs";
|
|
3875
|
+
import { spawn, spawnSync } from "child_process";
|
|
3876
|
+
import { join as join6, posix as posix3, win32 } from "path";
|
|
3877
|
+
var PACKAGE_MANAGERS = ["npm", "pnpm", "yarn", "bun"];
|
|
3878
|
+
function parsePackageManager(value) {
|
|
3879
|
+
if (!value) return void 0;
|
|
3880
|
+
const normalized = value.trim().toLowerCase();
|
|
3881
|
+
if (isPackageManager(normalized)) return normalized;
|
|
3882
|
+
throw new WensityError(
|
|
3883
|
+
"unsupported_package_manager",
|
|
3884
|
+
`Unsupported package manager "${value}". Use one of: ${PACKAGE_MANAGERS.join(", ")}.`
|
|
3885
|
+
);
|
|
2894
3886
|
}
|
|
2895
|
-
|
|
2896
|
-
const
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
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
|
-
}
|
|
3887
|
+
function packageManagerFromUserAgent(env = process.env) {
|
|
3888
|
+
const agent = env.npm_config_user_agent;
|
|
3889
|
+
if (!agent) return void 0;
|
|
3890
|
+
const first = agent.trim().split(/\s+/)[0] ?? "";
|
|
3891
|
+
const name = first.split("/")[0]?.toLowerCase();
|
|
3892
|
+
return name && isPackageManager(name) ? name : void 0;
|
|
2911
3893
|
}
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
`, {
|
|
2919
|
-
encoding: "utf8",
|
|
2920
|
-
mode: 384
|
|
2921
|
-
});
|
|
2922
|
-
await rename(temporaryPath, path);
|
|
2923
|
-
await chmod(path, 384);
|
|
2924
|
-
return path;
|
|
3894
|
+
function detectPackageManager(projectRoot, env = process.env) {
|
|
3895
|
+
if (existsSync3(join6(projectRoot, "pnpm-lock.yaml"))) return "pnpm";
|
|
3896
|
+
if (existsSync3(join6(projectRoot, "bun.lockb")) || existsSync3(join6(projectRoot, "bun.lock"))) return "bun";
|
|
3897
|
+
if (existsSync3(join6(projectRoot, "yarn.lock"))) return "yarn";
|
|
3898
|
+
if (existsSync3(join6(projectRoot, "package-lock.json"))) return "npm";
|
|
3899
|
+
return packageManagerFromUserAgent(env) ?? "npm";
|
|
2925
3900
|
}
|
|
2926
|
-
|
|
2927
|
-
|
|
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
|
-
}
|
|
3901
|
+
function isPackageManager(value) {
|
|
3902
|
+
return PACKAGE_MANAGERS.includes(value);
|
|
2935
3903
|
}
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
3904
|
+
function missingDependencies(projectRoot, dependencies) {
|
|
3905
|
+
const installed = readInstalledPackageNames(projectRoot);
|
|
3906
|
+
if (!installed) return dependencies;
|
|
3907
|
+
return dependencies.filter((dependency) => !installed.has(dependency));
|
|
3908
|
+
}
|
|
3909
|
+
function readInstalledPackageNames(projectRoot) {
|
|
3910
|
+
const packageJsonPath = join6(projectRoot, "package.json");
|
|
3911
|
+
if (!existsSync3(packageJsonPath)) return void 0;
|
|
3912
|
+
const packageJson2 = JSON.parse(readFileSync3(packageJsonPath, "utf8"));
|
|
3913
|
+
return new Set(
|
|
3914
|
+
Object.keys({
|
|
3915
|
+
...packageJson2.dependencies,
|
|
3916
|
+
...packageJson2.devDependencies,
|
|
3917
|
+
...packageJson2.peerDependencies,
|
|
3918
|
+
...packageJson2.optionalDependencies
|
|
3919
|
+
})
|
|
3920
|
+
);
|
|
3921
|
+
}
|
|
3922
|
+
var YARN_CONFIG_FILE = ".yarnrc.yml";
|
|
3923
|
+
function prepareProjectPackageManager(projectRoot, options = {}) {
|
|
3924
|
+
if (resolvePackageManager(projectRoot, options) !== "yarn") return [];
|
|
3925
|
+
const configPath = join6(projectRoot, YARN_CONFIG_FILE);
|
|
3926
|
+
if (existsSync3(configPath)) return [];
|
|
3927
|
+
if (!options.dryRun) writeFileSync(configPath, "nodeLinker: node-modules\n", "utf8");
|
|
3928
|
+
return [YARN_CONFIG_FILE];
|
|
3929
|
+
}
|
|
3930
|
+
async function runProjectInstall(projectRoot, options = {}) {
|
|
3931
|
+
const packageJsonPath = join6(projectRoot, "package.json");
|
|
3932
|
+
if (!existsSync3(packageJsonPath)) return;
|
|
3933
|
+
const packageManager = resolvePackageManager(projectRoot, options);
|
|
3934
|
+
await runPackageManager(projectRoot, packageManager, ["install"], options);
|
|
3935
|
+
}
|
|
3936
|
+
function resolvePackageManager(projectRoot, options = {}) {
|
|
3937
|
+
return options.packageManager ?? detectPackageManager(projectRoot, options.env);
|
|
3938
|
+
}
|
|
3939
|
+
async function runInstall(projectRoot, dependencies, options = {}) {
|
|
3940
|
+
if (dependencies.length === 0) return;
|
|
3941
|
+
const packageJsonPath = join6(projectRoot, "package.json");
|
|
3942
|
+
if (!existsSync3(packageJsonPath)) {
|
|
3943
|
+
(options.onWarning ?? writeWarning)(
|
|
3944
|
+
`No package.json found. Install these dependencies manually: ${dependencies.join(" ")}`
|
|
3945
|
+
);
|
|
3946
|
+
return;
|
|
2943
3947
|
}
|
|
2944
|
-
|
|
2945
|
-
|
|
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
|
-
});
|
|
3948
|
+
const packageManager = resolvePackageManager(projectRoot, options);
|
|
3949
|
+
await runPackageManager(projectRoot, packageManager, installArgs(packageManager, dependencies), options);
|
|
2980
3950
|
}
|
|
2981
|
-
function
|
|
2982
|
-
|
|
2983
|
-
|
|
3951
|
+
async function runPackageManager(projectRoot, packageManager, args, options) {
|
|
3952
|
+
const outcome = await runPackageManagerCommand(projectRoot, packageManager, args, {
|
|
3953
|
+
output: "inherit",
|
|
3954
|
+
forwardSignals: true,
|
|
3955
|
+
...options.spawnImpl ? { spawnImpl: options.spawnImpl } : {}
|
|
3956
|
+
});
|
|
3957
|
+
throwForFailure(packageManager, args, outcome);
|
|
2984
3958
|
}
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
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]);
|
|
3959
|
+
function throwForFailure(packageManager, args, outcome) {
|
|
3960
|
+
if (outcome.spawnError) {
|
|
3961
|
+
throw new WensityError("dependency_install_failed", `Could not run ${packageManager}: ${outcome.spawnError}`);
|
|
3010
3962
|
}
|
|
3011
|
-
if (
|
|
3012
|
-
|
|
3963
|
+
if (outcome.timedOut) {
|
|
3964
|
+
throw new WensityError(
|
|
3965
|
+
"dependency_install_failed",
|
|
3966
|
+
`${packageManager} ${args.join(" ")} did not finish within ${PACKAGE_MANAGER_TIMEOUT_MS / 1e3} s and was stopped.`
|
|
3967
|
+
);
|
|
3013
3968
|
}
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
return { packages: [], importLines: [], block: null };
|
|
3969
|
+
if (outcome.exitCode !== 0) {
|
|
3970
|
+
throw new WensityError("dependency_install_failed", `${packageManager} ${args.join(" ")} failed with exit code ${outcome.exitCode ?? 1}.`);
|
|
3017
3971
|
}
|
|
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
3972
|
}
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3973
|
+
var PACKAGE_MANAGER_TIMEOUT_MS = 18e4;
|
|
3974
|
+
var PACKAGE_NAME = /^(@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/;
|
|
3975
|
+
var SUBCOMMANDS = /* @__PURE__ */ new Set(["add", "install"]);
|
|
3976
|
+
var WINDOWS_UNSAFE = /["%!^&|<>()\r\n\t\u0000-\u001f\u007f]/;
|
|
3977
|
+
function assertValidPackageName(name) {
|
|
3978
|
+
if (typeof name !== "string" || name.length > 214 || !PACKAGE_NAME.test(name) || /(^|\/|^@)[-._]/.test(name)) {
|
|
3979
|
+
throw new WensityError("dependency_install_failed", `Refusing to install an invalid package name: ${JSON.stringify(String(name).slice(0, 80))}`);
|
|
3980
|
+
}
|
|
3981
|
+
}
|
|
3982
|
+
var WINDOWS_JS_ENTRIES = {
|
|
3983
|
+
npm: ["node_modules/npm/bin/npm-cli.js"],
|
|
3984
|
+
pnpm: ["node_modules/pnpm/bin/pnpm.cjs", "node_modules/pnpm/bin/pnpm.mjs"],
|
|
3985
|
+
yarn: ["node_modules/yarn/bin/yarn.js"],
|
|
3986
|
+
bun: []
|
|
3987
|
+
};
|
|
3988
|
+
function buildPackageManagerInvocation(packageManager, args, options = {}) {
|
|
3989
|
+
if (!PACKAGE_MANAGERS.includes(packageManager)) parsePackageManager(packageManager);
|
|
3990
|
+
const [subcommand, ...names] = args;
|
|
3991
|
+
if (!subcommand || !SUBCOMMANDS.has(subcommand)) {
|
|
3992
|
+
throw new WensityError("dependency_install_failed", `Refusing to run ${packageManager} with an unexpected subcommand.`);
|
|
3993
|
+
}
|
|
3994
|
+
for (const name of names) assertValidPackageName(name);
|
|
3995
|
+
const platform = options.platform ?? process.platform;
|
|
3996
|
+
const env = options.env ?? process.env;
|
|
3997
|
+
const display = [packageManager, ...args].join(" ");
|
|
3998
|
+
const isFile = options.isFile ?? defaultIsFile;
|
|
3999
|
+
if (platform !== "win32") {
|
|
4000
|
+
const isExecutable = options.isExecutable ?? defaultIsExecutable;
|
|
4001
|
+
const dirs = (env.PATH ?? "").split(posix3.delimiter).filter(Boolean);
|
|
4002
|
+
const resolved = dirs.map((dir) => posix3.join(dir, packageManager)).find(isExecutable);
|
|
4003
|
+
if (!resolved) throw notFound(packageManager);
|
|
4004
|
+
return { shape: "posix", command: resolved, args, windowsVerbatimArguments: false, display };
|
|
4005
|
+
}
|
|
4006
|
+
const pathValue = env.PATH ?? env.Path ?? Object.entries(env).find(([key]) => key.toLowerCase() === "path")?.[1] ?? "";
|
|
4007
|
+
const extensions = (env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").map((ext) => ext.toLowerCase()).filter(Boolean);
|
|
4008
|
+
for (const dir of pathValue.split(win32.delimiter).filter(Boolean)) {
|
|
4009
|
+
for (const extension of extensions) {
|
|
4010
|
+
const candidate = win32.join(dir, `${packageManager}${extension}`);
|
|
4011
|
+
if (!isFile(candidate)) continue;
|
|
4012
|
+
if (extension === ".exe" || extension === ".com") {
|
|
4013
|
+
return { shape: "windows-exe", command: candidate, args, windowsVerbatimArguments: false, display };
|
|
4014
|
+
}
|
|
4015
|
+
if (extension !== ".cmd" && extension !== ".bat") continue;
|
|
4016
|
+
const entry = WINDOWS_JS_ENTRIES[packageManager].map((relative9) => win32.join(dir, relative9)).find(isFile);
|
|
4017
|
+
if (entry) {
|
|
4018
|
+
return { shape: "windows-node", command: options.nodePath ?? process.execPath, args: [entry, ...args], windowsVerbatimArguments: false, display };
|
|
4019
|
+
}
|
|
4020
|
+
const cmd = resolveCmdExe(env);
|
|
4021
|
+
return {
|
|
4022
|
+
shape: "windows-cmd",
|
|
4023
|
+
command: cmd,
|
|
4024
|
+
args: ["/d", "/s", "/c", `"${quoteWindowsCommandLine([candidate, ...args])}"`],
|
|
4025
|
+
windowsVerbatimArguments: true,
|
|
4026
|
+
display
|
|
4027
|
+
};
|
|
3040
4028
|
}
|
|
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
4029
|
}
|
|
3050
|
-
|
|
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");
|
|
4030
|
+
throw notFound(packageManager);
|
|
3062
4031
|
}
|
|
3063
|
-
function
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
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).`;
|
|
4032
|
+
function quoteWindowsCommandLine(values) {
|
|
4033
|
+
return values.map((value) => {
|
|
4034
|
+
if (value.length === 0 || WINDOWS_UNSAFE.test(value)) {
|
|
4035
|
+
throw new WensityError("dependency_install_failed", `Refusing to build a command line from an unsafe value: ${JSON.stringify(value.slice(0, 80))}`);
|
|
4036
|
+
}
|
|
4037
|
+
return /\s/.test(value) ? `"${value}"` : value;
|
|
4038
|
+
}).join(" ");
|
|
3074
4039
|
}
|
|
3075
|
-
function
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
4040
|
+
function resolveCmdExe(env) {
|
|
4041
|
+
const fromEnv = env.ComSpec ?? env.COMSPEC;
|
|
4042
|
+
if (fromEnv && win32.isAbsolute(fromEnv) && win32.basename(fromEnv).toLowerCase() === "cmd.exe" && !WINDOWS_UNSAFE.test(fromEnv)) {
|
|
4043
|
+
return fromEnv;
|
|
4044
|
+
}
|
|
4045
|
+
const systemRoot = env.SystemRoot ?? env.SYSTEMROOT ?? "C:\\Windows";
|
|
4046
|
+
if (!win32.isAbsolute(systemRoot) || WINDOWS_UNSAFE.test(systemRoot)) {
|
|
4047
|
+
throw new WensityError("dependency_install_failed", "Could not locate cmd.exe safely.");
|
|
4048
|
+
}
|
|
4049
|
+
return win32.join(systemRoot, "System32", "cmd.exe");
|
|
4050
|
+
}
|
|
4051
|
+
function notFound(packageManager) {
|
|
4052
|
+
return new WensityError("dependency_install_failed", `Could not run ${packageManager}: it was not found on PATH.`);
|
|
4053
|
+
}
|
|
4054
|
+
function defaultIsExecutable(path) {
|
|
4055
|
+
try {
|
|
4056
|
+
accessSync(path, fsConstants2.X_OK);
|
|
4057
|
+
return statSync3(path).isFile();
|
|
4058
|
+
} catch {
|
|
4059
|
+
return false;
|
|
4060
|
+
}
|
|
4061
|
+
}
|
|
4062
|
+
function defaultIsFile(path) {
|
|
4063
|
+
try {
|
|
4064
|
+
return statSync3(path).isFile();
|
|
4065
|
+
} catch {
|
|
4066
|
+
return false;
|
|
4067
|
+
}
|
|
4068
|
+
}
|
|
4069
|
+
var OUTPUT_KEEP = 256 * 1024;
|
|
4070
|
+
var TREE_EXIT_WAIT_MS = 5e3;
|
|
4071
|
+
function runPackageManagerCommand(projectRoot, packageManager, args, options = {}) {
|
|
4072
|
+
const invocation = buildPackageManagerInvocation(packageManager, args, options);
|
|
4073
|
+
const platform = options.platform ?? process.platform;
|
|
4074
|
+
const inherit = options.output === "inherit";
|
|
4075
|
+
const ownGroup = !inherit && platform !== "win32";
|
|
4076
|
+
const timeoutMs = options.timeoutMs ?? (inherit ? void 0 : PACKAGE_MANAGER_TIMEOUT_MS);
|
|
4077
|
+
const started = Date.now();
|
|
4078
|
+
return new Promise((resolve3) => {
|
|
4079
|
+
let stdout2 = "";
|
|
4080
|
+
let stderr = "";
|
|
4081
|
+
let timedOut = false;
|
|
4082
|
+
const child = (options.spawnImpl ?? spawn)(invocation.command, invocation.args, {
|
|
4083
|
+
cwd: projectRoot,
|
|
4084
|
+
env: options.spawnEnv ?? process.env,
|
|
4085
|
+
shell: false,
|
|
4086
|
+
stdio: inherit ? "inherit" : ["ignore", "pipe", "pipe"],
|
|
4087
|
+
windowsHide: true,
|
|
4088
|
+
windowsVerbatimArguments: invocation.windowsVerbatimArguments,
|
|
4089
|
+
detached: ownGroup
|
|
4090
|
+
});
|
|
4091
|
+
const killTree = () => killProcessTree(child.pid, platform, options.env ?? process.env, spawnSync, ownGroup);
|
|
4092
|
+
const signalHandlers = [];
|
|
4093
|
+
if (options.forwardSignals) {
|
|
4094
|
+
for (const signal of ["SIGINT", "SIGTERM"]) {
|
|
4095
|
+
const handler = () => {
|
|
4096
|
+
killTree();
|
|
4097
|
+
removeSignalHandlers();
|
|
4098
|
+
process.kill(process.pid, signal);
|
|
4099
|
+
};
|
|
4100
|
+
signalHandlers.push([signal, handler]);
|
|
4101
|
+
process.once(signal, handler);
|
|
4102
|
+
}
|
|
3086
4103
|
}
|
|
3087
|
-
|
|
3088
|
-
const
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
4104
|
+
const removeSignalHandlers = () => {
|
|
4105
|
+
for (const [signal, handler] of signalHandlers.splice(0)) process.removeListener(signal, handler);
|
|
4106
|
+
};
|
|
4107
|
+
const timer = timeoutMs === void 0 ? void 0 : setTimeout(() => {
|
|
4108
|
+
timedOut = true;
|
|
4109
|
+
killTree();
|
|
4110
|
+
}, timeoutMs);
|
|
4111
|
+
child.stdout?.on("data", (chunk) => {
|
|
4112
|
+
stdout2 = (stdout2 + chunk.toString("utf8")).slice(-OUTPUT_KEEP);
|
|
4113
|
+
});
|
|
4114
|
+
child.stderr?.on("data", (chunk) => {
|
|
4115
|
+
stderr = (stderr + chunk.toString("utf8")).slice(-OUTPUT_KEEP);
|
|
4116
|
+
});
|
|
4117
|
+
let settled = false;
|
|
4118
|
+
const finish = async (exitCode, spawnError) => {
|
|
4119
|
+
if (settled) return;
|
|
4120
|
+
settled = true;
|
|
4121
|
+
clearTimeout(timer);
|
|
4122
|
+
if (ownGroup && child.pid !== void 0 && groupAlive(child.pid)) {
|
|
4123
|
+
killTree();
|
|
4124
|
+
await waitForGroupExit(child.pid);
|
|
4125
|
+
}
|
|
4126
|
+
removeSignalHandlers();
|
|
4127
|
+
child.stdout?.destroy();
|
|
4128
|
+
child.stderr?.destroy();
|
|
4129
|
+
resolve3({ invocation, exitCode, stdout: stdout2, stderr, durationMs: Date.now() - started, timedOut, ...spawnError ? { spawnError } : {} });
|
|
4130
|
+
};
|
|
4131
|
+
child.on("error", (error) => void finish(null, error.message));
|
|
4132
|
+
child.on("exit", (code) => void finish(code));
|
|
4133
|
+
});
|
|
4134
|
+
}
|
|
4135
|
+
function killProcessTree(pid, platform, env, run2 = spawnSync, ownGroup = true) {
|
|
4136
|
+
if (pid === void 0) return;
|
|
4137
|
+
try {
|
|
4138
|
+
if (platform !== "win32" && !ownGroup) {
|
|
4139
|
+
for (const member of [...descendantsOf(pid, run2).reverse(), pid]) {
|
|
4140
|
+
try {
|
|
4141
|
+
process.kill(member, "SIGKILL");
|
|
4142
|
+
} catch {
|
|
4143
|
+
}
|
|
4144
|
+
}
|
|
4145
|
+
return;
|
|
3092
4146
|
}
|
|
3093
|
-
if (
|
|
3094
|
-
const
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
4147
|
+
if (platform === "win32") {
|
|
4148
|
+
const systemRoot = env.SystemRoot ?? env.SYSTEMROOT ?? "C:\\Windows";
|
|
4149
|
+
run2(win32.join(systemRoot, "System32", "taskkill.exe"), ["/PID", String(pid), "/T", "/F"], { shell: false, stdio: "ignore", windowsHide: true });
|
|
4150
|
+
} else {
|
|
4151
|
+
process.kill(-pid, "SIGKILL");
|
|
3098
4152
|
}
|
|
3099
|
-
|
|
4153
|
+
} catch {
|
|
3100
4154
|
}
|
|
3101
|
-
|
|
4155
|
+
}
|
|
4156
|
+
function descendantsOf(pid, run2) {
|
|
4157
|
+
const listing = run2("ps", ["-A", "-o", "pid=,ppid="], { shell: false, encoding: "utf8" });
|
|
4158
|
+
const children = /* @__PURE__ */ new Map();
|
|
4159
|
+
for (const line of String(listing.stdout ?? "").split("\n")) {
|
|
4160
|
+
const [child, parent] = line.trim().split(/\s+/).map(Number);
|
|
4161
|
+
if (!child || !parent) continue;
|
|
4162
|
+
children.set(parent, [...children.get(parent) ?? [], child]);
|
|
4163
|
+
}
|
|
4164
|
+
const found = [];
|
|
4165
|
+
const queue = [...children.get(pid) ?? []];
|
|
4166
|
+
while (queue.length > 0) {
|
|
4167
|
+
const next = queue.shift();
|
|
4168
|
+
found.push(next);
|
|
4169
|
+
queue.push(...children.get(next) ?? []);
|
|
4170
|
+
}
|
|
4171
|
+
return found;
|
|
4172
|
+
}
|
|
4173
|
+
function groupAlive(pid) {
|
|
4174
|
+
try {
|
|
4175
|
+
process.kill(-pid, 0);
|
|
4176
|
+
return true;
|
|
4177
|
+
} catch (error) {
|
|
4178
|
+
return error.code === "EPERM";
|
|
4179
|
+
}
|
|
4180
|
+
}
|
|
4181
|
+
async function waitForGroupExit(pid) {
|
|
4182
|
+
const deadline = Date.now() + TREE_EXIT_WAIT_MS;
|
|
4183
|
+
while (groupAlive(pid) && Date.now() < deadline) {
|
|
4184
|
+
await new Promise((resolve3) => setTimeout(resolve3, 20));
|
|
4185
|
+
}
|
|
4186
|
+
}
|
|
4187
|
+
function installArgs(packageManager, dependencies) {
|
|
4188
|
+
switch (packageManager) {
|
|
4189
|
+
case "pnpm":
|
|
4190
|
+
return ["add", ...dependencies];
|
|
4191
|
+
case "yarn":
|
|
4192
|
+
return ["add", ...dependencies];
|
|
4193
|
+
case "bun":
|
|
4194
|
+
return ["add", ...dependencies];
|
|
4195
|
+
case "npm":
|
|
4196
|
+
return ["install", ...dependencies];
|
|
4197
|
+
}
|
|
4198
|
+
}
|
|
4199
|
+
function writeWarning(message) {
|
|
4200
|
+
process.stderr.write(`Warning: ${message}
|
|
4201
|
+
`);
|
|
4202
|
+
}
|
|
4203
|
+
|
|
4204
|
+
// ../install-core/src/project.ts
|
|
4205
|
+
var IMPORT_ALIAS_REMEDY = 'Add a compilerOptions.paths entry such as "@/*": ["./*"] (or ["./src/*"]) to tsconfig.json, or set components, utils and aliases in wensity.json so each alias maps to its folder.';
|
|
4206
|
+
function importAliasesResolve(projectRoot, config) {
|
|
4207
|
+
const mapping = readPathMapping(projectRoot);
|
|
4208
|
+
if (!mapping) return null;
|
|
4209
|
+
return aliasResolvesTo(mapping, config.aliases.components, config.components) && aliasResolvesTo(mapping, config.aliases.utils, config.utils);
|
|
4210
|
+
}
|
|
4211
|
+
function importAliasMismatchMessage(config) {
|
|
4212
|
+
return `Installed files would import ${config.aliases.components} and ${config.aliases.utils}, but the tsconfig/jsconfig paths do not map them to ${config.components} and ${config.utils}, so the imports would not resolve.`;
|
|
4213
|
+
}
|
|
4214
|
+
function importAliasBlockMessage(config) {
|
|
4215
|
+
return `${importAliasMismatchMessage(config)} ${IMPORT_ALIAS_REMEDY}`;
|
|
3102
4216
|
}
|
|
3103
4217
|
|
|
3104
4218
|
// src/output.ts
|
|
@@ -3272,54 +4386,334 @@ function centerLine(line) {
|
|
|
3272
4386
|
return `${" ".repeat(padding)}${line}`;
|
|
3273
4387
|
}
|
|
3274
4388
|
|
|
3275
|
-
// src/
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
4389
|
+
// src/errors.ts
|
|
4390
|
+
var PRICING_URL = "https://ui.wensity.com/pricing";
|
|
4391
|
+
var CliError = class _CliError extends Error {
|
|
4392
|
+
exitCode;
|
|
4393
|
+
/** The install-core error this one renders, when there is one. */
|
|
4394
|
+
wensityError;
|
|
4395
|
+
constructor(message, exitCode = 1, options = {}) {
|
|
4396
|
+
super(message, options.cause ? { cause: options.cause } : void 0);
|
|
4397
|
+
this.name = "CliError";
|
|
4398
|
+
this.exitCode = exitCode;
|
|
4399
|
+
if (options.cause) this.wensityError = options.cause;
|
|
4400
|
+
}
|
|
4401
|
+
/**
|
|
4402
|
+
* install-core throws WensityError wherever this code used to throw CliError,
|
|
4403
|
+
* so `instanceof CliError` keeps meaning "a user-facing CLI failure" for both.
|
|
4404
|
+
* Render one with `toCliError` to get the exit code and CLI wording.
|
|
4405
|
+
*/
|
|
4406
|
+
static [Symbol.hasInstance](value) {
|
|
4407
|
+
return isWensityError(value) || Function.prototype[Symbol.hasInstance].call(_CliError, value);
|
|
4408
|
+
}
|
|
4409
|
+
static fromWensityError(error) {
|
|
4410
|
+
return new _CliError(renderWensityErrorMessage(error), 1, { cause: error });
|
|
4411
|
+
}
|
|
4412
|
+
};
|
|
4413
|
+
function renderWensityErrorMessage(error) {
|
|
4414
|
+
if (error.code === "entitlement_required") {
|
|
4415
|
+
return `${error.message}
|
|
4416
|
+
Upgrade here: ${pricingBlue(PRICING_URL)}`;
|
|
4417
|
+
}
|
|
4418
|
+
return error.message;
|
|
4419
|
+
}
|
|
4420
|
+
function toCliError(error) {
|
|
4421
|
+
if (isWensityError(error)) return CliError.fromWensityError(error);
|
|
4422
|
+
return error instanceof CliError ? error : void 0;
|
|
4423
|
+
}
|
|
4424
|
+
|
|
4425
|
+
// src/args.ts
|
|
4426
|
+
function parseArgs(argv) {
|
|
4427
|
+
const positionals = [];
|
|
4428
|
+
const options = /* @__PURE__ */ new Map();
|
|
4429
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
4430
|
+
const token = argv[index];
|
|
4431
|
+
if (!token.startsWith("--")) {
|
|
4432
|
+
positionals.push(token);
|
|
4433
|
+
continue;
|
|
4434
|
+
}
|
|
4435
|
+
if (token === "--") {
|
|
4436
|
+
positionals.push(...argv.slice(index + 1));
|
|
4437
|
+
break;
|
|
4438
|
+
}
|
|
4439
|
+
const inlineValueIndex = token.indexOf("=");
|
|
4440
|
+
const rawName = inlineValueIndex > -1 ? token.slice(2, inlineValueIndex) : token.slice(2);
|
|
4441
|
+
const name = normalizeOptionName(rawName);
|
|
4442
|
+
if (!name) {
|
|
4443
|
+
throw new CliError(`Invalid option: ${token}`);
|
|
4444
|
+
}
|
|
4445
|
+
if (inlineValueIndex > -1) {
|
|
4446
|
+
options.set(name, token.slice(inlineValueIndex + 1));
|
|
4447
|
+
continue;
|
|
4448
|
+
}
|
|
4449
|
+
const nextToken = argv[index + 1];
|
|
4450
|
+
if (nextToken && !nextToken.startsWith("--") && expectsValue(name)) {
|
|
4451
|
+
options.set(name, nextToken);
|
|
4452
|
+
index += 1;
|
|
4453
|
+
continue;
|
|
4454
|
+
}
|
|
4455
|
+
options.set(name, true);
|
|
4456
|
+
}
|
|
4457
|
+
return { positionals, options };
|
|
4458
|
+
}
|
|
4459
|
+
function getStringOption(args, name) {
|
|
4460
|
+
const value = args.options.get(name);
|
|
4461
|
+
if (value === void 0) return void 0;
|
|
4462
|
+
if (typeof value === "boolean") {
|
|
4463
|
+
throw new CliError(`--${name} expects a value.`);
|
|
4464
|
+
}
|
|
4465
|
+
return value;
|
|
4466
|
+
}
|
|
4467
|
+
function hasFlag(args, name) {
|
|
4468
|
+
return args.options.get(name) === true;
|
|
4469
|
+
}
|
|
4470
|
+
function expectsValue(name) {
|
|
4471
|
+
return [
|
|
4472
|
+
"api-url",
|
|
4473
|
+
"registry",
|
|
4474
|
+
"token",
|
|
4475
|
+
"type",
|
|
4476
|
+
"preset",
|
|
4477
|
+
"preset-file",
|
|
4478
|
+
"template",
|
|
4479
|
+
"package-manager"
|
|
4480
|
+
].includes(name);
|
|
4481
|
+
}
|
|
4482
|
+
function normalizeOptionName(name) {
|
|
4483
|
+
if (name === "dry") return "dry-run";
|
|
4484
|
+
return name;
|
|
4485
|
+
}
|
|
4486
|
+
|
|
4487
|
+
// src/credentials.ts
|
|
4488
|
+
import { stdin, stdout } from "process";
|
|
4489
|
+
|
|
4490
|
+
// ../install-core/src/credentials.ts
|
|
4491
|
+
import { mkdir as mkdir3, readFile as readFile4, rename as rename2, rm as rm2, writeFile as writeFile2, chmod as chmod2 } from "fs/promises";
|
|
4492
|
+
import { dirname as dirname4, join as join8 } from "path";
|
|
4493
|
+
import { randomUUID } from "crypto";
|
|
4494
|
+
function credentialsPath(env) {
|
|
4495
|
+
if (env.WENSITY_CREDENTIALS_FILE) return env.WENSITY_CREDENTIALS_FILE;
|
|
4496
|
+
const configHome = env.WENSITY_CONFIG_HOME ?? env.XDG_CONFIG_HOME ?? join8(requiredHome(env), ".config");
|
|
4497
|
+
return join8(configHome, "wensity", "credentials.json");
|
|
4498
|
+
}
|
|
4499
|
+
async function readCredentials(env) {
|
|
4500
|
+
const path = credentialsPath(env);
|
|
4501
|
+
try {
|
|
4502
|
+
const content = await readFile4(path, "utf8");
|
|
4503
|
+
const parsed = JSON.parse(content);
|
|
4504
|
+
if (parsed.version !== 1 || !parsed.token || !parsed.apiBaseUrl) {
|
|
4505
|
+
throw new WensityError("auth_invalid", `Credentials at ${path} are not valid. Run wensity login again.`);
|
|
4506
|
+
}
|
|
4507
|
+
return parsed;
|
|
4508
|
+
} catch (error) {
|
|
4509
|
+
if (isNodeError(error) && error.code === "ENOENT") return void 0;
|
|
4510
|
+
if (error instanceof SyntaxError) {
|
|
4511
|
+
throw new WensityError("auth_invalid", `Credentials at ${path} are not valid JSON. Run wensity login again.`);
|
|
4512
|
+
}
|
|
4513
|
+
throw error;
|
|
4514
|
+
}
|
|
4515
|
+
}
|
|
4516
|
+
async function writeCredentials(env, credentials) {
|
|
4517
|
+
const path = credentialsPath(env);
|
|
4518
|
+
const directory = dirname4(path);
|
|
4519
|
+
const temporaryPath = join8(directory, `.credentials.${randomUUID()}.tmp`);
|
|
4520
|
+
await mkdir3(directory, { recursive: true, mode: 448 });
|
|
4521
|
+
await writeFile2(temporaryPath, `${JSON.stringify(credentials, null, 2)}
|
|
4522
|
+
`, {
|
|
4523
|
+
encoding: "utf8",
|
|
4524
|
+
mode: 384
|
|
4525
|
+
});
|
|
4526
|
+
await rename2(temporaryPath, path);
|
|
4527
|
+
await chmod2(path, 384);
|
|
4528
|
+
return path;
|
|
4529
|
+
}
|
|
4530
|
+
async function deleteCredentials(env) {
|
|
4531
|
+
const path = credentialsPath(env);
|
|
4532
|
+
try {
|
|
4533
|
+
await rm2(path, { force: false });
|
|
4534
|
+
return true;
|
|
4535
|
+
} catch (error) {
|
|
4536
|
+
if (isNodeError(error) && error.code === "ENOENT") return false;
|
|
4537
|
+
throw error;
|
|
4538
|
+
}
|
|
4539
|
+
}
|
|
4540
|
+
function requiredHome(env) {
|
|
4541
|
+
if (env.HOME) return env.HOME;
|
|
4542
|
+
throw new WensityError("config_missing", "HOME is not set, so the CLI cannot locate ~/.config/wensity.");
|
|
4543
|
+
}
|
|
4544
|
+
|
|
4545
|
+
// src/credentials.ts
|
|
4546
|
+
async function readTokenInteractively(prompt = "Wensity API token: ") {
|
|
4547
|
+
if (!stdin.isTTY) {
|
|
4548
|
+
const chunks = [];
|
|
4549
|
+
for await (const chunk of stdin) {
|
|
4550
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
4551
|
+
}
|
|
4552
|
+
return Buffer.concat(chunks).toString("utf8").trim();
|
|
4553
|
+
}
|
|
4554
|
+
return new Promise((resolveToken, rejectToken) => {
|
|
4555
|
+
const input = stdin;
|
|
4556
|
+
const previousRawMode = input.isRaw;
|
|
4557
|
+
let token = "";
|
|
4558
|
+
function cleanup() {
|
|
4559
|
+
input.off("data", onData);
|
|
4560
|
+
if (typeof previousRawMode === "boolean") {
|
|
4561
|
+
input.setRawMode(previousRawMode);
|
|
4562
|
+
}
|
|
4563
|
+
input.pause();
|
|
4564
|
+
}
|
|
4565
|
+
function onData(chunk) {
|
|
4566
|
+
const value = chunk.toString("utf8");
|
|
4567
|
+
if (value === "") {
|
|
4568
|
+
cleanup();
|
|
4569
|
+
stdout.write("\n");
|
|
4570
|
+
rejectToken(new CliError("Login cancelled.", 130));
|
|
4571
|
+
return;
|
|
4572
|
+
}
|
|
4573
|
+
if (value === "\r" || value === "\n" || value === "\r\n") {
|
|
4574
|
+
cleanup();
|
|
4575
|
+
stdout.write("\n");
|
|
4576
|
+
resolveToken(token.trim());
|
|
4577
|
+
return;
|
|
4578
|
+
}
|
|
4579
|
+
if (value === "\x7F") {
|
|
4580
|
+
token = token.slice(0, -1);
|
|
4581
|
+
return;
|
|
4582
|
+
}
|
|
4583
|
+
token += value;
|
|
4584
|
+
}
|
|
4585
|
+
stdout.write(prompt);
|
|
4586
|
+
input.setRawMode(true);
|
|
4587
|
+
input.resume();
|
|
4588
|
+
input.on("data", onData);
|
|
4589
|
+
});
|
|
4590
|
+
}
|
|
4591
|
+
|
|
4592
|
+
// ../install-core/src/font-install.ts
|
|
4593
|
+
var FONT_CSS_START_MARKER = "/* \u2500\u2500 Wensity Fonts";
|
|
4594
|
+
var FONT_CSS_END_MARKER = "/* \u2500\u2500 End Wensity Fonts \u2500\u2500 */";
|
|
4595
|
+
function planFontInstall(headingFont, bodyFont) {
|
|
4596
|
+
const packages = [];
|
|
4597
|
+
const importLines = [];
|
|
4598
|
+
const seenPackages = /* @__PURE__ */ new Set();
|
|
4599
|
+
const seenImports = /* @__PURE__ */ new Set();
|
|
4600
|
+
const pushFont = (meta) => {
|
|
4601
|
+
if (!meta) return;
|
|
4602
|
+
if (!seenPackages.has(meta.packageName)) {
|
|
4603
|
+
seenPackages.add(meta.packageName);
|
|
4604
|
+
packages.push(meta.packageName);
|
|
4605
|
+
}
|
|
4606
|
+
for (const specifier of curatedFontCssImportSpecifiers(meta)) {
|
|
4607
|
+
const line = `@import "${specifier}";`;
|
|
4608
|
+
if (!seenImports.has(line)) {
|
|
4609
|
+
seenImports.add(line);
|
|
4610
|
+
importLines.push(line);
|
|
4611
|
+
}
|
|
4612
|
+
}
|
|
4613
|
+
};
|
|
4614
|
+
if (headingFont !== "system") {
|
|
4615
|
+
pushFont(HEADING_FONT_PACKAGES[headingFont]);
|
|
3285
4616
|
}
|
|
3286
|
-
|
|
3287
|
-
|
|
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}`);
|
|
4617
|
+
if (bodyFont !== "system") {
|
|
4618
|
+
pushFont(BODY_FONT_PACKAGES[bodyFont]);
|
|
3292
4619
|
}
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
const message = formatErrorMessage(payload) ?? response.statusText;
|
|
3297
|
-
throw new CliError(`${response.status} ${message}`);
|
|
4620
|
+
packages.sort();
|
|
4621
|
+
if (importLines.length === 0) {
|
|
4622
|
+
return { packages: [], importLines: [], block: null };
|
|
3298
4623
|
}
|
|
3299
|
-
|
|
4624
|
+
const block = [
|
|
4625
|
+
`${FONT_CSS_START_MARKER} \u2500\u2500 */`,
|
|
4626
|
+
...importLines,
|
|
4627
|
+
FONT_CSS_END_MARKER
|
|
4628
|
+
].join("\n");
|
|
4629
|
+
return { packages, importLines, block };
|
|
3300
4630
|
}
|
|
3301
|
-
function
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
4631
|
+
function upsertFontImportBlock(existingCss, block) {
|
|
4632
|
+
const start = existingCss.indexOf(FONT_CSS_START_MARKER);
|
|
4633
|
+
const end = existingCss.indexOf(FONT_CSS_END_MARKER);
|
|
4634
|
+
if (start !== -1 || end !== -1) {
|
|
4635
|
+
if (start === -1 || end === -1 || end <= start) {
|
|
4636
|
+
throw new WensityError(
|
|
4637
|
+
"config_invalid",
|
|
4638
|
+
"Configured Tailwind CSS has a partial Wensity Fonts block. Remove the broken markers and re-run install."
|
|
4639
|
+
);
|
|
4640
|
+
}
|
|
4641
|
+
const before2 = existingCss.slice(0, start).replace(/\s*$/, "");
|
|
4642
|
+
const after2 = existingCss.slice(end + FONT_CSS_END_MARKER.length).replace(/^\s*/, "");
|
|
4643
|
+
if (!block) {
|
|
4644
|
+
const joined = [before2, after2].filter((part) => part.length > 0).join("\n\n");
|
|
4645
|
+
return joined ? `${joined.replace(/\n{3,}/g, "\n\n")}
|
|
4646
|
+
` : "";
|
|
4647
|
+
}
|
|
4648
|
+
const normalizedBlock2 = block.trim();
|
|
4649
|
+
const prefix2 = before2 ? `${before2}
|
|
4650
|
+
|
|
4651
|
+
` : "";
|
|
4652
|
+
const suffix2 = after2 ? `
|
|
4653
|
+
|
|
4654
|
+
${after2}` : "\n";
|
|
4655
|
+
return `${prefix2}${normalizedBlock2}${suffix2}`.replace(/\n{3,}/g, "\n\n");
|
|
3307
4656
|
}
|
|
4657
|
+
if (!block) return existingCss;
|
|
4658
|
+
const normalizedBlock = block.trim();
|
|
4659
|
+
const insertAt = findFontBlockInsertIndex(existingCss);
|
|
4660
|
+
const before = existingCss.slice(0, insertAt).replace(/\s*$/, "");
|
|
4661
|
+
const after = existingCss.slice(insertAt).replace(/^\s*/, "");
|
|
4662
|
+
const prefix = before ? `${before}
|
|
4663
|
+
|
|
4664
|
+
` : "";
|
|
4665
|
+
const suffix = after ? `
|
|
4666
|
+
|
|
4667
|
+
${after}` : "\n";
|
|
4668
|
+
return `${prefix}${normalizedBlock}${suffix}`.replace(/\n{3,}/g, "\n\n");
|
|
4669
|
+
}
|
|
4670
|
+
function warnUnusedFontPackageMessage(headingFont, bodyFont, installedPackages) {
|
|
4671
|
+
const needed = new Set(planFontInstall(headingFont, bodyFont).packages);
|
|
4672
|
+
const curated = [
|
|
4673
|
+
...Object.values(HEADING_FONT_PACKAGES).map((entry) => entry.packageName),
|
|
4674
|
+
...Object.values(BODY_FONT_PACKAGES).map((entry) => entry.packageName)
|
|
4675
|
+
];
|
|
4676
|
+
const leftover = curated.filter(
|
|
4677
|
+
(pkg) => installedPackages.has(pkg) && !needed.has(pkg)
|
|
4678
|
+
);
|
|
4679
|
+
if (leftover.length === 0) return void 0;
|
|
4680
|
+
return `Project package.json still lists ${leftover.join(", ")} while curated fonts are set to heading="${headingFont}" body="${bodyFont}". Wensity removed the @import for unused fonts but will not uninstall ${leftover.join(", ")} (it may be used outside Wensity).`;
|
|
3308
4681
|
}
|
|
3309
|
-
function
|
|
3310
|
-
|
|
3311
|
-
const
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
4682
|
+
function findFontBlockInsertIndex(css) {
|
|
4683
|
+
let index = 0;
|
|
4684
|
+
const length = css.length;
|
|
4685
|
+
while (index < length) {
|
|
4686
|
+
while (index < length && /\s/.test(css[index])) index += 1;
|
|
4687
|
+
if (index >= length) break;
|
|
4688
|
+
if (css.startsWith("/*", index)) {
|
|
4689
|
+
const end = css.indexOf("*/", index + 2);
|
|
4690
|
+
if (end === -1) break;
|
|
4691
|
+
index = end + 2;
|
|
4692
|
+
continue;
|
|
4693
|
+
}
|
|
4694
|
+
if (css.startsWith("//", index)) {
|
|
4695
|
+
const end = css.indexOf("\n", index);
|
|
4696
|
+
if (end === -1) return length;
|
|
4697
|
+
index = end + 1;
|
|
4698
|
+
continue;
|
|
4699
|
+
}
|
|
4700
|
+
if (css.startsWith("@charset", index) || css.startsWith("@import", index)) {
|
|
4701
|
+
const end = css.indexOf(";", index);
|
|
4702
|
+
if (end === -1) break;
|
|
4703
|
+
index = end + 1;
|
|
4704
|
+
continue;
|
|
4705
|
+
}
|
|
4706
|
+
break;
|
|
3318
4707
|
}
|
|
3319
|
-
return
|
|
4708
|
+
return index;
|
|
4709
|
+
}
|
|
4710
|
+
|
|
4711
|
+
// src/http.ts
|
|
4712
|
+
function requestJson2(url, options = {}) {
|
|
4713
|
+
return requestJson(url, { userAgent: `${CLI_NAME}/${CLI_VERSION}`, ...options });
|
|
3320
4714
|
}
|
|
3321
4715
|
|
|
3322
|
-
// src/icon-transform.ts
|
|
4716
|
+
// ../install-core/src/icon-transform.ts
|
|
3323
4717
|
var UI_PRIMITIVES_CATEGORY = "UI Primitives";
|
|
3324
4718
|
var TABLER_ICONS_PACKAGE = "@tabler/icons-react";
|
|
3325
4719
|
var LUCIDE_ICONS_PACKAGE = "lucide-react";
|
|
@@ -3363,7 +4757,7 @@ function createLucideAdapter() {
|
|
|
3363
4757
|
return resolveLucideExport(tablerName);
|
|
3364
4758
|
} catch (error) {
|
|
3365
4759
|
const message = error instanceof Error ? error.message : String(error);
|
|
3366
|
-
throw new
|
|
4760
|
+
throw new WensityError("source_integrity_failed", `${context}: ${message}`);
|
|
3367
4761
|
}
|
|
3368
4762
|
},
|
|
3369
4763
|
strokePropTo: "strokeWidth"
|
|
@@ -3477,13 +4871,15 @@ function rewriteTablerImportsToTarget(content, adapter, context) {
|
|
|
3477
4871
|
for (const spec of specs) {
|
|
3478
4872
|
if (spec.isTypeOnly || typePrefix.includes("type")) {
|
|
3479
4873
|
if (spec.imported === "Icon" || isIconTransformExclusion(spec.imported)) {
|
|
3480
|
-
throw new
|
|
4874
|
+
throw new WensityError(
|
|
4875
|
+
"source_integrity_failed",
|
|
3481
4876
|
`${context}: unexpected type-only icon import "${spec.imported}" from ${TABLER_ICONS_PACKAGE}.`
|
|
3482
4877
|
);
|
|
3483
4878
|
}
|
|
3484
4879
|
}
|
|
3485
4880
|
if (isIconTransformExclusion(spec.imported)) {
|
|
3486
|
-
throw new
|
|
4881
|
+
throw new WensityError(
|
|
4882
|
+
"source_integrity_failed",
|
|
3487
4883
|
`${context}: "${spec.imported}" is excluded from the icon map (Phase 0 internal geometry). It must not appear as a library import.`
|
|
3488
4884
|
);
|
|
3489
4885
|
}
|
|
@@ -3506,12 +4902,14 @@ function rewriteTablerImportsToTarget(content, adapter, context) {
|
|
|
3506
4902
|
}
|
|
3507
4903
|
);
|
|
3508
4904
|
if (hasPackageImport(next, TABLER_ICONS_PACKAGE)) {
|
|
3509
|
-
throw new
|
|
4905
|
+
throw new WensityError(
|
|
4906
|
+
"source_integrity_failed",
|
|
3510
4907
|
`${context}: failed to rewrite all ${TABLER_ICONS_PACKAGE} imports for ${adapter.id}.`
|
|
3511
4908
|
);
|
|
3512
4909
|
}
|
|
3513
4910
|
if (/(?:import|export)\s+\*\s+as\s+\w+\s+from\s+["']@tabler\/icons-react["']/.test(content)) {
|
|
3514
|
-
throw new
|
|
4911
|
+
throw new WensityError(
|
|
4912
|
+
"source_integrity_failed",
|
|
3515
4913
|
`${context}: namespace imports from ${TABLER_ICONS_PACKAGE} are not supported. Use named imports.`
|
|
3516
4914
|
);
|
|
3517
4915
|
}
|
|
@@ -3523,7 +4921,8 @@ function assertNoUnmappedTablerImports(content, context) {
|
|
|
3523
4921
|
const specs = parseNamedImportClause(match[1] ?? "");
|
|
3524
4922
|
for (const spec of specs) {
|
|
3525
4923
|
if (isIconTransformExclusion(spec.imported)) {
|
|
3526
|
-
throw new
|
|
4924
|
+
throw new WensityError(
|
|
4925
|
+
"source_integrity_failed",
|
|
3527
4926
|
`${context}: "${spec.imported}" must not appear as a library import (Phase 0 exclusion).`
|
|
3528
4927
|
);
|
|
3529
4928
|
}
|
|
@@ -3576,108 +4975,12 @@ function rewriteStrokeProps(content, localBindings) {
|
|
|
3576
4975
|
}
|
|
3577
4976
|
|
|
3578
4977
|
// src/package-manager.ts
|
|
3579
|
-
|
|
3580
|
-
|
|
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
|
-
}
|
|
4978
|
+
function runInstall2(projectRoot, dependencies, options = {}) {
|
|
4979
|
+
return runInstall(projectRoot, dependencies, { onWarning: warn, ...options });
|
|
3677
4980
|
}
|
|
3678
4981
|
|
|
3679
4982
|
// src/preset.ts
|
|
3680
|
-
import { readFile as
|
|
4983
|
+
import { readFile as readFile5 } from "fs/promises";
|
|
3681
4984
|
async function resolvePresetStyle(input) {
|
|
3682
4985
|
if (input.presetCode && input.presetFile) {
|
|
3683
4986
|
throw new CliError("Use either --preset or --preset-file, not both.");
|
|
@@ -3685,7 +4988,7 @@ async function resolvePresetStyle(input) {
|
|
|
3685
4988
|
let code = input.presetCode;
|
|
3686
4989
|
if (input.presetFile) {
|
|
3687
4990
|
try {
|
|
3688
|
-
const raw = await
|
|
4991
|
+
const raw = await readFile5(input.presetFile, "utf8");
|
|
3689
4992
|
const trimmed = raw.trim();
|
|
3690
4993
|
if (trimmed.startsWith("{")) {
|
|
3691
4994
|
const parsed = JSON.parse(trimmed);
|
|
@@ -3885,18 +5188,18 @@ function formatTokenPanel(summary) {
|
|
|
3885
5188
|
}
|
|
3886
5189
|
|
|
3887
5190
|
// src/preset-init-writes.ts
|
|
3888
|
-
import { mkdir as
|
|
3889
|
-
import { dirname as
|
|
5191
|
+
import { mkdir as mkdir4, writeFile as writeFile3 } from "fs/promises";
|
|
5192
|
+
import { dirname as dirname5 } from "path";
|
|
3890
5193
|
async function commitPresetInitWrites(input) {
|
|
3891
5194
|
let cssWritten = false;
|
|
3892
5195
|
try {
|
|
3893
5196
|
if (input.cssChanged) {
|
|
3894
|
-
await
|
|
5197
|
+
await mkdir4(dirname5(input.cssAbsolute), { recursive: true });
|
|
3895
5198
|
await writeFile3(input.cssAbsolute, input.nextCss, "utf8");
|
|
3896
5199
|
cssWritten = true;
|
|
3897
5200
|
}
|
|
3898
5201
|
if (input.writeUtility) {
|
|
3899
|
-
await
|
|
5202
|
+
await mkdir4(dirname5(input.utilityAbsolutePath), { recursive: true });
|
|
3900
5203
|
await writeFile3(input.utilityAbsolutePath, input.utilityContent, "utf8");
|
|
3901
5204
|
}
|
|
3902
5205
|
await input.commitConfig();
|
|
@@ -3912,8 +5215,8 @@ async function commitPresetInitWrites(input) {
|
|
|
3912
5215
|
}
|
|
3913
5216
|
|
|
3914
5217
|
// src/project-templates.ts
|
|
3915
|
-
import { mkdir as
|
|
3916
|
-
import { dirname as
|
|
5218
|
+
import { mkdir as mkdir5, readFile as readFile6, writeFile as writeFile4 } from "fs/promises";
|
|
5219
|
+
import { dirname as dirname6, join as join9 } from "path";
|
|
3917
5220
|
function templateCss() {
|
|
3918
5221
|
return `@import "tailwindcss";
|
|
3919
5222
|
|
|
@@ -3937,6 +5240,8 @@ function getProjectTemplateDefinition(id) {
|
|
|
3937
5240
|
function applyTemplateToConfig(config, template) {
|
|
3938
5241
|
return {
|
|
3939
5242
|
...config,
|
|
5243
|
+
components: "src/components/wensity",
|
|
5244
|
+
utils: "src/lib/utils",
|
|
3940
5245
|
tailwind: {
|
|
3941
5246
|
...config.tailwind,
|
|
3942
5247
|
css: template.cssPath
|
|
@@ -3948,7 +5253,7 @@ async function scaffoldProjectTemplate(projectRoot, template, options) {
|
|
|
3948
5253
|
if (!options.force) {
|
|
3949
5254
|
const conflicts = [];
|
|
3950
5255
|
for (const file of template.files) {
|
|
3951
|
-
if (await fileExists(
|
|
5256
|
+
if (await fileExists(join9(projectRoot, file.path))) {
|
|
3952
5257
|
conflicts.push(file.path);
|
|
3953
5258
|
}
|
|
3954
5259
|
}
|
|
@@ -3965,10 +5270,10 @@ async function scaffoldProjectTemplate(projectRoot, template, options) {
|
|
|
3965
5270
|
return planned;
|
|
3966
5271
|
}
|
|
3967
5272
|
async function writeTemplateFile(projectRoot, file) {
|
|
3968
|
-
const absolute =
|
|
3969
|
-
await
|
|
5273
|
+
const absolute = join9(projectRoot, file.path);
|
|
5274
|
+
await mkdir5(dirname6(absolute), { recursive: true });
|
|
3970
5275
|
if (file.path === "package.json" && await fileExists(absolute)) {
|
|
3971
|
-
const existing = JSON.parse(await
|
|
5276
|
+
const existing = JSON.parse(await readFile6(absolute, "utf8"));
|
|
3972
5277
|
const next = JSON.parse(file.content);
|
|
3973
5278
|
await writeFile4(
|
|
3974
5279
|
absolute,
|
|
@@ -4291,7 +5596,7 @@ function formatTemplateFiles(files) {
|
|
|
4291
5596
|
return files.map(toPosixPath);
|
|
4292
5597
|
}
|
|
4293
5598
|
|
|
4294
|
-
// src/style-css.ts
|
|
5599
|
+
// ../install-core/src/style-css.ts
|
|
4295
5600
|
var ROOT_SELECTOR = ":root";
|
|
4296
5601
|
var DARK_SELECTOR = ".dark";
|
|
4297
5602
|
var SHARED_TOKEN_KEYS = ["radius", "fontSans", "fontDisplay", "fontMono"];
|
|
@@ -4375,11 +5680,11 @@ function escapeRegExp(value) {
|
|
|
4375
5680
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
4376
5681
|
}
|
|
4377
5682
|
|
|
4378
|
-
// src/component-install.ts
|
|
4379
|
-
import { mkdir as
|
|
4380
|
-
import { dirname as
|
|
5683
|
+
// ../install-core/src/component-install.ts
|
|
5684
|
+
import { mkdir as mkdir6, readFile as readFile7, writeFile as writeFile5 } from "fs/promises";
|
|
5685
|
+
import { dirname as dirname7, join as join10, relative as relative7 } from "path";
|
|
4381
5686
|
|
|
4382
|
-
// src/templates.ts
|
|
5687
|
+
// ../install-core/src/templates.ts
|
|
4383
5688
|
function cnUtilityTemplate() {
|
|
4384
5689
|
return `import { clsx, type ClassValue } from "clsx";
|
|
4385
5690
|
import { twMerge } from "tailwind-merge";
|
|
@@ -4532,7 +5837,13 @@ export function WensitySecondaryButton({
|
|
|
4532
5837
|
`;
|
|
4533
5838
|
}
|
|
4534
5839
|
|
|
4535
|
-
// src/component-install.ts
|
|
5840
|
+
// ../install-core/src/component-install.ts
|
|
5841
|
+
function safeTarget(projectRoot, relativePath) {
|
|
5842
|
+
assertTargetPathLength(relativePath);
|
|
5843
|
+
const absolutePath = resolveInside(projectRoot, relativePath);
|
|
5844
|
+
assertNoSymlinkEscape(projectRoot, absolutePath);
|
|
5845
|
+
return absolutePath;
|
|
5846
|
+
}
|
|
4536
5847
|
var ALWAYS_PRESENT_PACKAGES = /* @__PURE__ */ new Set(["react", "react-dom", "next"]);
|
|
4537
5848
|
var OVERLAY_HELPER_PATH = "components/wensity/overlay.tsx";
|
|
4538
5849
|
var PRIMITIVE_CSS_START_MARKER = "/* \u2500\u2500 Wensity Primitive Tokens";
|
|
@@ -4554,7 +5865,7 @@ async function prepareInstall(projectRoot, config, payload, options = {}) {
|
|
|
4554
5865
|
for (const [index, payloadFile] of payload.files.entries()) {
|
|
4555
5866
|
const payloadFileKind = getPayloadFileKind(payloadFile);
|
|
4556
5867
|
const relativeOutputPath = getPayloadOutputPath(payloadFile, fallbackFileName, config);
|
|
4557
|
-
const absoluteOutputPath =
|
|
5868
|
+
const absoluteOutputPath = safeTarget(projectRoot, relativeOutputPath);
|
|
4558
5869
|
if (!payloadHelperNeeds) mergeHelperNeeds(inferredHelperNeeds, inferHelperNeeds(payloadFile.content));
|
|
4559
5870
|
const transformed = transformComponentSource(payloadFile.content, config, {
|
|
4560
5871
|
isPrimitiveCategory,
|
|
@@ -4577,9 +5888,9 @@ async function prepareInstall(projectRoot, config, payload, options = {}) {
|
|
|
4577
5888
|
const helperNeeds = payloadHelperNeeds ?? inferredHelperNeeds;
|
|
4578
5889
|
if (!hasGraphHelperFiles && helperNeeds.utils) {
|
|
4579
5890
|
const utilityPath = withSourceExtension(config.utils, "ts");
|
|
4580
|
-
if (!await fileExists(
|
|
5891
|
+
if (!await fileExists(safeTarget(projectRoot, utilityPath))) {
|
|
4581
5892
|
files.push({
|
|
4582
|
-
absolutePath:
|
|
5893
|
+
absolutePath: safeTarget(projectRoot, utilityPath),
|
|
4583
5894
|
relativePath: utilityPath,
|
|
4584
5895
|
content: cnUtilityTemplate(),
|
|
4585
5896
|
kind: "helper"
|
|
@@ -4587,11 +5898,11 @@ async function prepareInstall(projectRoot, config, payload, options = {}) {
|
|
|
4587
5898
|
}
|
|
4588
5899
|
}
|
|
4589
5900
|
if (!hasGraphHelperFiles && helperNeeds.primitives) {
|
|
4590
|
-
const primitivesPath = toPosixPath(
|
|
4591
|
-
if (!await fileExists(
|
|
5901
|
+
const primitivesPath = toPosixPath(join10(config.components, "primitives.tsx"));
|
|
5902
|
+
if (!await fileExists(safeTarget(projectRoot, primitivesPath))) {
|
|
4592
5903
|
const primitives = primitivesTemplate(config.aliases.utils);
|
|
4593
5904
|
files.push({
|
|
4594
|
-
absolutePath:
|
|
5905
|
+
absolutePath: safeTarget(projectRoot, primitivesPath),
|
|
4595
5906
|
relativePath: primitivesPath,
|
|
4596
5907
|
content: primitives,
|
|
4597
5908
|
kind: "helper"
|
|
@@ -4627,8 +5938,8 @@ async function planPrimitiveCssFile(projectRoot, config, payload) {
|
|
|
4627
5938
|
primitiveCss = applyStyleToPrimitiveCss(primitiveCss, style).trim();
|
|
4628
5939
|
const cssRelative = config.tailwind?.css;
|
|
4629
5940
|
if (!cssRelative) return void 0;
|
|
4630
|
-
const absolutePath =
|
|
4631
|
-
const existing = await fileExists(absolutePath) ? await
|
|
5941
|
+
const absolutePath = safeTarget(projectRoot, cssRelative);
|
|
5942
|
+
const existing = await fileExists(absolutePath) ? await readFile7(absolutePath, "utf8") : "";
|
|
4632
5943
|
const withBase = upsertBaseCssBlock(existing, buildBaseCssBlock(baseColorFromStyle(style)));
|
|
4633
5944
|
const withTokens = upsertPrimitiveCssBlock(withBase, primitiveCss);
|
|
4634
5945
|
const fonts = curatedFontsFromStyle(style);
|
|
@@ -4667,7 +5978,8 @@ function upsertDelimitedBlock(existingCss, block, options) {
|
|
|
4667
5978
|
return `${before}${normalizedBlock}${after}`.replace(/\n{3,}/g, "\n\n");
|
|
4668
5979
|
}
|
|
4669
5980
|
if (start !== -1 || end !== -1) {
|
|
4670
|
-
throw new
|
|
5981
|
+
throw new WensityError(
|
|
5982
|
+
"config_invalid",
|
|
4671
5983
|
`Configured Tailwind CSS has a partial ${options.label} block. Remove the broken markers and re-run install.`
|
|
4672
5984
|
);
|
|
4673
5985
|
}
|
|
@@ -4697,18 +6009,12 @@ async function assertNoConflicts(files, force) {
|
|
|
4697
6009
|
if (await fileExists(file.absolutePath)) conflicts.push(file.relativePath);
|
|
4698
6010
|
}
|
|
4699
6011
|
if (conflicts.length > 0) {
|
|
4700
|
-
throw new
|
|
6012
|
+
throw new WensityError("file_conflict", `Refusing to overwrite existing files. Use --force to replace:
|
|
4701
6013
|
${conflicts.map((path) => ` - ${path}`).join("\n")}`);
|
|
4702
6014
|
}
|
|
4703
6015
|
}
|
|
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
6016
|
function summarizeFiles(projectRoot, files) {
|
|
4711
|
-
return files.map((file) => toPosixPath(
|
|
6017
|
+
return files.map((file) => toPosixPath(relative7(projectRoot, file.absolutePath)));
|
|
4712
6018
|
}
|
|
4713
6019
|
function transformComponentSource(content, config, iconOptions) {
|
|
4714
6020
|
let transformed = content;
|
|
@@ -4729,13 +6035,13 @@ function getPayloadFileKind(payloadFile) {
|
|
|
4729
6035
|
}
|
|
4730
6036
|
function getPayloadOutputPath(payloadFile, fallbackFileName, config) {
|
|
4731
6037
|
if (isUtilsHelperFile(payloadFile)) return withSourceExtension(config.utils, "ts");
|
|
4732
|
-
if (isOverlayHelperFile(payloadFile)) return toPosixPath(
|
|
4733
|
-
if (isPrimitivesHelperFile(payloadFile)) return toPosixPath(
|
|
6038
|
+
if (isOverlayHelperFile(payloadFile)) return toPosixPath(join10(config.components, "overlay.tsx"));
|
|
6039
|
+
if (isPrimitivesHelperFile(payloadFile)) return toPosixPath(join10(config.components, "primitives.tsx"));
|
|
4734
6040
|
if (payloadFile.kind === "helper") {
|
|
4735
|
-
throw new
|
|
6041
|
+
throw new WensityError("path_unsafe", `Unsupported registry helper file path: ${payloadFile.path}`);
|
|
4736
6042
|
}
|
|
4737
6043
|
const safePayloadPath = sanitizePayloadPath(payloadFile.path, fallbackFileName);
|
|
4738
|
-
return toPosixPath(
|
|
6044
|
+
return toPosixPath(join10(config.components, safePayloadPath));
|
|
4739
6045
|
}
|
|
4740
6046
|
function isGraphHelperFile(payloadFile) {
|
|
4741
6047
|
return isUtilsHelperFile(payloadFile) || isOverlayHelperFile(payloadFile) || isPrimitivesHelperFile(payloadFile);
|
|
@@ -4832,7 +6138,7 @@ function normalizePackageName(specifier) {
|
|
|
4832
6138
|
function withSourceExtension(path, extension) {
|
|
4833
6139
|
const directory = parentDirectory(path);
|
|
4834
6140
|
const fileName = extensionlessPath(path.split("/").at(-1) ?? path);
|
|
4835
|
-
return toPosixPath(
|
|
6141
|
+
return toPosixPath(join10(directory === "." ? "" : directory, `${fileName}.${extension}`));
|
|
4836
6142
|
}
|
|
4837
6143
|
|
|
4838
6144
|
// src/commands.ts
|
|
@@ -4925,6 +6231,7 @@ async function initCommand(context, parsed) {
|
|
|
4925
6231
|
const hasPreset = Boolean(presetCode || presetFile);
|
|
4926
6232
|
const dependencies = ["clsx", "tailwind-merge"];
|
|
4927
6233
|
const installOptions = installOptionsFrom(context, parsed);
|
|
6234
|
+
assertProjectRootCandidate(context.cwd, context.env.HOME);
|
|
4928
6235
|
if (hasPreset) {
|
|
4929
6236
|
await initWithPreset(context, {
|
|
4930
6237
|
dryRun,
|
|
@@ -4942,7 +6249,7 @@ async function initCommand(context, parsed) {
|
|
|
4942
6249
|
const templateFiles = template ? await scaffoldProjectTemplate(context.cwd, template, { dryRun, force }) : [];
|
|
4943
6250
|
const packageManagerFiles = template ? prepareProjectPackageManager(context.cwd, { ...installOptions, dryRun }) : [];
|
|
4944
6251
|
const config = template ? applyTemplateToConfig(createDefaultConfig(context.cwd, apiUrl), template) : createDefaultConfig(context.cwd, apiUrl);
|
|
4945
|
-
const configPath =
|
|
6252
|
+
const configPath = join11(context.cwd, CONFIG_FILE_NAME);
|
|
4946
6253
|
const utilityPath = resolveConfiguredFilePath(context.cwd, config.utils);
|
|
4947
6254
|
const utilityRelativePath = toPosixPath(config.utils.endsWith(".ts") ? config.utils : `${config.utils}.ts`);
|
|
4948
6255
|
if (dryRun) {
|
|
@@ -4963,8 +6270,8 @@ async function initCommand(context, parsed) {
|
|
|
4963
6270
|
await writeTextFile(utilityPath, cnUtilityTemplate());
|
|
4964
6271
|
}
|
|
4965
6272
|
if (install) {
|
|
4966
|
-
if (template) runProjectInstall(context.cwd, installOptions);
|
|
4967
|
-
|
|
6273
|
+
if (template) await runProjectInstall(context.cwd, installOptions);
|
|
6274
|
+
await runInstall2(context.cwd, missingDependencies(context.cwd, dependencies), installOptions);
|
|
4968
6275
|
}
|
|
4969
6276
|
await assertInitPostconditions(context.cwd, [
|
|
4970
6277
|
...templateFiles,
|
|
@@ -4992,7 +6299,7 @@ async function initWithPreset(context, options) {
|
|
|
4992
6299
|
presetCode: options.presetCode,
|
|
4993
6300
|
presetFile: options.presetFile
|
|
4994
6301
|
});
|
|
4995
|
-
const configPath =
|
|
6302
|
+
const configPath = join11(context.cwd, CONFIG_FILE_NAME);
|
|
4996
6303
|
const configExists = await fileExists(configPath);
|
|
4997
6304
|
const templateFiles = options.template ? await scaffoldProjectTemplate(context.cwd, options.template, {
|
|
4998
6305
|
dryRun: options.dryRun,
|
|
@@ -5007,7 +6314,7 @@ async function initWithPreset(context, options) {
|
|
|
5007
6314
|
if (options.template) {
|
|
5008
6315
|
throw new CliError(`${CONFIG_FILE_NAME} already exists. Omit --template for existing projects or use --force.`);
|
|
5009
6316
|
}
|
|
5010
|
-
const loaded = await loadConfig(context.cwd, { apiUrl: options.apiUrl });
|
|
6317
|
+
const loaded = await loadConfig(context.cwd, { apiUrl: options.apiUrl }, { home: context.env.HOME });
|
|
5011
6318
|
config = {
|
|
5012
6319
|
...loaded.config,
|
|
5013
6320
|
style
|
|
@@ -5061,8 +6368,8 @@ async function initWithPreset(context, options) {
|
|
|
5061
6368
|
}
|
|
5062
6369
|
});
|
|
5063
6370
|
if (options.install) {
|
|
5064
|
-
if (options.template) runProjectInstall(context.cwd, options.installOptions);
|
|
5065
|
-
|
|
6371
|
+
if (options.template) await runProjectInstall(context.cwd, options.installOptions);
|
|
6372
|
+
await runInstall2(
|
|
5066
6373
|
context.cwd,
|
|
5067
6374
|
missingDependencies(context.cwd, installDependencies),
|
|
5068
6375
|
options.installOptions
|
|
@@ -5111,7 +6418,7 @@ async function applyCommand(context, parsed) {
|
|
|
5111
6418
|
if (!presetCodeInput && !presetFile) {
|
|
5112
6419
|
throw new CliError("Missing preset. Pass --preset <code> or --preset-file <path>.");
|
|
5113
6420
|
}
|
|
5114
|
-
const loaded = await loadConfig(context.cwd, { registry: registryOverride, apiUrl });
|
|
6421
|
+
const loaded = await loadConfig(context.cwd, { registry: registryOverride, apiUrl }, { home: context.env.HOME });
|
|
5115
6422
|
if (!loaded.exists) {
|
|
5116
6423
|
throw new CliError(`${CONFIG_FILE_NAME} not found. Run "wensity init" first, then "wensity apply --preset <code>".`);
|
|
5117
6424
|
}
|
|
@@ -5159,7 +6466,7 @@ async function applyCommand(context, parsed) {
|
|
|
5159
6466
|
}
|
|
5160
6467
|
});
|
|
5161
6468
|
if (install && installDependencies.length > 0) {
|
|
5162
|
-
|
|
6469
|
+
await runInstall2(
|
|
5163
6470
|
loaded.projectRoot,
|
|
5164
6471
|
missingDependencies(loaded.projectRoot, installDependencies),
|
|
5165
6472
|
installOptionsFrom(context, parsed)
|
|
@@ -5212,7 +6519,7 @@ async function preparePresetStyleApplication(projectRoot, config, style, operati
|
|
|
5212
6519
|
throw new CliError("No Tailwind CSS path configured. Set tailwind.css in wensity.json.");
|
|
5213
6520
|
}
|
|
5214
6521
|
const cssAbsolute = resolveInside(projectRoot, cssRelative);
|
|
5215
|
-
const existingCss = await fileExists(cssAbsolute) ? await
|
|
6522
|
+
const existingCss = await fileExists(cssAbsolute) ? await readFile8(cssAbsolute, "utf8") : "";
|
|
5216
6523
|
const withBase = upsertBaseCssBlock(existingCss, buildBaseCssBlock(baseColorFromStyle(style)));
|
|
5217
6524
|
const withTokens = upsertPrimitiveCssBlock(withBase, primitiveCss);
|
|
5218
6525
|
const nextCss = upsertFontImportBlock(withTokens, fontPlan.block);
|
|
@@ -5237,7 +6544,7 @@ async function loginCommand(context, parsed) {
|
|
|
5237
6544
|
if (!token) {
|
|
5238
6545
|
throw new CliError("A token is required. Create one in the Wensity dashboard and run wensity login again.");
|
|
5239
6546
|
}
|
|
5240
|
-
const whoami = await
|
|
6547
|
+
const whoami = await requestJson2(joinUrl(apiBaseUrl, "/api/cli/whoami"), { token });
|
|
5241
6548
|
const path = await writeCredentials(context.env, {
|
|
5242
6549
|
version: 1,
|
|
5243
6550
|
apiBaseUrl,
|
|
@@ -5266,7 +6573,7 @@ async function logoutCommand(context) {
|
|
|
5266
6573
|
}
|
|
5267
6574
|
async function whoamiCommand(context, parsed) {
|
|
5268
6575
|
const credentials = await requireCredentials(context.env);
|
|
5269
|
-
const whoami = await
|
|
6576
|
+
const whoami = await requestJson2(joinUrl(credentials.apiBaseUrl, "/api/cli/whoami"), {
|
|
5270
6577
|
token: credentials.token
|
|
5271
6578
|
});
|
|
5272
6579
|
if (hasFlag(parsed, "json")) {
|
|
@@ -5280,10 +6587,15 @@ async function listCommand(context, parsed) {
|
|
|
5280
6587
|
const resourceType = getResourceType(parsed);
|
|
5281
6588
|
const apiUrl = getStringOption(parsed, "api-url") ?? context.env.WENSITY_API_URL;
|
|
5282
6589
|
const registryOverride = getStringOption(parsed, "registry");
|
|
5283
|
-
const
|
|
5284
|
-
|
|
6590
|
+
const configuredRegistry = await loadConfig(context.cwd, { registry: registryOverride, apiUrl }, { home: context.env.HOME }).then((loaded) => loaded.config.registry).catch((error) => {
|
|
6591
|
+
if (isWensityError(error) && error.code === "project_ambiguous") {
|
|
6592
|
+
return registryOverride ?? createDefaultConfig(context.cwd, apiUrl).registry;
|
|
6593
|
+
}
|
|
6594
|
+
throw error;
|
|
6595
|
+
});
|
|
6596
|
+
const registry = registryUrlForType(configuredRegistry, resourceType, registryOverride);
|
|
5285
6597
|
if (resourceType === "block") {
|
|
5286
|
-
const response2 = await
|
|
6598
|
+
const response2 = await requestJson2(registry);
|
|
5287
6599
|
if (hasFlag(parsed, "json")) {
|
|
5288
6600
|
printJson(response2);
|
|
5289
6601
|
return;
|
|
@@ -5302,7 +6614,7 @@ async function listCommand(context, parsed) {
|
|
|
5302
6614
|
]));
|
|
5303
6615
|
return;
|
|
5304
6616
|
}
|
|
5305
|
-
const response = await
|
|
6617
|
+
const response = await requestJson2(registry);
|
|
5306
6618
|
if (hasFlag(parsed, "json")) {
|
|
5307
6619
|
printJson(response);
|
|
5308
6620
|
return;
|
|
@@ -5354,7 +6666,10 @@ ${usage}`);
|
|
|
5354
6666
|
}
|
|
5355
6667
|
const apiUrl = getStringOption(parsed, "api-url") ?? context.env.WENSITY_API_URL;
|
|
5356
6668
|
const registryOverride = getStringOption(parsed, "registry");
|
|
5357
|
-
const loaded = await loadConfig(context.cwd, { registry: registryOverride, apiUrl });
|
|
6669
|
+
const loaded = await loadConfig(context.cwd, { registry: registryOverride, apiUrl }, { home: context.env.HOME });
|
|
6670
|
+
if (importAliasesResolve(loaded.projectRoot, loaded.config) === false) {
|
|
6671
|
+
throw new CliError(importAliasBlockMessage(loaded.config));
|
|
6672
|
+
}
|
|
5358
6673
|
const credentials = await readCredentials(context.env);
|
|
5359
6674
|
const registry = registryUrlForType(loaded.config.registry, resourceType, registryOverride);
|
|
5360
6675
|
const force = updateMode || hasFlag(parsed, "force");
|
|
@@ -5405,13 +6720,12 @@ ${usage}`);
|
|
|
5405
6720
|
if (unusedFontWarning) warn(unusedFontWarning);
|
|
5406
6721
|
return;
|
|
5407
6722
|
}
|
|
5408
|
-
|
|
5409
|
-
|
|
5410
|
-
|
|
5411
|
-
|
|
5412
|
-
}
|
|
6723
|
+
await writeWithRollback(
|
|
6724
|
+
loaded.projectRoot,
|
|
6725
|
+
installPlans.flatMap((prepared) => prepared.cssFile ? [...prepared.files, prepared.cssFile] : prepared.files)
|
|
6726
|
+
);
|
|
5413
6727
|
if (install) {
|
|
5414
|
-
|
|
6728
|
+
await runInstall2(loaded.projectRoot, missing, installOptionsFrom(context, parsed));
|
|
5415
6729
|
}
|
|
5416
6730
|
const noun = payloads.length === 1 ? "Component" : "Components";
|
|
5417
6731
|
printTitle(updateMode ? `${noun} updated` : `${noun} installed`);
|
|
@@ -5436,61 +6750,95 @@ ${usage}`);
|
|
|
5436
6750
|
resourceType === "block" ? "Customize the block copy, colors, and sections" : "Customize props and styling for your app"
|
|
5437
6751
|
]));
|
|
5438
6752
|
}
|
|
6753
|
+
async function writeWithRollback(projectRoot, files) {
|
|
6754
|
+
const byPath = /* @__PURE__ */ new Map();
|
|
6755
|
+
for (const file of files) {
|
|
6756
|
+
byPath.delete(file.absolutePath);
|
|
6757
|
+
byPath.set(file.absolutePath, file);
|
|
6758
|
+
}
|
|
6759
|
+
const result = await apply({
|
|
6760
|
+
projectRoot,
|
|
6761
|
+
planId: `cli_${randomBytes(8).toString("hex")}`,
|
|
6762
|
+
files: [...byPath.values()].map((file) => ({
|
|
6763
|
+
relativePath: toPosixPath(relative8(projectRoot, file.absolutePath)),
|
|
6764
|
+
content: file.content,
|
|
6765
|
+
action: existsSync5(file.absolutePath) ? file.kind === "config" ? "modify" : "overwrite" : "create"
|
|
6766
|
+
}))
|
|
6767
|
+
});
|
|
6768
|
+
if (result.status === "committed") {
|
|
6769
|
+
if (result.cleanupWarning) warn(result.cleanupWarning);
|
|
6770
|
+
return;
|
|
6771
|
+
}
|
|
6772
|
+
const where = result.failedPath ? ` while writing ${result.failedPath}` : "";
|
|
6773
|
+
if (result.status === "rolled_back") {
|
|
6774
|
+
throw new WensityError(
|
|
6775
|
+
result.error.code === "write_failed" ? "write_failed" : result.error.code,
|
|
6776
|
+
`Installation failed${where}; all project-file changes were reverted.
|
|
6777
|
+
${result.error.message}` + (result.cleanupWarning ? `
|
|
6778
|
+
${result.cleanupWarning}` : ""),
|
|
6779
|
+
{ cause: result.error }
|
|
6780
|
+
);
|
|
6781
|
+
}
|
|
6782
|
+
throw new WensityError(
|
|
6783
|
+
"rollback_failed",
|
|
6784
|
+
[
|
|
6785
|
+
`Installation failed${where}, and some files could not be restored. Nothing was deleted: the backups are in ${result.journalPath}.`,
|
|
6786
|
+
...result.unrecovered.map((item) => ` - ${item.path}: ${item.recovery}`),
|
|
6787
|
+
"Verify with git diff before removing that directory yourself."
|
|
6788
|
+
].join("\n"),
|
|
6789
|
+
{ cause: result.error }
|
|
6790
|
+
);
|
|
6791
|
+
}
|
|
5439
6792
|
function dedupe(values) {
|
|
5440
6793
|
return [...new Set(values)];
|
|
5441
6794
|
}
|
|
5442
6795
|
async function fetchComponentPayload(input) {
|
|
6796
|
+
const client = registryClient(input.registry, input.credentials);
|
|
5443
6797
|
const metadata = await withSpinner(
|
|
5444
6798
|
`Finding ${input.slug}`,
|
|
5445
|
-
() =>
|
|
6799
|
+
() => client.fetchItemMetadata(input.slug, "component"),
|
|
5446
6800
|
`Found ${input.slug}`
|
|
5447
6801
|
);
|
|
5448
|
-
|
|
5449
|
-
|
|
5450
|
-
|
|
5451
|
-
|
|
5452
|
-
|
|
5453
|
-
|
|
5454
|
-
|
|
5455
|
-
|
|
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"));
|
|
6802
|
+
try {
|
|
6803
|
+
return await client.fetchSourcePayload(input.slug, "component", {
|
|
6804
|
+
metadata,
|
|
6805
|
+
download: (task) => withSpinner(`Downloading ${metadata.name}`, task, `Downloaded ${metadata.name}`),
|
|
6806
|
+
onCredentialsRejected: () => warn("Saved Wensity credentials were rejected; falling back to the public free registry payload.")
|
|
6807
|
+
});
|
|
6808
|
+
} catch (error) {
|
|
6809
|
+
throw proRequiredError(error, metadata.name, "component");
|
|
5465
6810
|
}
|
|
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
6811
|
}
|
|
5477
6812
|
async function fetchBlockPayload(input) {
|
|
6813
|
+
const client = registryClient(input.registry, input.credentials);
|
|
5478
6814
|
const metadata = await withSpinner(
|
|
5479
6815
|
`Finding ${input.slug}`,
|
|
5480
|
-
() =>
|
|
6816
|
+
() => client.fetchItemMetadata(input.slug, "block"),
|
|
5481
6817
|
`Found ${input.slug}`
|
|
5482
6818
|
);
|
|
5483
|
-
|
|
5484
|
-
|
|
5485
|
-
|
|
5486
|
-
|
|
5487
|
-
|
|
5488
|
-
|
|
5489
|
-
|
|
5490
|
-
|
|
5491
|
-
|
|
5492
|
-
|
|
5493
|
-
|
|
6819
|
+
try {
|
|
6820
|
+
return await client.fetchSourcePayload(input.slug, "block", {
|
|
6821
|
+
metadata,
|
|
6822
|
+
download: (task) => withSpinner(`Downloading ${metadata.name}`, task, `Downloaded ${metadata.name}`)
|
|
6823
|
+
});
|
|
6824
|
+
} catch (error) {
|
|
6825
|
+
throw proRequiredError(error, metadata.name, "block");
|
|
6826
|
+
}
|
|
6827
|
+
}
|
|
6828
|
+
function registryClient(registry, credentials) {
|
|
6829
|
+
return createWensityClient({
|
|
6830
|
+
origin: registryOrigin(registry),
|
|
6831
|
+
registryUrl: registry,
|
|
6832
|
+
blocksRegistryUrl: registry,
|
|
6833
|
+
...credentials ? { token: credentials.token, apiBaseUrl: credentials.apiBaseUrl } : {},
|
|
6834
|
+
userAgent: `${CLI_NAME}/${CLI_VERSION}`
|
|
6835
|
+
});
|
|
6836
|
+
}
|
|
6837
|
+
function proRequiredError(error, resourceName, resourceType) {
|
|
6838
|
+
if (isWensityError(error) && error.code === "auth_required") {
|
|
6839
|
+
return new CliError(formatProRequiredError(resourceName, resourceType), 1, { cause: error });
|
|
6840
|
+
}
|
|
6841
|
+
return error;
|
|
5494
6842
|
}
|
|
5495
6843
|
function getResourceType(parsed) {
|
|
5496
6844
|
const value = getStringOption(parsed, "type") ?? "component";
|
|
@@ -5509,18 +6857,6 @@ async function requireCredentials(env) {
|
|
|
5509
6857
|
}
|
|
5510
6858
|
return credentials;
|
|
5511
6859
|
}
|
|
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
6860
|
function formatHelp() {
|
|
5525
6861
|
return [
|
|
5526
6862
|
brandBanner("Own your component source."),
|
|
@@ -5548,7 +6884,7 @@ function formatAccountPanel(whoami, savedPath) {
|
|
|
5548
6884
|
return panel(isPro ? "Wensity Pro" : "Wensity Free", lines);
|
|
5549
6885
|
}
|
|
5550
6886
|
async function writeTextFile(path, content) {
|
|
5551
|
-
await
|
|
6887
|
+
await mkdir7(dirname8(path), { recursive: true });
|
|
5552
6888
|
await writeFile6(path, content, "utf8");
|
|
5553
6889
|
}
|
|
5554
6890
|
function installOptionsFrom(context, parsed) {
|
|
@@ -5560,7 +6896,7 @@ function installOptionsFrom(context, parsed) {
|
|
|
5560
6896
|
async function assertInitPostconditions(projectRoot, relativePaths) {
|
|
5561
6897
|
const missing = [];
|
|
5562
6898
|
for (const relativePath of new Set(relativePaths)) {
|
|
5563
|
-
const absolute =
|
|
6899
|
+
const absolute = join11(projectRoot, relativePath);
|
|
5564
6900
|
let size = 0;
|
|
5565
6901
|
try {
|
|
5566
6902
|
size = (await stat(absolute)).size;
|
|
@@ -5584,10 +6920,11 @@ run({
|
|
|
5584
6920
|
cwd: process.cwd(),
|
|
5585
6921
|
env: process.env
|
|
5586
6922
|
}).catch((error) => {
|
|
5587
|
-
|
|
5588
|
-
|
|
6923
|
+
const cliError = toCliError(error);
|
|
6924
|
+
if (cliError) {
|
|
6925
|
+
process.stderr.write(`${formatCliError(cliError.message)}
|
|
5589
6926
|
`);
|
|
5590
|
-
process.exitCode =
|
|
6927
|
+
process.exitCode = cliError.exitCode;
|
|
5591
6928
|
return;
|
|
5592
6929
|
}
|
|
5593
6930
|
const message = error instanceof Error ? error.stack ?? error.message : String(error);
|