@dujavi/ai-md 0.5.0 → 0.6.1

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/lib/commands.js CHANGED
@@ -5,60 +5,24 @@ const path = require("path");
5
5
  const {
6
6
  resolveConfig,
7
7
  templatePath,
8
- symlinkState,
9
- agentSkillTargets,
8
+ writeMachineConfig,
9
+ gitClone,
10
+ gitPull,
11
+ gitPush,
10
12
  } = require("./config");
11
13
  const { collectStatus, statusHelp, collectProjects } = require("./status");
12
-
13
- function ensureDir(p) {
14
- fs.mkdirSync(p, { recursive: true });
15
- }
16
-
17
- function linkPath(target, link, { force = false, dryRun = false } = {}) {
18
- ensureDir(path.dirname(link));
19
- ensureDir(target);
20
- const state = symlinkState(link, target);
21
- if (state.state === "ok") {
22
- return { path: link, action: "ok", target };
23
- }
24
- if (state.state === "not_symlink" && !force) {
25
- const err = new Error(
26
- `${link} exists and is not a symlink (use --force to replace)`
27
- );
28
- err.code = "EEXIST";
29
- throw err;
30
- }
31
- if (dryRun) {
32
- return { path: link, action: state.state === "missing" ? "would_link" : "would_repair", target };
33
- }
34
- if (fs.existsSync(link) || state.state !== "missing") {
35
- try {
36
- fs.lstatSync(link);
37
- fs.rmSync(link, { recursive: true, force: true });
38
- } catch {
39
- /* missing */
40
- }
41
- }
42
- fs.symlinkSync(target, link);
43
- return {
44
- path: link,
45
- action: state.state === "missing" ? "linked" : "repaired",
46
- target,
47
- };
48
- }
49
-
50
- function ensureCursorLinks(cfg, opts = {}) {
51
- return [
52
- linkPath(cfg.skillsDir, cfg.cursorSkills, opts),
53
- linkPath(cfg.rulesDir, cfg.cursorRules, opts),
54
- ];
55
- }
56
-
57
- function ensureAgentSkillLinks(cfg, agents, opts = {}) {
58
- return agentSkillTargets(cfg.home, agents)
59
- .filter((t) => t.name !== "cursor") // cursor handled via ensureCursorLinks
60
- .map((t) => linkPath(cfg.skillsDir, t.path, opts));
61
- }
14
+ const { linkPath, ensureDir, wslWarnings, defaultLinkMode } = require("./link");
15
+ const { runBuild, distRoot, distDirty } = require("./build");
16
+ const { runRescue } = require("./rescue");
17
+ const {
18
+ selectHarnesses,
19
+ resolveHarness,
20
+ listBuiltinIds,
21
+ getBuiltin,
22
+ ensureAgentSourceDirs,
23
+ isHarnessInstalled,
24
+ } = require("./harnesses");
25
+ const { seedSkeleton, initRepo, SKELETON_VERSION, isEmptyDir } = require("./skeleton");
62
26
 
63
27
  function ensureGitignore(repoPath, { dryRun = false } = {}) {
64
28
  const gitignore = path.join(repoPath, ".gitignore");
@@ -84,6 +48,37 @@ function copyDir(src, dest, { dryRun = false } = {}) {
84
48
  return { action: "copied", from: src, to: dest };
85
49
  }
86
50
 
51
+ function countEntries(dir) {
52
+ if (!fs.existsSync(dir)) return 0;
53
+ return fs.readdirSync(dir).filter((n) => !n.startsWith(".")).length;
54
+ }
55
+
56
+ function mergeTemplate(templateDir, projectDir, { dryRun = false } = {}) {
57
+ const actions = [];
58
+ for (const kind of ["rules", "skills"]) {
59
+ const srcRoot = path.join(templateDir, kind);
60
+ const destRoot = path.join(projectDir, kind);
61
+ if (!fs.existsSync(srcRoot)) continue;
62
+ ensureDir(destRoot);
63
+ for (const name of fs.readdirSync(srcRoot)) {
64
+ if (name.startsWith(".")) continue;
65
+ const src = path.join(srcRoot, name);
66
+ const dest = path.join(destRoot, name);
67
+ if (fs.existsSync(dest)) {
68
+ actions.push({ action: "keep", path: dest });
69
+ continue;
70
+ }
71
+ if (dryRun) {
72
+ actions.push({ action: "would_add", path: dest, from: src });
73
+ } else {
74
+ fs.cpSync(src, dest, { recursive: true });
75
+ actions.push({ action: "added", path: dest, from: src });
76
+ }
77
+ }
78
+ }
79
+ return actions;
80
+ }
81
+
87
82
  function initProject({
88
83
  repo,
89
84
  name,
@@ -92,8 +87,8 @@ function initProject({
92
87
  dryRun = false,
93
88
  } = {}) {
94
89
  const cfg = resolveConfig();
95
- if (!fs.existsSync(path.join(cfg.dir, ".git"))) {
96
- const err = new Error(`${cfg.dir} is not a git repo; run ai-md install first`);
90
+ if (!fs.existsSync(path.join(cfg.dir, ".git")) && !fs.existsSync(cfg.dir)) {
91
+ const err = new Error(`${cfg.dir} missing; run ai-md init or ai-md install first`);
97
92
  err.code = "ENOENT";
98
93
  throw err;
99
94
  }
@@ -131,7 +126,13 @@ function initProject({
131
126
  }
132
127
 
133
128
  const link = path.join(absRepo, ".cursor");
134
- actions.push(linkPath(projectDir, link, { force, dryRun }));
129
+ actions.push(
130
+ linkPath(projectDir, link, {
131
+ force,
132
+ dryRun,
133
+ linkMode: cfg.linkMode,
134
+ })
135
+ );
135
136
  actions.push(ensureGitignore(absRepo, { dryRun }));
136
137
 
137
138
  return {
@@ -144,37 +145,6 @@ function initProject({
144
145
  };
145
146
  }
146
147
 
147
- function countEntries(dir) {
148
- if (!fs.existsSync(dir)) return 0;
149
- return fs.readdirSync(dir).filter((n) => !n.startsWith(".")).length;
150
- }
151
-
152
- function mergeTemplate(templateDir, projectDir, { dryRun = false } = {}) {
153
- const actions = [];
154
- for (const kind of ["rules", "skills"]) {
155
- const srcRoot = path.join(templateDir, kind);
156
- const destRoot = path.join(projectDir, kind);
157
- if (!fs.existsSync(srcRoot)) continue;
158
- ensureDir(destRoot);
159
- for (const name of fs.readdirSync(srcRoot)) {
160
- if (name.startsWith(".")) continue;
161
- const src = path.join(srcRoot, name);
162
- const dest = path.join(destRoot, name);
163
- if (fs.existsSync(dest)) {
164
- actions.push({ action: "keep", path: dest });
165
- continue;
166
- }
167
- if (dryRun) {
168
- actions.push({ action: "would_add", path: dest, from: src });
169
- } else {
170
- fs.cpSync(src, dest, { recursive: true });
171
- actions.push({ action: "added", path: dest, from: src });
172
- }
173
- }
174
- }
175
- return actions;
176
- }
177
-
178
148
  function applyTemplate({ project, from = "base", dryRun = false } = {}) {
179
149
  const cfg = resolveConfig();
180
150
  if (!project) {
@@ -200,11 +170,6 @@ function applyTemplate({ project, from = "base", dryRun = false } = {}) {
200
170
 
201
171
  function linkProject({ repo, name, force = false, dryRun = false } = {}) {
202
172
  const cfg = resolveConfig();
203
- if (!fs.existsSync(path.join(cfg.dir, ".git"))) {
204
- const err = new Error(`${cfg.dir} is not a git repo; run ai-md install first`);
205
- err.code = "ENOENT";
206
- throw err;
207
- }
208
173
  if (!repo || !fs.existsSync(repo)) {
209
174
  const err = new Error("missing or invalid --repo <path>");
210
175
  err.code = "EINVAL";
@@ -218,37 +183,376 @@ function linkProject({ repo, name, force = false, dryRun = false } = {}) {
218
183
  ensureDir(path.join(projectDir, "skills"));
219
184
  }
220
185
  const actions = [
221
- linkPath(projectDir, path.join(absRepo, ".cursor"), { force, dryRun }),
186
+ linkPath(projectDir, path.join(absRepo, ".cursor"), {
187
+ force,
188
+ dryRun,
189
+ linkMode: cfg.linkMode,
190
+ }),
222
191
  ensureGitignore(absRepo, { dryRun }),
223
192
  ];
224
193
  return { project: projectName, projectDir, repo: absRepo, actions };
225
194
  }
226
195
 
227
- function runDoctor({ fix = false, force = false, agents = ["cursor"], dryRun = false } = {}) {
196
+ /** Link dist/<id> live harness paths. Only for installed harnesses. */
197
+ function linkHarnesses(cfg, agents, opts = {}) {
198
+ const {
199
+ force = false,
200
+ dryRun = false,
201
+ forceLink = false,
202
+ } = opts;
203
+ const { selected, skipped, warnings } = selectHarnesses(cfg, agents, {
204
+ forceLink,
205
+ });
206
+ const links = [];
207
+ const platformWarn = wslWarnings(cfg, selected);
208
+
209
+ const linkedDist = new Set();
210
+ for (const h of selected) {
211
+ if (linkedDist.has(h.distId)) continue;
212
+ linkedDist.add(h.distId);
213
+ const dist = distRoot(cfg, h.distId);
214
+ if (!fs.existsSync(dist)) {
215
+ links.push({
216
+ harness: h.id,
217
+ action: "skip",
218
+ reason: "dist_missing",
219
+ help: "Run ai-md build first",
220
+ });
221
+ continue;
222
+ }
223
+ if (h.skills) {
224
+ links.push({
225
+ harness: h.id,
226
+ kind: "skills",
227
+ ...linkPath(path.join(dist, "skills"), h.skills, {
228
+ force,
229
+ dryRun,
230
+ linkMode: cfg.linkMode,
231
+ }),
232
+ });
233
+ }
234
+ if (h.rules && h.format !== "skills-only") {
235
+ links.push({
236
+ harness: h.id,
237
+ kind: "rules",
238
+ ...linkPath(path.join(dist, "rules"), h.rules, {
239
+ force,
240
+ dryRun,
241
+ linkMode: cfg.linkMode,
242
+ }),
243
+ });
244
+ }
245
+ }
246
+
247
+ return {
248
+ links,
249
+ skipped,
250
+ warnings: [...warnings, ...platformWarn],
251
+ };
252
+ }
253
+
254
+ function buildAndLink(cfg, opts = {}) {
255
+ const build = runBuild(cfg, opts);
256
+ const link = linkHarnesses(cfg, opts.agents, opts);
257
+ return { build, link };
258
+ }
259
+
260
+ function runInstall(cfg, opts = {}) {
261
+ const dir = cfg.dir;
262
+ const empty = !fs.existsSync(dir) || isEmptyDir(dir);
263
+
264
+ if (empty) {
265
+ if (!cfg.remote) {
266
+ const err = new Error(
267
+ "No remote configured and content dir is empty.\n" +
268
+ " ai-md init # skeleton only (no remote)\n" +
269
+ " ai-md setup --remote <url> # clone existing github.com/<user>/.ai-md"
270
+ );
271
+ err.code = "EINVAL";
272
+ throw err;
273
+ }
274
+ if (fs.existsSync(dir) && isEmptyDir(dir)) {
275
+ fs.rmSync(dir, { recursive: true, force: true });
276
+ }
277
+ gitClone(cfg.remote, dir, { dryRun: opts.dryRun });
278
+ if (!opts.dryRun) {
279
+ seedSkeleton(cfg, { force: false });
280
+ }
281
+ } else if (!fs.existsSync(path.join(dir, ".git"))) {
282
+ const err = new Error(
283
+ `${dir} exists but is not a git repo. Move it aside, then: ai-md setup --remote <url>`
284
+ );
285
+ err.code = "EEXIST";
286
+ throw err;
287
+ } else if (cfg.remote && opts.pull !== false) {
288
+ try {
289
+ gitPull(cfg.dir, { dryRun: opts.dryRun });
290
+ } catch {
291
+ /* offline / no upstream — continue with local tree */
292
+ }
293
+ }
294
+
295
+ return buildAndLink(cfg, {
296
+ agents: opts.agents,
297
+ force: opts.force,
298
+ dryRun: opts.dryRun,
299
+ forceLink: opts.forceLink,
300
+ });
301
+ }
302
+
303
+ /**
304
+ * Bootstrap: remote set → clone/sync first (never skeleton-first).
305
+ * No remote → seed skeleton only.
306
+ */
307
+ function bootstrapContent(cfg, opts = {}) {
308
+ const {
309
+ noGit = false,
310
+ force = false,
311
+ dryRun = false,
312
+ forceLink = false,
313
+ agents = ["cursor"],
314
+ } = opts;
315
+
316
+ if (cfg.remote) {
317
+ const dir = cfg.dir;
318
+ const exists = fs.existsSync(dir);
319
+ const empty = !exists || isEmptyDir(dir);
320
+ const isGit = exists && fs.existsSync(path.join(dir, ".git"));
321
+
322
+ if (!empty && isGit) {
323
+ if (!dryRun) {
324
+ writeMachineConfig({
325
+ dir: cfg.dir,
326
+ remote: cfg.remote,
327
+ agents,
328
+ linkMode: cfg.linkMode,
329
+ });
330
+ }
331
+ return {
332
+ action: "synced",
333
+ remote: cfg.remote,
334
+ dir,
335
+ ...runInstall(cfg, {
336
+ agents,
337
+ force,
338
+ dryRun,
339
+ forceLink,
340
+ pull: true,
341
+ }),
342
+ };
343
+ }
344
+
345
+ if (!empty && !isGit) {
346
+ if (!force) {
347
+ const err = new Error(
348
+ `${dir} is not empty and has no .git.\n` +
349
+ ` mv ${dir} ${dir}.bak && ai-md setup --remote ${cfg.remote}\n` +
350
+ ` (refusing to seed skeleton before sync when a remote is set)`
351
+ );
352
+ err.code = "EEXIST";
353
+ throw err;
354
+ }
355
+ if (dryRun) {
356
+ return { action: "would_replace_and_clone", remote: cfg.remote, dir };
357
+ }
358
+ fs.rmSync(dir, { recursive: true, force: true });
359
+ }
360
+
361
+ if (dryRun) {
362
+ return { action: "would_clone", remote: cfg.remote, dir };
363
+ }
364
+ if (fs.existsSync(dir) && isEmptyDir(dir)) {
365
+ fs.rmSync(dir, { recursive: true, force: true });
366
+ }
367
+ gitClone(cfg.remote, dir, { dryRun: false });
368
+ // After clone only — fill gaps; never invent a tree instead of syncing
369
+ seedSkeleton(cfg, { force: false });
370
+ writeMachineConfig({
371
+ dir: cfg.dir,
372
+ remote: cfg.remote,
373
+ agents,
374
+ linkMode: cfg.linkMode,
375
+ });
376
+ const fresh = resolveConfig(process.env, {
377
+ dir: cfg.dir,
378
+ remote: cfg.remote,
379
+ skipRemoteDetect: true,
380
+ });
381
+ return {
382
+ action: "cloned",
383
+ remote: cfg.remote,
384
+ dir,
385
+ ...buildAndLink(fresh, { agents, force: true, forceLink }),
386
+ };
387
+ }
388
+
389
+ const data = initRepo(cfg, { noGit, force, dryRun });
390
+ if (!dryRun) {
391
+ writeMachineConfig({
392
+ dir: cfg.dir,
393
+ agents,
394
+ linkMode: cfg.linkMode,
395
+ });
396
+ }
397
+ const fresh = resolveConfig(process.env, {
398
+ dir: cfg.dir,
399
+ skipRemoteDetect: true,
400
+ });
401
+ let bl = null;
402
+ if (!dryRun) {
403
+ bl = buildAndLink(fresh, { agents, force: true, forceLink });
404
+ }
405
+ return { action: "initialized", init: data, buildLink: bl };
406
+ }
407
+
408
+ function runPull(cfg, opts = {}) {
409
+ if (!fs.existsSync(path.join(cfg.dir, ".git"))) {
410
+ const err = new Error(`${cfg.dir} is not a git repo; run ai-md install`);
411
+ err.code = "ENOENT";
412
+ throw err;
413
+ }
414
+ gitPull(cfg.dir, { dryRun: opts.dryRun });
415
+ return buildAndLink(cfg, {
416
+ agents: opts.agents,
417
+ force: opts.force,
418
+ dryRun: opts.dryRun,
419
+ forceLink: opts.forceLink,
420
+ });
421
+ }
422
+
423
+ function runPush(cfg, opts = {}) {
424
+ return gitPush(cfg.dir, {
425
+ message: opts.message,
426
+ dryRun: opts.dryRun,
427
+ });
428
+ }
429
+
430
+ function runDoctor({
431
+ fix = false,
432
+ force = false,
433
+ agents = ["cursor"],
434
+ dryRun = false,
435
+ forceLink = false,
436
+ } = {}) {
228
437
  const cfg = resolveConfig();
229
438
  const status = collectStatus({ full: true, agents });
230
439
  const repairs = [];
231
440
  if (fix || force) {
232
- repairs.push(...ensureCursorLinks(cfg, { force: true, dryRun }));
233
- repairs.push(...ensureAgentSkillLinks(cfg, agents, { force: true, dryRun }));
441
+ try {
442
+ const bl = buildAndLink(cfg, {
443
+ agents,
444
+ force: true,
445
+ dryRun,
446
+ forceLink,
447
+ });
448
+ repairs.push(bl);
449
+ } catch (e) {
450
+ repairs.push({ error: e.message, code: e.code, dirty: e.dirty });
451
+ }
234
452
  }
235
453
  const after = fix || force ? collectStatus({ full: true, agents }) : status;
236
454
  return {
237
- before: {
238
- problems: status.problems,
239
- counts: status.counts,
240
- },
455
+ before: { problems: status.problems, counts: status.counts },
241
456
  repairs,
242
457
  after: {
243
458
  problems: after.problems,
244
459
  counts: after.counts,
245
460
  links: after.links,
461
+ harnesses: after.harnesses,
246
462
  projects: after.projects,
247
463
  },
248
464
  help: statusHelp(after),
465
+ wsl: wslWarnings(cfg, []),
249
466
  };
250
467
  }
251
468
 
469
+ function harnessList(cfg) {
470
+ const ids = new Set([
471
+ ...listBuiltinIds(),
472
+ ...Object.keys((cfg.machineConfig && cfg.machineConfig.harnesses) || {}),
473
+ ]);
474
+ return [...ids].sort().map((id) => {
475
+ const def = resolveHarness(id, cfg);
476
+ return {
477
+ id: def.id,
478
+ format: def.format,
479
+ emitter: def.emitter,
480
+ aliasOf: def.aliasOf,
481
+ skills: def.skills,
482
+ rules: def.rules,
483
+ enabled: def.enabled,
484
+ installed: isHarnessInstalled(def),
485
+ distId: def.distId,
486
+ tier: def.tier,
487
+ };
488
+ });
489
+ }
490
+
491
+ function harnessShow(cfg, id) {
492
+ const def = resolveHarness(id, cfg);
493
+ return {
494
+ ...def,
495
+ installed: isHarnessInstalled(def),
496
+ builtin: Boolean(getBuiltin(id)),
497
+ };
498
+ }
499
+
500
+ function harnessSet(cfg, id, { skills, rules, format } = {}) {
501
+ const key = String(id).toLowerCase();
502
+ const existing = { ...(cfg.machineConfig || {}) };
503
+ const harnesses = { ...(existing.harnesses || {}) };
504
+ const cur = { ...(harnesses[key] || {}) };
505
+ if (skills != null) cur.skills = skills;
506
+ if (rules != null) cur.rules = rules;
507
+ if (format != null) cur.format = format;
508
+ cur.enabled = cur.enabled !== false;
509
+ harnesses[key] = cur;
510
+ const agents = existing.agents || ["cursor"];
511
+ if (!agents.includes(key) && key !== "codex") {
512
+ agents.push(key);
513
+ }
514
+ const saved = writeMachineConfig({ ...existing, harnesses, agents });
515
+ ensureAgentSourceDirs(
516
+ { ...cfg, dir: saved.config.dir || cfg.dir },
517
+ key === "codex" ? "agents" : key
518
+ );
519
+ return { id: key, harness: cur, config: saved };
520
+ }
521
+
522
+ function harnessUnset(cfg, id, { pathsOnly = false } = {}) {
523
+ const key = String(id).toLowerCase();
524
+ const existing = { ...(cfg.machineConfig || {}) };
525
+ const harnesses = { ...(existing.harnesses || {}) };
526
+ if (pathsOnly && harnesses[key]) {
527
+ delete harnesses[key].skills;
528
+ delete harnesses[key].rules;
529
+ delete harnesses[key].format;
530
+ } else {
531
+ delete harnesses[key];
532
+ }
533
+ return writeMachineConfig({ ...existing, harnesses });
534
+ }
535
+
536
+ function harnessEnable(cfg, id, enabled) {
537
+ const key = String(id).toLowerCase();
538
+ const existing = { ...(cfg.machineConfig || {}) };
539
+ const harnesses = { ...(existing.harnesses || {}) };
540
+ harnesses[key] = { ...(harnesses[key] || {}), enabled };
541
+ let agents = [...(existing.agents || ["cursor"])];
542
+ if (enabled && !agents.includes(key) && key !== "codex") agents.push(key);
543
+ if (!enabled) agents = agents.filter((a) => a !== key);
544
+ return writeMachineConfig({ ...existing, harnesses, agents });
545
+ }
546
+
547
+ // legacy exports expected by older callers
548
+ function ensureCursorLinks(cfg, opts = {}) {
549
+ return linkHarnesses(cfg, ["cursor"], opts).links;
550
+ }
551
+
552
+ function ensureAgentSkillLinks(cfg, agents, opts = {}) {
553
+ return linkHarnesses(cfg, agents, opts).links;
554
+ }
555
+
252
556
  module.exports = {
253
557
  ensureCursorLinks,
254
558
  ensureAgentSkillLinks,
@@ -258,5 +562,23 @@ module.exports = {
258
562
  linkProject,
259
563
  runDoctor,
260
564
  linkPath,
565
+ linkHarnesses,
566
+ buildAndLink,
567
+ runInstall,
568
+ runPull,
569
+ runPush,
570
+ runBuild,
571
+ runRescue,
572
+ seedSkeleton,
573
+ initRepo,
574
+ bootstrapContent,
575
+ harnessList,
576
+ harnessShow,
577
+ harnessSet,
578
+ harnessUnset,
579
+ harnessEnable,
261
580
  collectProjects,
581
+ SKELETON_VERSION,
582
+ distDirty,
583
+ defaultLinkMode,
262
584
  };
@@ -0,0 +1,73 @@
1
+ "use strict";
2
+
3
+ const fs = require("fs");
4
+ const os = require("os");
5
+ const path = require("path");
6
+
7
+ function expandHome(p) {
8
+ if (!p) return p;
9
+ let s = String(p);
10
+ if (process.platform === "win32") {
11
+ s = s.replace(/%([^%]+)%/g, (_, name) => process.env[name] || `%${name}%`);
12
+ }
13
+ if (s === "~") return os.homedir();
14
+ if (s.startsWith("~/") || s.startsWith("~\\")) {
15
+ return path.join(os.homedir(), s.slice(2));
16
+ }
17
+ return s;
18
+ }
19
+
20
+ function isWsl() {
21
+ if (process.platform !== "linux") return false;
22
+ try {
23
+ const rel = os.release().toLowerCase();
24
+ if (rel.includes("microsoft") || rel.includes("wsl")) return true;
25
+ } catch {
26
+ /* ignore */
27
+ }
28
+ try {
29
+ const v = fs.readFileSync("/proc/version", "utf8").toLowerCase();
30
+ return v.includes("microsoft") || v.includes("wsl");
31
+ } catch {
32
+ return false;
33
+ }
34
+ }
35
+
36
+ function defaultLinkMode() {
37
+ if (process.platform === "win32") return "junction";
38
+ return "symlink";
39
+ }
40
+
41
+ function looksLikeWindowsPath(p) {
42
+ return /^[a-zA-Z]:[\\/]/.test(p) || p.startsWith("\\\\");
43
+ }
44
+
45
+ function normalizePath(p) {
46
+ if (!p) return p;
47
+ try {
48
+ return fs.realpathSync.native
49
+ ? fs.realpathSync.native(p)
50
+ : fs.realpathSync(p);
51
+ } catch {
52
+ return path.resolve(p);
53
+ }
54
+ }
55
+
56
+ function pathsEqual(a, b) {
57
+ if (!a || !b) return false;
58
+ const na = normalizePath(a);
59
+ const nb = normalizePath(b);
60
+ if (process.platform === "win32") {
61
+ return na.toLowerCase() === nb.toLowerCase();
62
+ }
63
+ return na === nb;
64
+ }
65
+
66
+ module.exports = {
67
+ expandHome,
68
+ isWsl,
69
+ defaultLinkMode,
70
+ looksLikeWindowsPath,
71
+ normalizePath,
72
+ pathsEqual,
73
+ };