@sky.ui.pro/core 0.0.1 → 0.0.3

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/package.json CHANGED
@@ -1,24 +1,21 @@
1
1
  {
2
2
  "name": "@sky.ui.pro/core",
3
- "version": "0.0.1",
4
- "description": "Sky UI Pro web components (stub — artifacts download on postinstall)",
3
+ "version": "0.0.3",
4
+ "description": "Sky UI Pro web components — installs full package via postinstall (online license).",
5
5
  "type": "module",
6
6
  "license": "SEE LICENSE IN LICENSE.md",
7
- "sideEffects": true,
8
7
  "main": "./dist/stub/index.js",
9
- "types": "./dist/stub/index.d.ts",
10
8
  "exports": {
11
- ".": {
12
- "import": "./dist/stub/index.js",
13
- "types": "./dist/stub/index.d.ts"
14
- },
15
- "./package.json": "./package.json"
9
+ ".": "./dist/stub/index.js"
16
10
  },
17
11
  "files": [
18
12
  "scripts/postinstall.mjs",
13
+ "scripts/token.mjs",
19
14
  "dist/stub",
20
15
  "README.md",
21
- "LICENSE.md"
16
+ "LICENSE.md",
17
+ "scripts/project-env.mjs",
18
+ "scripts/merge-manifest.mjs"
22
19
  ],
23
20
  "scripts": {
24
21
  "postinstall": "node ./scripts/postinstall.mjs"
@@ -27,15 +24,30 @@
27
24
  "tar": "^7.4.3"
28
25
  },
29
26
  "peerDependencies": {
30
- "@sky.ui/core": "^0.0.1",
27
+ "@sky.ui/core": "^0.0.3",
31
28
  "lit": ">=3 <4"
32
29
  },
30
+ "peerDependenciesMeta": {
31
+ "lit": {
32
+ "optional": true
33
+ }
34
+ },
33
35
  "skyUiPro": {
34
36
  "apiBase": "https://library.sky-ui.com",
35
- "artifactRegistry": "gitlab"
37
+ "artifactRegistry": "gitlab",
38
+ "artifactName": "@sky.ui.pro-artifact/core"
36
39
  },
37
40
  "publishConfig": {
38
41
  "access": "public",
39
42
  "registry": "https://registry.npmjs.org/"
40
- }
43
+ },
44
+ "repository": {
45
+ "type": "git",
46
+ "url": "git+https://gitlab.com/sky_ui/sky-ui.git",
47
+ "directory": "packages/pro-npm/core"
48
+ },
49
+ "bugs": {
50
+ "url": "https://gitlab.com/sky_ui/sky-ui/-/issues"
51
+ },
52
+ "homepage": "https://library.sky-ui.com"
41
53
  }
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Merge public npm stub manifest with GitLab artifact manifest after postinstall hydration.
3
+ */
4
+
5
+ export const ARTIFACT_MANIFEST_SIDECAR = ".sky-ui-pro-artifact-manifest.json";
6
+
7
+ const PRO_ARTIFACT_PREFIX = "@sky.ui.pro-artifact/";
8
+ const PRO_PUBLIC_PREFIX = "@sky.ui.pro/";
9
+
10
+ /** Rename @sky.ui.pro-artifact/* deps to @sky.ui.pro/* for installed packages. */
11
+ export function renameProArtifactDeps(deps) {
12
+ if (!deps || typeof deps !== "object") return deps;
13
+ const out = { ...deps };
14
+ for (const [key, value] of Object.entries(out)) {
15
+ if (key.startsWith(PRO_ARTIFACT_PREFIX)) {
16
+ const next = `${PRO_PUBLIC_PREFIX}${key.slice(PRO_ARTIFACT_PREFIX.length)}`;
17
+ if (!(next in out)) out[next] = value;
18
+ delete out[key];
19
+ }
20
+ }
21
+ return out;
22
+ }
23
+
24
+ /** True when exports already point at hydrated artifact paths (not stub-only). */
25
+ export function hasHydratedExports(manifest) {
26
+ const exp = manifest?.exports;
27
+ if (!exp || typeof exp !== "object") return false;
28
+ if (exp["./vite"] || exp["./webpack"] || exp["./rollup"]) return true;
29
+ const root = exp["."];
30
+ if (typeof root === "string") {
31
+ return root.includes("dist/index") || (root.includes("/dist/") && !root.includes("/stub/"));
32
+ }
33
+ if (root && typeof root === "object") {
34
+ const imp = root.import ?? root.default ?? root.module;
35
+ return typeof imp === "string" && imp.includes("dist/index");
36
+ }
37
+ return false;
38
+ }
39
+
40
+ /**
41
+ * Build installed package.json: artifact resolution surface + public stub identity.
42
+ * @param {Record<string, unknown>} stub
43
+ * @param {Record<string, unknown>} artifact
44
+ */
45
+ export function mergeHydratedManifest(stub, artifact) {
46
+ const stubDeps = stub.dependencies && typeof stub.dependencies === "object" ? stub.dependencies : {};
47
+ const tarVersion = stubDeps.tar;
48
+
49
+ const merged = {
50
+ name: stub.name,
51
+ version: stub.version,
52
+ description: stub.description ?? artifact.description,
53
+ type: stub.type ?? artifact.type,
54
+ license: stub.license ?? artifact.license,
55
+ main: artifact.main,
56
+ module: artifact.module,
57
+ types: artifact.types,
58
+ sideEffects: artifact.sideEffects,
59
+ exports: artifact.exports,
60
+ peerDependencies: {
61
+ ...renameProArtifactDeps(artifact.peerDependencies),
62
+ ...(stub.peerDependencies ?? {}),
63
+ },
64
+ dependencies: {
65
+ ...renameProArtifactDeps(artifact.dependencies),
66
+ ...(tarVersion ? { tar: tarVersion } : {}),
67
+ },
68
+ scripts: stub.scripts?.postinstall ? { postinstall: stub.scripts.postinstall } : {},
69
+ skyUiPro: stub.skyUiPro,
70
+ publishConfig: stub.publishConfig,
71
+ repository: stub.repository,
72
+ bugs: stub.bugs,
73
+ homepage: stub.homepage,
74
+ };
75
+
76
+ if (artifact.peerDependenciesMeta && typeof artifact.peerDependenciesMeta === "object") {
77
+ merged.peerDependenciesMeta = artifact.peerDependenciesMeta;
78
+ }
79
+
80
+ return merged;
81
+ }
@@ -1,37 +1,47 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
3
  * Downloads the full @sky.ui.pro/* artifact from the Sky UI npm proxy (GitLab upstream).
4
- * Install flow matches HeroUI Pro: cache restore → else license-checked download; failed auth
5
- * does not remove artifacts already on disk.
4
+ * Cache restore re-validates the access token; install fails closed without a valid token.
6
5
  */
7
6
  import { createWriteStream } from "node:fs";
8
- import { access, cp, mkdir, mkdtemp, readdir, readFile, rm } from "node:fs/promises";
9
- import { homedir, tmpdir } from "node:os";
7
+ import { cp, mkdir, mkdtemp, readdir, readFile, rm, writeFile } from "node:fs/promises";
8
+ import { tmpdir } from "node:os";
10
9
  import { dirname, join } from "node:path";
11
10
  import { fileURLToPath } from "node:url";
12
11
  import { pipeline } from "node:stream/promises";
13
12
  import { extract } from "tar";
13
+ import {
14
+ ARTIFACT_MANIFEST_SIDECAR,
15
+ hasHydratedExports,
16
+ mergeHydratedManifest,
17
+ } from "./merge-manifest.mjs";
18
+ import {
19
+ ENTITLEMENT_MARKER,
20
+ cacheRoot,
21
+ exists,
22
+ isInstallStrict,
23
+ legacyCacheRoot,
24
+ loadInstallProjectEnv,
25
+ resolveApiBase,
26
+ resolveToken,
27
+ validateAccessTokenOnline,
28
+ writeEntitlementMarker,
29
+ formatRenewalEmailHint,
30
+ } from "./token.mjs";
14
31
 
15
32
  const scriptDir = dirname(fileURLToPath(import.meta.url));
16
33
  const pkgRoot = join(scriptDir, "..");
17
- const SKY_UI_HOME = join(homedir(), ".sky.ui");
18
- const LEGACY_SKY_UI_HOME = join(homedir(), ".sky-ui");
19
- const cacheRoot = join(SKY_UI_HOME, "cache");
20
- const legacyCacheRoot = join(LEGACY_SKY_UI_HOME, "cache");
21
34
 
22
- async function exists(p) {
23
- try {
24
- await access(p);
25
- return true;
26
- } catch {
27
- return false;
28
- }
29
- }
35
+ const SKIP_COPY_TO_INSTALL = new Set([ENTITLEMENT_MARKER, ARTIFACT_MANIFEST_SIDECAR]);
30
36
 
31
37
  async function readJson(p) {
32
38
  return JSON.parse(await readFile(p, "utf8"));
33
39
  }
34
40
 
41
+ async function writeJson(p, data) {
42
+ await writeFile(p, `${JSON.stringify(data, null, 2)}\n`, "utf8");
43
+ }
44
+
35
45
  function packageSlug(name) {
36
46
  return name.replace(/^@/, "").replace("/", "-");
37
47
  }
@@ -40,9 +50,10 @@ function cacheVersionDir(root, slug, version) {
40
50
  return join(root, slug, version);
41
51
  }
42
52
 
43
- async function copyDirectoryContents(src, dest) {
53
+ async function copyDirectoryContents(src, dest, { skip = SKIP_COPY_TO_INSTALL } = {}) {
44
54
  await mkdir(dest, { recursive: true });
45
55
  for (const entry of await readdir(src, { withFileTypes: true })) {
56
+ if (skip.has(entry.name)) continue;
46
57
  await cp(join(src, entry.name), join(dest, entry.name), { recursive: true, force: true });
47
58
  }
48
59
  }
@@ -57,61 +68,95 @@ async function enforceCachePolicy(slug, version) {
57
68
  }
58
69
  }
59
70
 
60
- async function restoreFromCache(slug, version, target) {
71
+ async function readArtifactManifestSidecar(cacheDir) {
72
+ const sidecar = join(cacheDir, ARTIFACT_MANIFEST_SIDECAR);
73
+ if (!(await exists(sidecar))) return null;
74
+ return readJson(sidecar);
75
+ }
76
+
77
+ /** Read artifact package.json from cached tarball (legacy caches without sidecar). */
78
+ async function readArtifactManifestFromTarball(slug, version) {
79
+ const tgz = join(cacheRoot, `${slug}-${version}.tgz`);
80
+ if (!(await exists(tgz))) return null;
81
+ const stagingDir = await mkdtemp(join(tmpdir(), `sky.ui.pro-manifest-${slug}-${version}-`));
82
+ try {
83
+ await extract({ file: tgz, cwd: stagingDir, strip: 1 });
84
+ return readJson(join(stagingDir, "package.json"));
85
+ } catch {
86
+ return null;
87
+ } finally {
88
+ await rm(stagingDir, { recursive: true, force: true });
89
+ }
90
+ }
91
+
92
+ async function resolveArtifactManifest(stubManifest, slug, version, cacheDir) {
93
+ const sidecar = cacheDir ? await readArtifactManifestSidecar(cacheDir) : null;
94
+ if (sidecar) return sidecar;
95
+ return readArtifactManifestFromTarball(slug, version);
96
+ }
97
+
98
+ async function applyHydratedManifest(targetRoot, stubManifest, artifactManifest) {
99
+ const merged = mergeHydratedManifest(stubManifest, artifactManifest);
100
+ await writeJson(join(targetRoot, "package.json"), merged);
101
+ return merged;
102
+ }
103
+
104
+ async function finalizeHydratedManifest(targetRoot, stubManifest, slug, version, cacheDir) {
105
+ const current = await readJson(join(targetRoot, "package.json"));
106
+ if (hasHydratedExports(current)) {
107
+ return current;
108
+ }
109
+ const artifactManifest = await resolveArtifactManifest(stubManifest, slug, version, cacheDir);
110
+ if (!artifactManifest) return current;
111
+ return applyHydratedManifest(targetRoot, stubManifest, artifactManifest);
112
+ }
113
+
114
+ async function restoreFromCache(slug, version, target, stubManifest) {
61
115
  for (const root of [cacheRoot, legacyCacheRoot]) {
62
116
  const src = cacheVersionDir(root, slug, version);
63
117
  if (!(await exists(src))) continue;
64
118
  const entries = await readdir(src);
65
119
  if (entries.length === 0) continue;
66
120
  await copyDirectoryContents(src, target);
67
- return true;
121
+ await finalizeHydratedManifest(target, stubManifest, slug, version, src);
122
+ return { cacheDir: src };
68
123
  }
69
- return false;
124
+ return null;
70
125
  }
71
126
 
72
- async function saveToCache(slug, version, source) {
127
+ async function saveToCache(slug, version, source, artifactManifest) {
73
128
  const dest = cacheVersionDir(cacheRoot, slug, version);
74
129
  await rm(dest, { recursive: true, force: true });
75
130
  await mkdir(dest, { recursive: true });
76
131
  await copyDirectoryContents(source, dest);
132
+ if (artifactManifest) {
133
+ await writeJson(join(dest, ARTIFACT_MANIFEST_SIDECAR), artifactManifest);
134
+ }
77
135
  }
78
136
 
79
- async function resolveToken() {
80
- const envCi = process.env.SKY_UI_AUTH_TOKEN?.trim();
81
- if (envCi) return envCi;
82
- const envPersonal = process.env.SKY_UI_PERSONAL_TOKEN?.trim();
83
- if (envPersonal) return envPersonal;
84
- for (const credPath of [
85
- join(SKY_UI_HOME, "credentials.json"),
86
- join(LEGACY_SKY_UI_HOME, "credentials.json"),
87
- ]) {
88
- if (!(await exists(credPath))) continue;
89
- const creds = JSON.parse(await readFile(credPath, "utf8"));
90
- if (typeof creds.personalToken === "string" && creds.personalToken.trim()) {
91
- return creds.personalToken.trim();
92
- }
93
- if (typeof creds.authToken === "string" && creds.authToken.trim()) {
94
- return creds.authToken.trim();
137
+ async function invalidateCache(slug, version) {
138
+ for (const root of [cacheRoot, legacyCacheRoot]) {
139
+ const dir = cacheVersionDir(root, slug, version);
140
+ if (await exists(dir)) {
141
+ await rm(dir, { recursive: true, force: true });
95
142
  }
96
143
  }
97
- return null;
144
+ const tgz = join(cacheRoot, `${slug}-${version}.tgz`);
145
+ if (await exists(tgz)) await rm(tgz, { force: true });
98
146
  }
99
147
 
100
- function resolveApiBase(pkg) {
101
- const fromEnv = process.env.SKY_UI_API_URL?.trim();
102
- if (fromEnv) return fromEnv.replace(/\/api\/?$/, "").replace(/\/$/, "");
103
- const fromPkg = pkg?.skyUiPro?.apiBase?.trim();
104
- if (fromPkg) return fromPkg.replace(/\/$/, "");
105
- return "https://library.sky-ui.com";
148
+ function tarballFileName(name, version) {
149
+ return `${name.replace(/^@/, "").replace("/", "-")}-${version}.tgz`;
106
150
  }
107
151
 
108
152
  function tarballUrl(apiBase, name, version) {
109
153
  const base = apiBase.replace(/\/$/, "");
110
154
  const scopePath = name.replace("/", "%2f");
111
- return `${base}/npm/${scopePath}/-/${name}-${version}.tgz`;
155
+ const file = tarballFileName(name, version);
156
+ return `${base}/npm/${scopePath}/-/${file}`;
112
157
  }
113
158
 
114
- function printMissingToken(name) {
159
+ function printMissingToken(name, apiBase) {
115
160
  console.warn(`
116
161
  [@sky.ui.pro] Missing install token — ${name} was not downloaded.
117
162
 
@@ -122,17 +167,37 @@ function printMissingToken(name) {
122
167
  CI / pipelines:
123
168
  export SKY_UI_AUTH_TOKEN=sk_ci_…
124
169
 
125
- Then re-run: npm install ${name}
170
+ Or add SKY_UI_PERSONAL_TOKEN / SKY_UI_AUTH_TOKEN to your project .env
171
+ API host (SKY_UI_API_URL): ${apiBase}
172
+
173
+ Then re-run: npm install "${name}"
126
174
  Docs: https://library.sky-ui.com/docs/getting-started
175
+
176
+ Maintainer escape hatch: SKY_UI_PRO_LENIENT=1
127
177
  `);
128
178
  }
129
179
 
130
- async function installFromTarball(tgzPath, slug, version) {
180
+ function printInvalidToken(name, result, apiBase) {
181
+ console.error(
182
+ `[@sky.ui.pro] License validation failed for ${name}: ${result.status ?? "inactive"}${
183
+ result.error ? ` — ${result.error}` : ""
184
+ }`,
185
+ );
186
+ console.error(`[@sky.ui.pro] API host: ${apiBase}`);
187
+ const renewalHint = formatRenewalEmailHint(result);
188
+ console.error(`
189
+ Renew or regenerate your token, then re-run: npm install "${name}"${renewalHint}
190
+ `);
191
+ }
192
+
193
+ async function installFromTarball(tgzPath, slug, version, stubManifest) {
131
194
  const stagingDir = await mkdtemp(join(tmpdir(), `sky.ui.pro-${slug}-${version}-`));
132
195
  try {
133
196
  await extract({ file: tgzPath, cwd: stagingDir, strip: 1 });
197
+ const artifactManifest = await readJson(join(stagingDir, "package.json"));
134
198
  await copyDirectoryContents(stagingDir, pkgRoot);
135
- await saveToCache(slug, version, stagingDir);
199
+ await applyHydratedManifest(pkgRoot, stubManifest, artifactManifest);
200
+ await saveToCache(slug, version, pkgRoot, artifactManifest);
136
201
  } finally {
137
202
  await rm(stagingDir, { recursive: true, force: true });
138
203
  }
@@ -141,6 +206,8 @@ async function installFromTarball(tgzPath, slug, version) {
141
206
  async function main() {
142
207
  if (process.env.SKY_UI_PRO_SKIP_POSTINSTALL === "1") return;
143
208
 
209
+ await loadInstallProjectEnv(pkgRoot);
210
+
144
211
  const pkg = await readJson(join(pkgRoot, "package.json"));
145
212
  const { name, version } = pkg;
146
213
  if (!name?.startsWith("@sky.ui.pro/") || !version) {
@@ -148,24 +215,53 @@ async function main() {
148
215
  return;
149
216
  }
150
217
 
218
+ const stubManifest = structuredClone(pkg);
151
219
  const slug = packageSlug(name);
220
+ const apiBase = resolveApiBase(pkg);
221
+ const strict = isInstallStrict();
222
+
152
223
  await enforceCachePolicy(slug, version);
153
224
 
154
- if (await restoreFromCache(slug, version, pkgRoot)) {
155
- console.log(`[@sky.ui.pro] Restored ${name}@${version} from cache.`);
156
- return;
225
+ const token = await resolveToken();
226
+ const cacheAvailable =
227
+ (await exists(cacheVersionDir(cacheRoot, slug, version))) ||
228
+ (await exists(cacheVersionDir(legacyCacheRoot, slug, version)));
229
+
230
+ if (cacheAvailable) {
231
+ if (!token) {
232
+ printMissingToken(name, apiBase);
233
+ if (strict) process.exit(1);
234
+ return;
235
+ }
236
+ const validation = await validateAccessTokenOnline(token, apiBase);
237
+ if (validation.active) {
238
+ if (await restoreFromCache(slug, version, pkgRoot, stubManifest)) {
239
+ await writeEntitlementMarker(pkgRoot, { name, version, status: validation.status ?? "active" });
240
+ console.log(`[@sky.ui.pro] Restored ${name}@${version} from cache.`);
241
+ return;
242
+ }
243
+ } else {
244
+ printInvalidToken(name, validation, apiBase);
245
+ await invalidateCache(slug, version);
246
+ if (strict) process.exit(1);
247
+ return;
248
+ }
157
249
  }
158
250
 
159
- const token = await resolveToken();
160
251
  if (!token) {
161
- printMissingToken(name);
162
- if (process.env.SKY_UI_PRO_STRICT === "1" || process.env.SKY_UI_AUTH_TOKEN) {
163
- process.exit(1);
164
- }
252
+ printMissingToken(name, apiBase);
253
+ if (strict) process.exit(1);
254
+ return;
255
+ }
256
+
257
+ const preDownloadValidation = await validateAccessTokenOnline(token, apiBase);
258
+ if (!preDownloadValidation.active) {
259
+ printInvalidToken(name, preDownloadValidation, apiBase);
260
+ if (strict) process.exit(1);
165
261
  return;
166
262
  }
167
263
 
168
- const url = tarballUrl(resolveApiBase(pkg), name, version);
264
+ const url = tarballUrl(apiBase, name, version);
169
265
  const res = await fetch(url, {
170
266
  headers: { Authorization: `Bearer ${token}`, Accept: "application/octet-stream" },
171
267
  });
@@ -180,7 +276,8 @@ async function main() {
180
276
  await mkdir(cacheRoot, { recursive: true });
181
277
  const tgzPath = join(cacheRoot, `${slug}-${version}.tgz`);
182
278
  await pipeline(res.body, createWriteStream(tgzPath));
183
- await installFromTarball(tgzPath, slug, version);
279
+ await installFromTarball(tgzPath, slug, version, stubManifest);
280
+ await writeEntitlementMarker(pkgRoot, { name, version, status: "active" });
184
281
  console.log(`[@sky.ui.pro] Installed ${name}@${version}`);
185
282
  }
186
283
 
@@ -0,0 +1,150 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Unified Sky UI Pro / CLI project `.env` loading.
4
+ * Source of truth — synced to @sky.ui/cli and @sky.ui.pro/* postinstall scripts before publish.
5
+ *
6
+ * Loads only `SKY_UI_*` keys. Shell env always wins (existing process.env is never overwritten).
7
+ * Search order: INIT_CWD → npm local prefix → SKY_UI_PROJECT_ROOT → cwd → project roots.
8
+ */
9
+ import { access, readFile } from "node:fs/promises";
10
+ import { dirname, join } from "node:path";
11
+
12
+ export const SKY_UI_ENV_PREFIX = "SKY_UI_";
13
+
14
+ /** Filenames tried in each project directory (later files override earlier within the same dir). */
15
+ export const SKY_UI_ENV_FILES = [".env", ".env.local"];
16
+
17
+ /** Documented variables (for README / tooling — not enforced here). */
18
+ export const SKY_UI_ENV_VARS = [
19
+ "SKY_UI_API_URL",
20
+ "SKY_UI_AUTH_TOKEN",
21
+ "SKY_UI_PERSONAL_TOKEN",
22
+ "SKY_UI_PROJECT_ROOT",
23
+ "SKY_UI_PRO_FRAMEWORK",
24
+ "SKY_UI_PRO_LENIENT",
25
+ "SKY_UI_PRO_LICENSE_LENIENT",
26
+ "SKY_UI_PRO_SKIP_POSTINSTALL",
27
+ "SKY_UI_PRO_STRICT",
28
+ "SKY_UI_NPM_REGISTRY",
29
+ ];
30
+
31
+ export async function pathExists(p) {
32
+ try {
33
+ await access(p);
34
+ return true;
35
+ } catch {
36
+ return false;
37
+ }
38
+ }
39
+
40
+ function normalizeDirKey(dir) {
41
+ return dir.replace(/\\/g, "/").replace(/\/+$/, "").toLowerCase();
42
+ }
43
+
44
+ function shouldLoadKey(key) {
45
+ return key.startsWith(SKY_UI_ENV_PREFIX);
46
+ }
47
+
48
+ function envAlreadySet(key) {
49
+ const v = process.env[key];
50
+ return typeof v === "string" && v.trim().length > 0;
51
+ }
52
+
53
+ function unquote(value) {
54
+ if (
55
+ (value.startsWith('"') && value.endsWith('"')) ||
56
+ (value.startsWith("'") && value.endsWith("'"))
57
+ ) {
58
+ return value.slice(1, -1);
59
+ }
60
+ return value;
61
+ }
62
+
63
+ /** Parse and apply one `.env` file. Does not overwrite non-empty process.env values. */
64
+ export async function loadEnvFile(envFilePath) {
65
+ try {
66
+ const raw = await readFile(envFilePath, "utf8");
67
+ for (const line of raw.split(/\r?\n/)) {
68
+ const trimmed = line.trim();
69
+ if (!trimmed || trimmed.startsWith("#")) continue;
70
+ const eq = trimmed.indexOf("=");
71
+ if (eq <= 0) continue;
72
+ const key = trimmed.slice(0, eq).trim();
73
+ if (!shouldLoadKey(key)) continue;
74
+ if (envAlreadySet(key)) continue;
75
+ process.env[key] = unquote(trimmed.slice(eq + 1).trim());
76
+ }
77
+ } catch {
78
+ /* missing or unreadable */
79
+ }
80
+ }
81
+
82
+ /** Load `.env` (+ `.env.local`) from a project directory. */
83
+ export async function loadProjectEnv(cwd = process.cwd()) {
84
+ for (const name of SKY_UI_ENV_FILES) {
85
+ await loadEnvFile(join(cwd, name));
86
+ }
87
+ }
88
+
89
+ /** Walk up to find the nearest directory containing `package.json`. */
90
+ export async function findProjectRoot(startDir, maxDepth = 12) {
91
+ let dir = startDir;
92
+ for (let i = 0; i < maxDepth; i++) {
93
+ if (await pathExists(join(dir, "package.json"))) return dir;
94
+ const parent = dirname(dir);
95
+ if (parent === dir) break;
96
+ dir = parent;
97
+ }
98
+ return startDir;
99
+ }
100
+
101
+ /**
102
+ * Directories to scan for `.env`, in priority order (first match wins per key;
103
+ * later dirs only fill keys still unset).
104
+ */
105
+ export async function collectEnvSearchDirs(options = {}) {
106
+ const { pkgRoot, extraDirs = [] } = options;
107
+ const seen = new Set();
108
+ const ordered = [];
109
+
110
+ const push = (dir) => {
111
+ if (!dir || typeof dir !== "string") return;
112
+ const key = normalizeDirKey(dir);
113
+ if (seen.has(key)) return;
114
+ seen.add(key);
115
+ ordered.push(dir);
116
+ };
117
+
118
+ push(process.env.INIT_CWD);
119
+ push(process.env.npm_config_local_prefix);
120
+ push(process.env.SKY_UI_PROJECT_ROOT?.trim());
121
+ push(process.cwd());
122
+
123
+ const cwdRoot = await findProjectRoot(process.cwd());
124
+ push(cwdRoot);
125
+
126
+ if (pkgRoot) {
127
+ push(await findProjectRoot(dirname(pkgRoot)));
128
+ }
129
+
130
+ for (const dir of extraDirs) {
131
+ push(dir);
132
+ }
133
+
134
+ return ordered;
135
+ }
136
+
137
+ /**
138
+ * Load Sky UI env from consumer project `.env` files.
139
+ * @param {{ pkgRoot?: string, extraDirs?: string[] }} [options]
140
+ */
141
+ export async function loadConsumerProjectEnv(options = {}) {
142
+ for (const dir of await collectEnvSearchDirs(options)) {
143
+ await loadProjectEnv(dir);
144
+ }
145
+ }
146
+
147
+ /** @deprecated Alias used by postinstall — prefer loadConsumerProjectEnv */
148
+ export async function loadInstallProjectEnv(pkgRoot) {
149
+ return loadConsumerProjectEnv({ pkgRoot });
150
+ }