@doki-land/nifty 0.0.0

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.
@@ -0,0 +1,77 @@
1
+ import { loadNiftyNative } from "../native.js";
2
+ import { authPayload, parseAuthArgs } from "./authArgs.js";
3
+
4
+ export type TrustReport = {
5
+ root: string;
6
+ configured: string[];
7
+ skipped: string[];
8
+ failed: string[];
9
+ };
10
+
11
+ export async function runTrust(argv: string[]): Promise<void> {
12
+ const options = parseTrustArgs(argv);
13
+ const native = loadNiftyNative();
14
+ const report = native.publisher["trust-workspace"]({
15
+ cwd: options.cwd,
16
+ dryRun: options.dryRun,
17
+ only: options.only,
18
+ ...authPayload(options.auth),
19
+ });
20
+ printReport(report);
21
+ }
22
+
23
+ function parseTrustArgs(argv: string[]) {
24
+ let cwd: string | undefined;
25
+ let dryRun = false;
26
+ let only: string | undefined;
27
+ const auth = parseAuthArgs(argv);
28
+
29
+ for (let i = 0; i < argv.length; i += 1) {
30
+ const arg = argv[i];
31
+ if (arg === "--dry-run") {
32
+ dryRun = true;
33
+ } else if (arg === "-C" || arg === "--cwd") {
34
+ cwd = argv[++i];
35
+ } else if (arg === "--only") {
36
+ only = argv[++i];
37
+ } else if (arg === "-h" || arg === "--help") {
38
+ printTrustHelp();
39
+ process.exit(0);
40
+ }
41
+ }
42
+
43
+ return { cwd, dryRun, only, auth };
44
+ }
45
+
46
+ function printReport(report: TrustReport): void {
47
+ console.log(`trust workspace ${report.root}`);
48
+ console.log(` configured: ${report.configured.length}`);
49
+ for (const name of report.configured) {
50
+ console.log(` + ${name}`);
51
+ }
52
+ console.log(` skipped: ${report.skipped.length}`);
53
+ for (const name of report.skipped) {
54
+ console.log(` = ${name}`);
55
+ }
56
+ if (report.failed.length > 0) {
57
+ console.log(` failed: ${report.failed.length}`);
58
+ for (const name of report.failed) {
59
+ console.log(` ! ${name}`);
60
+ }
61
+ }
62
+ }
63
+
64
+ function printTrustHelp(): void {
65
+ console.log(`nifty trust — configure npm Trusted Publisher for workspace packages
66
+
67
+ Reads NPM_TOTP_SECRET / NPM_OTP / NPM_TOKEN from env or .env.placeholder.local.
68
+
69
+ Usage:
70
+ nifty trust
71
+ nifty trust --only @doki-land/nifty
72
+ nifty trust --dry-run
73
+ nifty trust --totp-secret <base32>
74
+ nifty trust --otp 123456
75
+ nifty trust -C <cwd>
76
+ `);
77
+ }
@@ -0,0 +1,182 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { stdin as input, stdout as output } from "node:process";
3
+ import { createInterface } from "node:readline/promises";
4
+ import { dirname } from "node:path";
5
+
6
+ import { detectProjectLayout, discoverPackageDirs } from "../config/detectProject.js";
7
+
8
+ type UpdateOptions = {
9
+ interactive: boolean;
10
+ cwd?: string;
11
+ };
12
+
13
+ type CargoUpgrade = {
14
+ crate: string;
15
+ from: string;
16
+ to: string;
17
+ };
18
+
19
+ type NpmOutdated = {
20
+ name: string;
21
+ current: string;
22
+ wanted: string;
23
+ latest: string;
24
+ };
25
+
26
+ export async function runUpdate(argv: string[]): Promise<void> {
27
+ const options = parseUpdateArgs(argv);
28
+ const cwd = options.cwd ?? process.cwd();
29
+ const layout = detectProjectLayout(cwd);
30
+
31
+ if (layout.kind === "unknown") {
32
+ throw new Error(`no Cargo.toml or package.json found from ${cwd}`);
33
+ }
34
+
35
+ if (layout.kind === "cargo" || layout.kind === "hybrid") {
36
+ await updateCargo(layout, options.interactive);
37
+ }
38
+ if (layout.kind === "npm" || layout.kind === "hybrid") {
39
+ await updateNpm(layout, options.interactive);
40
+ }
41
+ }
42
+
43
+ function parseUpdateArgs(argv: string[]): UpdateOptions {
44
+ let interactive = false;
45
+ let cwd: string | undefined;
46
+ for (let i = 0; i < argv.length; i += 1) {
47
+ const arg = argv[i];
48
+ if (arg === "-i" || arg === "--interactive") {
49
+ interactive = true;
50
+ } else if (arg === "-C" || arg === "--cwd") {
51
+ cwd = argv[++i];
52
+ }
53
+ }
54
+ return { interactive, cwd };
55
+ }
56
+
57
+ function cargoRoot(layout: ReturnType<typeof detectProjectLayout>): string | undefined {
58
+ return (
59
+ layout.cargoWorkspaceRoot ??
60
+ (layout.cargoManifest ? dirname(layout.cargoManifest) : undefined)
61
+ );
62
+ }
63
+
64
+ async function updateCargo(layout: ReturnType<typeof detectProjectLayout>, interactive: boolean): Promise<void> {
65
+ const root = cargoRoot(layout);
66
+ if (!root) {
67
+ throw new Error("cargo project not found");
68
+ }
69
+
70
+ const dry = spawnSync("cargo", ["upgrade", "--workspace", "--dry-run"], {
71
+ cwd: root,
72
+ encoding: "utf8",
73
+ });
74
+ const combined = `${dry.stdout ?? ""}${dry.stderr ?? ""}`;
75
+ const upgrades = parseCargoUpgrades(combined);
76
+ if (dry.status !== 0 && !combined.includes("no upgrades found") && upgrades.length === 0) {
77
+ throw new Error(`cargo upgrade --dry-run failed:\n${combined}`);
78
+ }
79
+
80
+ if (upgrades.length === 0) {
81
+ console.log("cargo: dependencies already up to date");
82
+ return;
83
+ }
84
+
85
+ if (interactive) {
86
+ const selected = await pickItems(
87
+ "Select cargo upgrades",
88
+ upgrades.map((item) => `${item.crate} ${item.from} -> ${item.to}`),
89
+ );
90
+ for (const index of selected) {
91
+ const item = upgrades[index];
92
+ run("cargo", ["upgrade", "--workspace", "-p", item.crate], root);
93
+ }
94
+ } else {
95
+ run("cargo", ["upgrade", "--workspace"], root);
96
+ }
97
+ }
98
+
99
+ async function updateNpm(layout: ReturnType<typeof detectProjectLayout>, interactive: boolean): Promise<void> {
100
+ const packages = discoverPackageDirs(layout);
101
+ if (packages.length === 0) {
102
+ console.log("npm: no package.json directories found");
103
+ return;
104
+ }
105
+ for (const packageDir of packages) {
106
+ await updateNpmDir(packageDir, interactive);
107
+ }
108
+ }
109
+
110
+ async function updateNpmDir(packageDir: string, interactive: boolean): Promise<void> {
111
+ const outdated = listNpmOutdated(packageDir);
112
+ if (outdated.length === 0) {
113
+ console.log(`npm (${packageDir}): dependencies already up to date`);
114
+ return;
115
+ }
116
+
117
+ if (interactive) {
118
+ const selected = await pickItems(
119
+ `Select npm upgrades (${packageDir})`,
120
+ outdated.map((item) => `${item.name} ${item.current} -> ${item.wanted}`),
121
+ );
122
+ for (const index of selected) {
123
+ const item = outdated[index];
124
+ run("npm", ["install", `${item.name}@${item.wanted}`], packageDir);
125
+ }
126
+ } else {
127
+ run("npm", ["update"], packageDir);
128
+ }
129
+ }
130
+
131
+ function parseCargoUpgrades(text: string): CargoUpgrade[] {
132
+ const upgrades: CargoUpgrade[] = [];
133
+ for (const line of text.split(/\r?\n/)) {
134
+ const match = line.match(/^\s*(\S+)\s+v(\S+)\s+->\s+v(\S+)/);
135
+ if (match) {
136
+ upgrades.push({ crate: match[1], from: match[2], to: match[3] });
137
+ }
138
+ }
139
+ return upgrades;
140
+ }
141
+
142
+ function listNpmOutdated(packageDir: string): NpmOutdated[] {
143
+ const result = spawnSync("npm", ["outdated", "--json"], { cwd: packageDir, encoding: "utf8" });
144
+ if (!result.stdout?.trim()) {
145
+ return [];
146
+ }
147
+ const parsed = JSON.parse(result.stdout) as Record<
148
+ string,
149
+ { current: string; wanted: string; latest: string }
150
+ >;
151
+ return Object.entries(parsed).map(([name, entry]) => ({
152
+ name,
153
+ current: entry.current,
154
+ wanted: entry.wanted,
155
+ latest: entry.latest,
156
+ }));
157
+ }
158
+
159
+ async function pickItems(prompt: string, labels: string[]): Promise<number[]> {
160
+ console.log(prompt);
161
+ for (const [index, label] of labels.entries()) {
162
+ console.log(` [${index}] ${label}`);
163
+ }
164
+ const rl = createInterface({ input, output });
165
+ const answer = await rl.question("Indexes (comma-separated, empty = all): ");
166
+ rl.close();
167
+ const trimmed = answer.trim();
168
+ if (!trimmed) {
169
+ return labels.map((_, index) => index);
170
+ }
171
+ return trimmed
172
+ .split(",")
173
+ .map((part) => Number.parseInt(part.trim(), 10))
174
+ .filter((index) => Number.isInteger(index) && index >= 0 && index < labels.length);
175
+ }
176
+
177
+ function run(command: string, args: string[], cwd: string): void {
178
+ const result = spawnSync(command, args, { cwd, stdio: "inherit" });
179
+ if (result.status !== 0) {
180
+ throw new Error(`${command} ${args.join(" ")} failed`);
181
+ }
182
+ }
@@ -0,0 +1,280 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { cpSync, existsSync, mkdtempSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { basename, join, relative, resolve } from "node:path";
5
+
6
+ import { detectProjectLayout } from "../config/detectProject.js";
7
+ import { loadNiftyNative } from "../native.js";
8
+
9
+ const USER_AGENT = "nifty-upload";
10
+
11
+ type UploadOptions = {
12
+ release: boolean;
13
+ pages: boolean;
14
+ both: boolean;
15
+ dir: string;
16
+ tag?: string;
17
+ repo?: string;
18
+ token?: string;
19
+ name?: string;
20
+ notes?: string;
21
+ notesFile?: string;
22
+ draft: boolean;
23
+ generateNotes: boolean;
24
+ githubAction: boolean;
25
+ cwd?: string;
26
+ };
27
+
28
+ type GithubRelease = {
29
+ id: number;
30
+ };
31
+
32
+ export async function runUpload(argv: string[]): Promise<void> {
33
+ const options = parseUploadArgs(argv);
34
+ if (options.githubAction) {
35
+ applyGithubActionDefaults(options);
36
+ }
37
+
38
+ const token = options.token ?? process.env.GITHUB_TOKEN;
39
+ if (!token) {
40
+ throw new Error("GitHub token is required (set --token or GITHUB_TOKEN)");
41
+ }
42
+
43
+ const cwd = options.cwd ?? process.cwd();
44
+ const layout = detectProjectLayout(cwd);
45
+ const native = loadNiftyNative();
46
+ const repo =
47
+ options.repo ??
48
+ native.git["detect-github-repo"](layout.root) ??
49
+ process.env.GITHUB_REPOSITORY;
50
+ if (!repo) {
51
+ throw new Error("could not detect GitHub repo (use --repo owner/name)");
52
+ }
53
+
54
+ const tag = options.tag ?? process.env.GITHUB_REF_NAME;
55
+ if (!tag) {
56
+ throw new Error("release tag is required (use --tag or push a git tag)");
57
+ }
58
+
59
+ const target = parseTarget(options);
60
+ const notes = await resolveNotes(options, layout.root, tag, native);
61
+
62
+ if (target === "release" || target === "both") {
63
+ await uploadReleaseAssets(repo, token, tag, options, notes);
64
+ }
65
+ if (target === "pages" || target === "both") {
66
+ await deployGithubPages(repo, token, options.dir);
67
+ }
68
+ }
69
+
70
+ function parseUploadArgs(argv: string[]): UploadOptions {
71
+ const options: UploadOptions = {
72
+ release: false,
73
+ pages: false,
74
+ both: false,
75
+ dir: "dist",
76
+ draft: false,
77
+ generateNotes: true,
78
+ githubAction: false,
79
+ };
80
+
81
+ for (let i = 0; i < argv.length; i += 1) {
82
+ const arg = argv[i];
83
+ if (arg === "--release") options.release = true;
84
+ else if (arg === "--pages") options.pages = true;
85
+ else if (arg === "--both") options.both = true;
86
+ else if (arg === "-d" || arg === "--dir") options.dir = argv[++i];
87
+ else if (arg === "-t" || arg === "--tag") options.tag = argv[++i];
88
+ else if (arg === "--repo") options.repo = argv[++i];
89
+ else if (arg === "--token") options.token = argv[++i];
90
+ else if (arg === "--name") options.name = argv[++i];
91
+ else if (arg === "--notes") options.notes = argv[++i];
92
+ else if (arg === "--notes-file") options.notesFile = argv[++i];
93
+ else if (arg === "--draft") options.draft = true;
94
+ else if (arg === "--no-generate-notes") options.generateNotes = false;
95
+ else if (arg === "--github-action") options.githubAction = true;
96
+ else if (arg === "-C" || arg === "--cwd") options.cwd = argv[++i];
97
+ }
98
+
99
+ return options;
100
+ }
101
+
102
+ function parseTarget(options: UploadOptions): "release" | "pages" | "both" {
103
+ if (options.both) return "both";
104
+ if (options.pages) return "pages";
105
+ if (options.release) return "release";
106
+ throw new Error("specify --release, --pages, or --both");
107
+ }
108
+
109
+ function applyGithubActionDefaults(options: UploadOptions): void {
110
+ options.token ??= process.env.GITHUB_TOKEN;
111
+ options.repo ??= process.env.GITHUB_REPOSITORY;
112
+ options.tag ??= process.env.GITHUB_REF_NAME;
113
+ options.cwd ??= process.env.GITHUB_WORKSPACE;
114
+ }
115
+
116
+ async function resolveNotes(
117
+ options: UploadOptions,
118
+ repoRoot: string,
119
+ tag: string,
120
+ native: ReturnType<typeof loadNiftyNative>,
121
+ ): Promise<string> {
122
+ if (options.notes) return options.notes;
123
+ if (options.notesFile) return readFileSync(options.notesFile, "utf8");
124
+ if (!options.generateNotes) return "";
125
+ try {
126
+ const range = native.git["resolve-range"](repoRoot, tag.replace(/^v/, ""), undefined, tag);
127
+ const commits = native.git["collect-commits"](repoRoot, range.fromRef, range.toRef);
128
+ return commits.map((commit) => `- ${commit.body}`).join("\n");
129
+ } catch {
130
+ return "";
131
+ }
132
+ }
133
+
134
+ async function uploadReleaseAssets(
135
+ repo: string,
136
+ token: string,
137
+ tag: string,
138
+ options: UploadOptions,
139
+ notes: string,
140
+ ): Promise<void> {
141
+ const [owner, name] = splitRepo(repo);
142
+ let release = await apiReleaseByTag(owner, name, tag, token);
143
+ if (!release) {
144
+ release = await apiCreateRelease(owner, name, tag, options.name ?? tag, notes, options.draft, token);
145
+ }
146
+ const dir = resolve(options.cwd ?? process.cwd(), options.dir);
147
+ for (const file of collectFiles(dir)) {
148
+ const asset = relative(dir, file).split(/[/\\]/).join("-") || basename(file);
149
+ await apiUploadAsset(owner, name, release.id, asset, file, token);
150
+ console.log(`release: uploaded ${asset}`);
151
+ }
152
+ }
153
+
154
+ async function deployGithubPages(repo: string, token: string, dir: string): Promise<void> {
155
+ const [owner, name] = splitRepo(repo);
156
+ const source = resolve(dir);
157
+ const work = mkdtempSync(join(tmpdir(), "nifty-pages-"));
158
+ cpSync(source, work, { recursive: true });
159
+ const nojekyll = join(work, ".nojekyll");
160
+ if (!existsSync(nojekyll)) {
161
+ writeFileSync(nojekyll, "");
162
+ }
163
+ git(work, ["init"]);
164
+ git(work, ["config", "user.name", "github-actions[bot]"]);
165
+ git(work, ["config", "user.email", "41898282+github-actions[bot]@users.noreply.github.com"]);
166
+ git(work, ["add", "."]);
167
+ git(work, ["commit", "-m", "Deploy GitHub Pages"]);
168
+ const remote = `https://x-access-token:${token}@github.com/${owner}/${name}.git`;
169
+ git(work, ["remote", "add", "origin", remote]);
170
+ git(work, ["push", "--force", "origin", "HEAD:gh-pages"]);
171
+ console.log(`pages: pushed ${source} to gh-pages`);
172
+ }
173
+
174
+ function collectFiles(dir: string): string[] {
175
+ const out: string[] = [];
176
+ for (const entry of walk(dir)) {
177
+ if (statSync(entry).isFile()) {
178
+ out.push(entry);
179
+ }
180
+ }
181
+ return out;
182
+ }
183
+
184
+ function walk(dir: string): string[] {
185
+ const files: string[] = [];
186
+ for (const entry of readdirSync(dir)) {
187
+ const path = join(dir, entry);
188
+ if (statSync(path).isDirectory()) {
189
+ files.push(...walk(path));
190
+ } else {
191
+ files.push(path);
192
+ }
193
+ }
194
+ return files;
195
+ }
196
+
197
+ function splitRepo(repo: string): [string, string] {
198
+ const parts = repo.split("/");
199
+ if (parts.length !== 2 || !parts[0] || !parts[1]) {
200
+ throw new Error(`invalid repo ${repo}, expected owner/name`);
201
+ }
202
+ return [parts[0], parts[1]];
203
+ }
204
+
205
+ async function apiReleaseByTag(
206
+ owner: string,
207
+ name: string,
208
+ tag: string,
209
+ token: string,
210
+ ): Promise<GithubRelease | null> {
211
+ const response = await fetch(
212
+ `https://api.github.com/repos/${owner}/${name}/releases/tags/${encodeURIComponent(tag)}`,
213
+ { headers: githubHeaders(token) },
214
+ );
215
+ if (response.status === 404) return null;
216
+ if (!response.ok) {
217
+ throw new Error(`GitHub API ${response.status}: ${await response.text()}`);
218
+ }
219
+ return (await response.json()) as GithubRelease;
220
+ }
221
+
222
+ async function apiCreateRelease(
223
+ owner: string,
224
+ name: string,
225
+ tag: string,
226
+ title: string,
227
+ body: string,
228
+ draft: boolean,
229
+ token: string,
230
+ ): Promise<GithubRelease> {
231
+ const response = await fetch(`https://api.github.com/repos/${owner}/${name}/releases`, {
232
+ method: "POST",
233
+ headers: { ...githubHeaders(token), "Content-Type": "application/json" },
234
+ body: JSON.stringify({ tag_name: tag, name: title, body, draft }),
235
+ });
236
+ if (!response.ok) {
237
+ throw new Error(`GitHub API ${response.status}: ${await response.text()}`);
238
+ }
239
+ return (await response.json()) as GithubRelease;
240
+ }
241
+
242
+ async function apiUploadAsset(
243
+ owner: string,
244
+ name: string,
245
+ releaseId: number,
246
+ assetName: string,
247
+ filePath: string,
248
+ token: string,
249
+ ): Promise<void> {
250
+ const url = `https://uploads.github.com/repos/${owner}/${name}/releases/${releaseId}/assets?name=${encodeURIComponent(assetName)}`;
251
+ const body = readFileSync(filePath);
252
+ const response = await fetch(url, {
253
+ method: "POST",
254
+ headers: {
255
+ ...githubHeaders(token),
256
+ "Content-Type": "application/octet-stream",
257
+ "Content-Length": String(body.length),
258
+ },
259
+ body,
260
+ });
261
+ if (!response.ok) {
262
+ throw new Error(`GitHub upload ${response.status}: ${await response.text()}`);
263
+ }
264
+ }
265
+
266
+ function githubHeaders(token: string): Record<string, string> {
267
+ return {
268
+ Accept: "application/vnd.github+json",
269
+ Authorization: `Bearer ${token}`,
270
+ "User-Agent": USER_AGENT,
271
+ "X-GitHub-Api-Version": "2022-11-28",
272
+ };
273
+ }
274
+
275
+ function git(cwd: string, args: string[]): void {
276
+ const result = spawnSync("git", args, { cwd, stdio: "inherit" });
277
+ if (result.status !== 0) {
278
+ throw new Error(`git ${args.join(" ")} failed`);
279
+ }
280
+ }
@@ -0,0 +1,124 @@
1
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
2
+ import { join, resolve } from "node:path";
3
+
4
+ import { detectProjectLayout } from "../config/detectProject.js";
5
+
6
+ export type WorkspacePackage = {
7
+ name: string;
8
+ version: string;
9
+ dir: string;
10
+ manifestPath: string;
11
+ private: boolean;
12
+ manifest: PackageManifest;
13
+ };
14
+
15
+ export type PackageManifest = {
16
+ name: string;
17
+ version: string;
18
+ private?: boolean;
19
+ dependencies?: Record<string, string>;
20
+ devDependencies?: Record<string, string>;
21
+ optionalDependencies?: Record<string, string>;
22
+ peerDependencies?: Record<string, string>;
23
+ };
24
+
25
+ const DEPENDENCY_FIELDS = [
26
+ "dependencies",
27
+ "devDependencies",
28
+ "optionalDependencies",
29
+ "peerDependencies",
30
+ ] as const;
31
+
32
+ export function findWorkspaceRoot(cwd: string): string {
33
+ const layout = detectProjectLayout(cwd);
34
+ const candidates = [layout.root, resolve(cwd)];
35
+ for (const candidate of candidates) {
36
+ const crates = join(candidate, "projects", "crates");
37
+ const packages = join(candidate, "projects", "packages");
38
+ if (existsSync(crates) && existsSync(packages)) {
39
+ return candidate;
40
+ }
41
+ }
42
+ throw new Error("could not find Nifty workspace (expected projects/crates and projects/packages)");
43
+ }
44
+
45
+ export function listWorkspacePackages(root: string): WorkspacePackage[] {
46
+ const packagesDir = join(root, "projects", "packages");
47
+ if (!existsSync(packagesDir)) {
48
+ return [];
49
+ }
50
+
51
+ const packages: WorkspacePackage[] = [];
52
+ for (const entry of readdirSync(packagesDir, { withFileTypes: true })) {
53
+ if (!entry.isDirectory() || entry.name === "node_modules") {
54
+ continue;
55
+ }
56
+ const manifestPath = join(packagesDir, entry.name, "package.json");
57
+ if (!existsSync(manifestPath)) {
58
+ continue;
59
+ }
60
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf8")) as PackageManifest;
61
+ if (!manifest.name || !manifest.version) {
62
+ throw new Error(`package.json missing name or version: ${manifestPath}`);
63
+ }
64
+ packages.push({
65
+ name: manifest.name,
66
+ version: manifest.version,
67
+ dir: join(packagesDir, entry.name),
68
+ manifestPath,
69
+ private: manifest.private === true,
70
+ manifest,
71
+ });
72
+ }
73
+
74
+ return packages.sort((a, b) => a.name.localeCompare(b.name));
75
+ }
76
+
77
+ export function collectInternalDependencyNames(
78
+ pkg: WorkspacePackage,
79
+ byName: Map<string, WorkspacePackage>,
80
+ ): Set<string> {
81
+ const deps = new Set<string>();
82
+ for (const field of DEPENDENCY_FIELDS) {
83
+ const entries = pkg.manifest[field];
84
+ if (!entries) {
85
+ continue;
86
+ }
87
+ for (const [depName, spec] of Object.entries(entries)) {
88
+ if (byName.has(depName)) {
89
+ deps.add(depName);
90
+ continue;
91
+ }
92
+ const resolved = resolveWorkspaceDependencyName(spec, pkg.dir, byName);
93
+ if (resolved) {
94
+ deps.add(resolved);
95
+ }
96
+ }
97
+ }
98
+ return deps;
99
+ }
100
+
101
+ function resolveWorkspaceDependencyName(
102
+ spec: string,
103
+ packageDir: string,
104
+ byName: Map<string, WorkspacePackage>,
105
+ ): string | undefined {
106
+ if (spec.startsWith("workspace:")) {
107
+ return undefined;
108
+ }
109
+ if (!spec.startsWith("file:")) {
110
+ return undefined;
111
+ }
112
+ const targetDir = resolve(packageDir, spec.slice("file:".length));
113
+ for (const candidate of byName.values()) {
114
+ if (candidate.dir === targetDir) {
115
+ return candidate.name;
116
+ }
117
+ }
118
+ const manifestPath = join(targetDir, "package.json");
119
+ if (!existsSync(manifestPath)) {
120
+ return undefined;
121
+ }
122
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf8")) as PackageManifest;
123
+ return manifest.name;
124
+ }
@@ -0,0 +1,12 @@
1
+ import type { NiftyConfig, NiftyConfigEnv } from "./types.js";
2
+
3
+ /** Type-safe helper for `nifty.config.ts` / `nifty.config.js`. */
4
+ export function defineConfig(config: NiftyConfig): NiftyConfig;
5
+ export function defineConfig(
6
+ config: (env: NiftyConfigEnv) => NiftyConfig | Promise<NiftyConfig>,
7
+ ): (env: NiftyConfigEnv) => NiftyConfig | Promise<NiftyConfig>;
8
+ export function defineConfig(
9
+ config: NiftyConfig | ((env: NiftyConfigEnv) => NiftyConfig | Promise<NiftyConfig>),
10
+ ): NiftyConfig | ((env: NiftyConfigEnv) => NiftyConfig | Promise<NiftyConfig>) {
11
+ return config;
12
+ }