@hacksmith/doraval 0.2.26 → 0.2.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/bin/doraval.js +277 -174
  2. package/package.json +1 -1
package/bin/doraval.js CHANGED
@@ -599,7 +599,7 @@ var init_dist = __esm(() => {
599
599
  var require_package = __commonJS((exports, module) => {
600
600
  module.exports = {
601
601
  name: "@hacksmith/doraval",
602
- version: "0.2.26",
602
+ version: "0.2.27",
603
603
  author: "Saif",
604
604
  repository: {
605
605
  type: "git",
@@ -772,6 +772,23 @@ var init_frontmatter = __esm(() => {
772
772
  });
773
773
 
774
774
  // src/core/skill-validate.ts
775
+ import { existsSync } from "fs";
776
+ import { resolve } from "path";
777
+ async function loadSkill(dir) {
778
+ const skillMd = resolve(dir, "SKILL.md");
779
+ if (!existsSync(skillMd)) {
780
+ return { ok: false, error: "No SKILL.md found" };
781
+ }
782
+ const raw = await Bun.file(skillMd).text();
783
+ let parsed;
784
+ try {
785
+ parsed = parseFrontmatter(raw);
786
+ } catch {
787
+ return { ok: false, error: "Failed to parse YAML frontmatter in SKILL.md" };
788
+ }
789
+ const existingDirs = OPTIONAL_DIRS.filter((d) => existsSync(resolve(dir, d)));
790
+ return { ok: true, model: parsed, existingDirs };
791
+ }
775
792
  function checkFrontmatterPresence(model, _ctx) {
776
793
  const keys = Object.keys(model.data);
777
794
  if (keys.length === 0) {
@@ -836,8 +853,9 @@ var merge = (a, b) => ({
836
853
  errors: [...a.errors, ...b.errors ?? []],
837
854
  warnings: [...a.warnings, ...b.warnings ?? []],
838
855
  passes: [...a.passes, ...b.passes ?? []]
839
- }), NAME_REGEX, KNOWN_FIELDS, SUPPORTING_DIRS, EMPTY, checks;
856
+ }), NAME_REGEX, KNOWN_FIELDS, SUPPORTING_DIRS, OPTIONAL_DIRS, EMPTY, checks;
840
857
  var init_skill_validate = __esm(() => {
858
+ init_frontmatter();
841
859
  NAME_REGEX = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
842
860
  KNOWN_FIELDS = new Set([
843
861
  "name",
@@ -858,6 +876,7 @@ var init_skill_validate = __esm(() => {
858
876
  "shell"
859
877
  ]);
860
878
  SUPPORTING_DIRS = ["references", "scripts", "assets", "examples"];
879
+ OPTIONAL_DIRS = ["references", "scripts", "assets"];
861
880
  EMPTY = { errors: [], warnings: [], passes: [] };
862
881
  checks = [
863
882
  checkFrontmatterPresence,
@@ -876,16 +895,14 @@ var exports_validate = {};
876
895
  __export(exports_validate, {
877
896
  default: () => validate_default
878
897
  });
879
- import { existsSync } from "fs";
880
- import { resolve } from "path";
881
- var import_picocolors2, OPTIONAL_DIRS, validate_default;
898
+ import { existsSync as existsSync2 } from "fs";
899
+ import { resolve as resolve2 } from "path";
900
+ var import_picocolors2, validate_default;
882
901
  var init_validate = __esm(() => {
883
902
  init_dist();
884
903
  init_out();
885
- init_frontmatter();
886
904
  init_skill_validate();
887
905
  import_picocolors2 = __toESM(require_picocolors(), 1);
888
- OPTIONAL_DIRS = ["references", "scripts", "assets"];
889
906
  validate_default = defineCommand({
890
907
  meta: {
891
908
  name: "validate",
@@ -921,16 +938,17 @@ var init_validate = __esm(() => {
921
938
  },
922
939
  async run({ args }) {
923
940
  const targetPath = args.path;
924
- const fullPath = resolve(targetPath);
925
- if (!existsSync(fullPath)) {
941
+ const fullPath = resolve2(targetPath);
942
+ if (!existsSync2(fullPath)) {
926
943
  ui.fail(`Path not found: ${targetPath}
927
944
 
928
945
  Check that the path is correct and the directory exists.`);
929
946
  process.exit(1);
930
947
  }
931
- const skillMd = resolve(fullPath, "SKILL.md");
932
- if (!existsSync(skillMd)) {
933
- ui.fail(`No skill or plugin found at ${targetPath}
948
+ const loaded = await loadSkill(fullPath);
949
+ if (!loaded.ok) {
950
+ if (loaded.error === "No SKILL.md found") {
951
+ ui.fail(`No skill or plugin found at ${targetPath}
934
952
 
935
953
  Searched for:
936
954
  \u2022 SKILL.md (Agent Skills spec)
@@ -939,19 +957,14 @@ Searched for:
939
957
  Try:
940
958
  \u2022 Check the path points to a skill or plugin directory
941
959
  \u2022 Use --for to target a specific validator`);
942
- process.exit(1);
943
- }
944
- const raw = await Bun.file(skillMd).text();
945
- let parsed;
946
- try {
947
- parsed = parseFrontmatter(raw);
948
- } catch {
949
- ui.fail(`Failed to parse YAML frontmatter in SKILL.md
960
+ } else {
961
+ ui.fail(`${loaded.error}
950
962
 
951
963
  Fix the YAML syntax and retry.`);
964
+ }
952
965
  process.exit(1);
953
966
  }
954
- const existingDirs = OPTIONAL_DIRS.filter((dir) => existsSync(resolve(fullPath, dir)));
967
+ const { model: parsed, existingDirs } = loaded;
955
968
  const { errors, warnings, passes } = validateSkillModel(parsed, {
956
969
  existingDirs: [...existingDirs]
957
970
  });
@@ -1060,13 +1073,12 @@ var exports_drift = {};
1060
1073
  __export(exports_drift, {
1061
1074
  default: () => drift_default
1062
1075
  });
1063
- import { existsSync as existsSync2 } from "fs";
1064
- import { resolve as resolve2 } from "path";
1076
+ import { resolve as resolve3 } from "path";
1065
1077
  var import_picocolors3, drift_default;
1066
1078
  var init_drift = __esm(() => {
1067
1079
  init_dist();
1068
1080
  init_out();
1069
- init_frontmatter();
1081
+ init_skill_validate();
1070
1082
  init_skill_drift();
1071
1083
  import_picocolors3 = __toESM(require_picocolors(), 1);
1072
1084
  drift_default = defineCommand({
@@ -1104,22 +1116,19 @@ var init_drift = __esm(() => {
1104
1116
  },
1105
1117
  async run({ args }) {
1106
1118
  const targetPath = args.path;
1107
- const fullPath = resolve2(targetPath);
1108
- const skillMd = resolve2(fullPath, "SKILL.md");
1109
- if (!existsSync2(skillMd)) {
1110
- ui.fail(`No SKILL.md found at ${targetPath}
1119
+ const fullPath = resolve3(targetPath);
1120
+ const loaded = await loadSkill(fullPath);
1121
+ if (!loaded.ok) {
1122
+ if (loaded.error === "No SKILL.md found") {
1123
+ ui.fail(`No SKILL.md found at ${targetPath}
1111
1124
 
1112
1125
  Check that the path points to a skill directory containing SKILL.md.`);
1126
+ } else {
1127
+ ui.fail(loaded.error);
1128
+ }
1113
1129
  process.exit(1);
1114
1130
  }
1115
- const raw = await Bun.file(skillMd).text();
1116
- let parsed;
1117
- try {
1118
- parsed = parseFrontmatter(raw);
1119
- } catch {
1120
- ui.fail("Failed to parse YAML frontmatter in SKILL.md");
1121
- process.exit(1);
1122
- }
1131
+ const { model: parsed } = loaded;
1123
1132
  const desc = String(parsed.data.description || "");
1124
1133
  const when = String(parsed.data.when_to_use || "");
1125
1134
  const { drifts, driftCount, total } = analyzeDrift({
@@ -2869,7 +2878,7 @@ var exports_bump = {};
2869
2878
  __export(exports_bump, {
2870
2879
  default: () => bump_default
2871
2880
  });
2872
- import { resolve as resolve3, join as join9, dirname, relative } from "path";
2881
+ import { resolve as resolve4, join as join9, dirname, relative } from "path";
2873
2882
  import { existsSync as existsSync10, readFileSync, writeFileSync as writeFileSync2, readdirSync as readdirSync4, statSync } from "fs";
2874
2883
  function bumpVersion(current, type) {
2875
2884
  if (/^\d+\.\d+\.\d+$/.test(type))
@@ -3005,7 +3014,7 @@ var init_bump = __esm(() => {
3005
3014
  process.exit(1);
3006
3015
  }
3007
3016
  const isKnownType = ["patch", "minor", "major"].includes(rawType) || /^\d+\.\d+\.\d+$/.test(rawType);
3008
- const maybePath = resolve3(rawType);
3017
+ const maybePath = resolve4(rawType);
3009
3018
  const looksLikeDir = existsSync10(maybePath) || rawType === "." || rawType.startsWith("./") || rawType.startsWith("../");
3010
3019
  if (!isKnownType && looksLikeDir) {
3011
3020
  targetPath = rawType;
@@ -3014,7 +3023,7 @@ var init_bump = __esm(() => {
3014
3023
  ui.fail(`Unknown bump type "${rawType}". Use patch | minor | major | 1.2.3`);
3015
3024
  process.exit(1);
3016
3025
  }
3017
- const root = resolve3(targetPath);
3026
+ const root = resolve4(targetPath);
3018
3027
  if (!existsSync10(root)) {
3019
3028
  ui.fail(`Path does not exist: ${root}`);
3020
3029
  process.exit(1);
@@ -3329,42 +3338,36 @@ var init_new2 = __esm(() => {
3329
3338
 
3330
3339
  // src/validators/claude/skill.ts
3331
3340
  import { existsSync as existsSync13 } from "fs";
3332
- import { resolve as resolve4 } from "path";
3333
- var OPTIONAL_DIRS2, claudeSkillValidator;
3341
+ import { resolve as resolve5 } from "path";
3342
+ var claudeSkillValidator;
3334
3343
  var init_skill = __esm(() => {
3335
- init_frontmatter();
3336
3344
  init_skill_validate();
3337
- OPTIONAL_DIRS2 = ["references", "scripts", "assets"];
3338
3345
  claudeSkillValidator = {
3339
3346
  id: "claude:skill",
3340
3347
  provider: "claude",
3341
3348
  name: "Claude Skill",
3342
3349
  description: "Validates SKILL.md per current Claude Code spec: frontmatter (name/description relaxed to recommended; directory name usually provides the /command), body, supporting files, dynamic injection (!`cmd`), substitutions ($ARGUMENTS, ${CLAUDE_*}), and advanced fields (allowed-tools, context, disable-model-invocation, when_to_use, etc.)",
3343
3350
  detect(dir) {
3344
- return existsSync13(resolve4(dir, "SKILL.md"));
3351
+ return existsSync13(resolve5(dir, "SKILL.md"));
3345
3352
  },
3346
3353
  async validate(dir, _opts) {
3347
- const skillMd = resolve4(dir, "SKILL.md");
3348
- const raw = await Bun.file(skillMd).text();
3349
- let parsed;
3350
- try {
3351
- parsed = parseFrontmatter(raw);
3352
- } catch {
3354
+ const loaded = await loadSkill(dir);
3355
+ if (!loaded.ok) {
3353
3356
  return {
3354
- errors: ["Failed to parse YAML frontmatter in SKILL.md"],
3357
+ errors: [loaded.error],
3355
3358
  warnings: [],
3356
3359
  passes: []
3357
3360
  };
3358
3361
  }
3359
- const existingDirs = OPTIONAL_DIRS2.filter((d) => existsSync13(resolve4(dir, d)));
3360
- return validateSkillModel(parsed, { existingDirs: [...existingDirs] });
3362
+ const { model, existingDirs } = loaded;
3363
+ return validateSkillModel(model, { existingDirs: [...existingDirs] });
3361
3364
  }
3362
3365
  };
3363
3366
  });
3364
3367
 
3365
3368
  // src/validators/claude/plugin.ts
3366
3369
  import { existsSync as existsSync14, readdirSync as readdirSync6 } from "fs";
3367
- import { resolve as resolve5, join as join12 } from "path";
3370
+ import { resolve as resolve6, join as join12 } from "path";
3368
3371
  function levenshtein(a, b) {
3369
3372
  if (a === b)
3370
3373
  return 0;
@@ -3373,15 +3376,21 @@ function levenshtein(a, b) {
3373
3376
  return n;
3374
3377
  if (n === 0)
3375
3378
  return m;
3376
- const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
3377
- for (let i = 0;i <= m; i++)
3378
- dp[i][0] = i;
3379
- for (let j = 0;j <= n; j++)
3380
- dp[0][j] = j;
3379
+ const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
3380
+ for (let i = 0;i <= m; i++) {
3381
+ const row = dp[i];
3382
+ row[0] = i;
3383
+ }
3384
+ for (let j = 0;j <= n; j++) {
3385
+ const row = dp[0];
3386
+ row[j] = j;
3387
+ }
3381
3388
  for (let i = 1;i <= m; i++) {
3389
+ const row = dp[i];
3390
+ const prev = dp[i - 1];
3382
3391
  for (let j = 1;j <= n; j++) {
3383
3392
  const cost = a[i - 1] === b[j - 1] ? 0 : 1;
3384
- dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost);
3393
+ row[j] = Math.min((prev[j] ?? 0) + 1, (row[j - 1] ?? 0) + 1, (prev[j - 1] ?? 0) + cost);
3385
3394
  }
3386
3395
  }
3387
3396
  return dp[m][n];
@@ -3450,14 +3459,14 @@ var init_plugin = __esm(() => {
3450
3459
  name: "Claude Plugin",
3451
3460
  description: "Validates .claude-plugin/plugin.json manifest (complete schema per Plugins reference), component path rules (replace vs augment), .claude-plugin/ purity, default dirs, single-root-skill layout, unrecognized fields + suggestions, and structure",
3452
3461
  detect(dir) {
3453
- return existsSync14(resolve5(dir, ".claude-plugin", "plugin.json"));
3462
+ return existsSync14(resolve6(dir, ".claude-plugin", "plugin.json"));
3454
3463
  },
3455
3464
  async validate(dir, _opts) {
3456
3465
  const errors = [];
3457
3466
  const warnings = [];
3458
3467
  const passes = [];
3459
- const manifestPath = resolve5(dir, ".claude-plugin", "plugin.json");
3460
- const dotClaudePluginDir = resolve5(dir, ".claude-plugin");
3468
+ const manifestPath = resolve6(dir, ".claude-plugin", "plugin.json");
3469
+ const dotClaudePluginDir = resolve6(dir, ".claude-plugin");
3461
3470
  let manifest;
3462
3471
  try {
3463
3472
  const raw = await Bun.file(manifestPath).text();
@@ -3553,7 +3562,7 @@ var init_plugin = __esm(() => {
3553
3562
  errors.push(`${field}: path "${s}" must start with "./"`);
3554
3563
  } else if (s.includes("..")) {
3555
3564
  errors.push(`${field}: path "${s}" must not use ".." (paths are confined to the plugin tree after cache copy)`);
3556
- } else if (existsSync14(resolve5(dir, s))) {
3565
+ } else if (existsSync14(resolve6(dir, s))) {
3557
3566
  passes.push(`${field}: path "${s}" exists`);
3558
3567
  } else {
3559
3568
  warnings.push(`${field}: path "${s}" does not exist on disk`);
@@ -3602,7 +3611,7 @@ var init_plugin = __esm(() => {
3602
3611
  if (Array.isArray(manifest.dependencies)) {
3603
3612
  passes.push(`dependencies: declares ${manifest.dependencies.length} plugin dependency/ies`);
3604
3613
  }
3605
- const skillsDir = resolve5(dir, "skills");
3614
+ const skillsDir = resolve6(dir, "skills");
3606
3615
  if (existsSync14(skillsDir)) {
3607
3616
  const entries = readdirSync6(skillsDir, { withFileTypes: true }).filter((e) => e.isDirectory());
3608
3617
  for (const e of entries) {
@@ -3617,7 +3626,7 @@ var init_plugin = __esm(() => {
3617
3626
  warnings.push('Default skills/ dir co-exists with manifest "skills" \u2014 manifest path is authoritative; default folder ignored for loading');
3618
3627
  }
3619
3628
  }
3620
- const commandsDir = resolve5(dir, "commands");
3629
+ const commandsDir = resolve6(dir, "commands");
3621
3630
  if (existsSync14(commandsDir)) {
3622
3631
  const mds = readdirSync6(commandsDir).filter((f) => f.endsWith(".md"));
3623
3632
  if (mds.length) {
@@ -3627,7 +3636,7 @@ var init_plugin = __esm(() => {
3627
3636
  warnings.push('commands/ co-exists with manifest "commands" \u2014 manifest replaces default (dir ignored)');
3628
3637
  }
3629
3638
  }
3630
- const agentsDir = resolve5(dir, "agents");
3639
+ const agentsDir = resolve6(dir, "agents");
3631
3640
  if (existsSync14(agentsDir)) {
3632
3641
  const mds = readdirSync6(agentsDir).filter((f) => f.endsWith(".md"));
3633
3642
  if (mds.length) {
@@ -3637,30 +3646,30 @@ var init_plugin = __esm(() => {
3637
3646
  warnings.push('agents/ co-exists with manifest "agents" \u2014 manifest replaces default (dir ignored)');
3638
3647
  }
3639
3648
  }
3640
- if (existsSync14(resolve5(dir, "output-styles"))) {
3649
+ if (existsSync14(resolve6(dir, "output-styles"))) {
3641
3650
  passes.push("output-styles/ directory present");
3642
3651
  if (manifest.outputStyles)
3643
3652
  warnings.push("output-styles/ co-exists with manifest outputStyles \u2014 manifest wins");
3644
3653
  }
3645
- if (existsSync14(resolve5(dir, "themes")))
3654
+ if (existsSync14(resolve6(dir, "themes")))
3646
3655
  passes.push("themes/ present (experimental)");
3647
- if (existsSync14(resolve5(dir, "monitors")) || manifest.experimental?.monitors) {
3656
+ if (existsSync14(resolve6(dir, "monitors")) || manifest.experimental?.monitors) {
3648
3657
  passes.push("monitors config present (experimental)");
3649
3658
  }
3650
- if (existsSync14(resolve5(dir, "bin")))
3659
+ if (existsSync14(resolve6(dir, "bin")))
3651
3660
  passes.push("bin/ present (adds executables to Bash tool $PATH)");
3652
- if (existsSync14(resolve5(dir, "settings.json")))
3661
+ if (existsSync14(resolve6(dir, "settings.json")))
3653
3662
  passes.push("settings.json present (plugin defaults for agent/statusline)");
3654
- if (existsSync14(resolve5(dir, "README.md")))
3663
+ if (existsSync14(resolve6(dir, "README.md")))
3655
3664
  passes.push("README.md present");
3656
- if (existsSync14(resolve5(dir, ".mcp.json")))
3665
+ if (existsSync14(resolve6(dir, ".mcp.json")))
3657
3666
  passes.push(".mcp.json present (validated by claude:mcp)");
3658
- if (existsSync14(resolve5(dir, ".lsp.json")))
3667
+ if (existsSync14(resolve6(dir, ".lsp.json")))
3659
3668
  passes.push(".lsp.json present (validated by claude:lsp when registered)");
3660
- if (existsSync14(resolve5(dir, "hooks/hooks.json")) || existsSync14(resolve5(dir, "hooks.json"))) {
3669
+ if (existsSync14(resolve6(dir, "hooks/hooks.json")) || existsSync14(resolve6(dir, "hooks.json"))) {
3661
3670
  passes.push("hooks config present (validated by claude:hooks)");
3662
3671
  }
3663
- if (existsSync14(resolve5(dir, "SKILL.md")) && !existsSync14(skillsDir) && manifest.skills === undefined) {
3672
+ if (existsSync14(resolve6(dir, "SKILL.md")) && !existsSync14(skillsDir) && manifest.skills === undefined) {
3664
3673
  passes.push('Root SKILL.md detected \u2014 plugin will be treated as a single-skill plugin (prefer frontmatter "name" for stable /command)');
3665
3674
  }
3666
3675
  return { errors, warnings, passes };
@@ -3670,7 +3679,7 @@ var init_plugin = __esm(() => {
3670
3679
 
3671
3680
  // src/validators/claude/marketplace.ts
3672
3681
  import { existsSync as existsSync15, readdirSync as readdirSync7 } from "fs";
3673
- import { resolve as resolve6, join as join13 } from "path";
3682
+ import { resolve as resolve7, join as join13 } from "path";
3674
3683
  var claudeMarketplaceValidator;
3675
3684
  var init_marketplace = __esm(() => {
3676
3685
  claudeMarketplaceValidator = {
@@ -3679,7 +3688,7 @@ var init_marketplace = __esm(() => {
3679
3688
  name: "Claude Plugin Marketplace",
3680
3689
  description: "Validates marketplace structure: plugins/ directory with valid plugin subdirectories",
3681
3690
  detect(dir) {
3682
- const pluginsDir = resolve6(dir, "plugins");
3691
+ const pluginsDir = resolve7(dir, "plugins");
3683
3692
  if (!existsSync15(pluginsDir))
3684
3693
  return false;
3685
3694
  try {
@@ -3699,7 +3708,7 @@ var init_marketplace = __esm(() => {
3699
3708
  const errors = [];
3700
3709
  const warnings = [];
3701
3710
  const passes = [];
3702
- const pluginsDir = resolve6(dir, "plugins");
3711
+ const pluginsDir = resolve7(dir, "plugins");
3703
3712
  if (!existsSync15(pluginsDir)) {
3704
3713
  errors.push("Missing plugins/ directory");
3705
3714
  return { errors, warnings, passes };
@@ -3711,12 +3720,12 @@ var init_marketplace = __esm(() => {
3711
3720
  return { errors, warnings, passes };
3712
3721
  }
3713
3722
  passes.push(`${pluginEntries.length} plugin(s) found`);
3714
- if (existsSync15(resolve6(dir, "README.md"))) {
3723
+ if (existsSync15(resolve7(dir, "README.md"))) {
3715
3724
  passes.push("README.md exists at marketplace root");
3716
3725
  } else {
3717
3726
  warnings.push("No README.md at marketplace root \u2014 recommended for discoverability");
3718
3727
  }
3719
- if (existsSync15(resolve6(dir, "LICENSE"))) {
3728
+ if (existsSync15(resolve7(dir, "LICENSE"))) {
3720
3729
  passes.push("LICENSE exists at marketplace root");
3721
3730
  } else {
3722
3731
  warnings.push("No LICENSE at marketplace root \u2014 recommended");
@@ -3742,7 +3751,7 @@ var init_marketplace = __esm(() => {
3742
3751
 
3743
3752
  // src/validators/claude/hooks.ts
3744
3753
  import { existsSync as existsSync16 } from "fs";
3745
- import { resolve as resolve7 } from "path";
3754
+ import { resolve as resolve8 } from "path";
3746
3755
  var KNOWN_EVENTS, claudeHooksValidator;
3747
3756
  var init_hooks = __esm(() => {
3748
3757
  KNOWN_EVENTS = [
@@ -3783,13 +3792,13 @@ var init_hooks = __esm(() => {
3783
3792
  name: "Claude Hooks",
3784
3793
  description: "Validates hooks/hooks.json (or root hooks.json): all lifecycle events per Plugins reference, hook group structure (matcher + hooks[]), supported hook types (command, http, mcp_tool, prompt, agent)",
3785
3794
  detect(dir) {
3786
- return existsSync16(resolve7(dir, "hooks", "hooks.json")) || existsSync16(resolve7(dir, "hooks.json"));
3795
+ return existsSync16(resolve8(dir, "hooks", "hooks.json")) || existsSync16(resolve8(dir, "hooks.json"));
3787
3796
  },
3788
3797
  async validate(dir, _opts) {
3789
3798
  const errors = [];
3790
3799
  const warnings = [];
3791
3800
  const passes = [];
3792
- const hooksPath = existsSync16(resolve7(dir, "hooks", "hooks.json")) ? resolve7(dir, "hooks", "hooks.json") : resolve7(dir, "hooks.json");
3801
+ const hooksPath = existsSync16(resolve8(dir, "hooks", "hooks.json")) ? resolve8(dir, "hooks", "hooks.json") : resolve8(dir, "hooks.json");
3793
3802
  let config;
3794
3803
  try {
3795
3804
  const raw = await Bun.file(hooksPath).text();
@@ -3856,7 +3865,7 @@ var init_hooks = __esm(() => {
3856
3865
 
3857
3866
  // src/validators/claude/mcp.ts
3858
3867
  import { existsSync as existsSync17 } from "fs";
3859
- import { resolve as resolve8 } from "path";
3868
+ import { resolve as resolve9 } from "path";
3860
3869
  var claudeMcpValidator;
3861
3870
  var init_mcp = __esm(() => {
3862
3871
  claudeMcpValidator = {
@@ -3865,13 +3874,13 @@ var init_mcp = __esm(() => {
3865
3874
  name: "Claude MCP Config",
3866
3875
  description: "Validates .mcp.json (or inline via plugin.json mcpServers): server entries (stdio: command+args, or url), env, cwd, ${CLAUDE_PLUGIN_ROOT} etc. substitutions per Plugins reference",
3867
3876
  detect(dir) {
3868
- return existsSync17(resolve8(dir, ".mcp.json"));
3877
+ return existsSync17(resolve9(dir, ".mcp.json"));
3869
3878
  },
3870
3879
  async validate(dir, _opts) {
3871
3880
  const errors = [];
3872
3881
  const warnings = [];
3873
3882
  const passes = [];
3874
- const mcpPath = resolve8(dir, ".mcp.json");
3883
+ const mcpPath = resolve9(dir, ".mcp.json");
3875
3884
  let config;
3876
3885
  try {
3877
3886
  const raw = await Bun.file(mcpPath).text();
@@ -3926,7 +3935,7 @@ var init_mcp = __esm(() => {
3926
3935
 
3927
3936
  // src/validators/claude/subagent.ts
3928
3937
  import { existsSync as existsSync18, readdirSync as readdirSync8 } from "fs";
3929
- import { resolve as resolve9, join as join14 } from "path";
3938
+ import { resolve as resolve10, join as join14 } from "path";
3930
3939
  var claudeSubagentValidator;
3931
3940
  var init_subagent = __esm(() => {
3932
3941
  init_frontmatter();
@@ -3936,7 +3945,7 @@ var init_subagent = __esm(() => {
3936
3945
  name: "Claude Subagents",
3937
3946
  description: "Validates agents/*.md (plugin subagents): frontmatter per spec (name, description, model, effort, maxTurns, tools, disallowedTools, skills, memory, background, isolation=worktree), body; warns on disallowed fields (hooks, mcpServers, permissionMode) for security",
3938
3947
  detect(dir) {
3939
- const agentsDir = resolve9(dir, "agents");
3948
+ const agentsDir = resolve10(dir, "agents");
3940
3949
  if (!existsSync18(agentsDir))
3941
3950
  return false;
3942
3951
  try {
@@ -3949,7 +3958,7 @@ var init_subagent = __esm(() => {
3949
3958
  const errors = [];
3950
3959
  const warnings = [];
3951
3960
  const passes = [];
3952
- const agentsDir = resolve9(dir, "agents");
3961
+ const agentsDir = resolve10(dir, "agents");
3953
3962
  const mdFiles = readdirSync8(agentsDir).filter((f) => f.endsWith(".md"));
3954
3963
  if (mdFiles.length === 0) {
3955
3964
  errors.push("agents/ directory has no .md files");
@@ -4018,7 +4027,7 @@ var init_subagent = __esm(() => {
4018
4027
 
4019
4028
  // src/validators/claude/command.ts
4020
4029
  import { existsSync as existsSync19, readdirSync as readdirSync9 } from "fs";
4021
- import { resolve as resolve10, join as join15 } from "path";
4030
+ import { resolve as resolve11, join as join15 } from "path";
4022
4031
  var claudeCommandValidator;
4023
4032
  var init_command = __esm(() => {
4024
4033
  init_frontmatter();
@@ -4028,7 +4037,7 @@ var init_command = __esm(() => {
4028
4037
  name: "Claude Commands",
4029
4038
  description: "Validates commands/ (or legacy .claude/commands/) .md files: frontmatter (including rich skill fields), description, body",
4030
4039
  detect(dir) {
4031
- const commandsDir = resolve10(dir, "commands");
4040
+ const commandsDir = resolve11(dir, "commands");
4032
4041
  if (!existsSync19(commandsDir))
4033
4042
  return false;
4034
4043
  try {
@@ -4041,7 +4050,7 @@ var init_command = __esm(() => {
4041
4050
  const errors = [];
4042
4051
  const warnings = [];
4043
4052
  const passes = [];
4044
- const commandsDir = resolve10(dir, "commands");
4053
+ const commandsDir = resolve11(dir, "commands");
4045
4054
  const mdFiles = readdirSync9(commandsDir).filter((f) => f.endsWith(".md"));
4046
4055
  if (mdFiles.length === 0) {
4047
4056
  errors.push("commands/ directory has no .md files");
@@ -4079,7 +4088,7 @@ var init_command = __esm(() => {
4079
4088
 
4080
4089
  // src/validators/claude/memory.ts
4081
4090
  import { existsSync as existsSync20 } from "fs";
4082
- import { resolve as resolve11 } from "path";
4091
+ import { resolve as resolve12 } from "path";
4083
4092
  var claudeMemoryValidator;
4084
4093
  var init_memory = __esm(() => {
4085
4094
  claudeMemoryValidator = {
@@ -4088,13 +4097,13 @@ var init_memory = __esm(() => {
4088
4097
  name: "Claude CLAUDE.md",
4089
4098
  description: "Validates CLAUDE.md: non-empty, length recommendations, @path imports",
4090
4099
  detect(dir) {
4091
- return existsSync20(resolve11(dir, "CLAUDE.md"));
4100
+ return existsSync20(resolve12(dir, "CLAUDE.md"));
4092
4101
  },
4093
4102
  async validate(dir, _opts) {
4094
4103
  const errors = [];
4095
4104
  const warnings = [];
4096
4105
  const passes = [];
4097
- const filePath = resolve11(dir, "CLAUDE.md");
4106
+ const filePath = resolve12(dir, "CLAUDE.md");
4098
4107
  const raw = await Bun.file(filePath).text();
4099
4108
  if (!raw.trim()) {
4100
4109
  errors.push("CLAUDE.md is empty");
@@ -4112,7 +4121,7 @@ var init_memory = __esm(() => {
4112
4121
  let match;
4113
4122
  while ((match = importRegex.exec(raw)) !== null) {
4114
4123
  const importPath = match[1];
4115
- const resolvedImport = resolve11(dir, importPath);
4124
+ const resolvedImport = resolve12(dir, importPath);
4116
4125
  if (existsSync20(resolvedImport)) {
4117
4126
  passes.push(`@import "${importPath}" exists`);
4118
4127
  } else {
@@ -4126,7 +4135,7 @@ var init_memory = __esm(() => {
4126
4135
 
4127
4136
  // src/validators/claude/lsp.ts
4128
4137
  import { existsSync as existsSync21 } from "fs";
4129
- import { resolve as resolve12 } from "path";
4138
+ import { resolve as resolve13 } from "path";
4130
4139
  var claudeLspValidator;
4131
4140
  var init_lsp = __esm(() => {
4132
4141
  claudeLspValidator = {
@@ -4135,14 +4144,14 @@ var init_lsp = __esm(() => {
4135
4144
  name: "Claude LSP Servers",
4136
4145
  description: "Validates .lsp.json (or plugin.json lspServers): language server configs with required command + extensionToLanguage; optional transport, env, settings, diagnostics etc. (binaries installed separately)",
4137
4146
  detect(dir) {
4138
- return existsSync21(resolve12(dir, ".lsp.json")) || existsSync21(resolve12(dir, ".claude-plugin", "plugin.json"));
4147
+ return existsSync21(resolve13(dir, ".lsp.json")) || existsSync21(resolve13(dir, ".claude-plugin", "plugin.json"));
4139
4148
  },
4140
4149
  async validate(dir, _opts) {
4141
4150
  const errors = [];
4142
4151
  const warnings = [];
4143
4152
  const passes = [];
4144
4153
  let cfg = null;
4145
- const lspPath = resolve12(dir, ".lsp.json");
4154
+ const lspPath = resolve13(dir, ".lsp.json");
4146
4155
  if (existsSync21(lspPath)) {
4147
4156
  try {
4148
4157
  cfg = JSON.parse(await Bun.file(lspPath).text());
@@ -4152,7 +4161,7 @@ var init_lsp = __esm(() => {
4152
4161
  return { errors, warnings, passes };
4153
4162
  }
4154
4163
  } else {
4155
- const manifestPath = resolve12(dir, ".claude-plugin", "plugin.json");
4164
+ const manifestPath = resolve13(dir, ".claude-plugin", "plugin.json");
4156
4165
  if (existsSync21(manifestPath)) {
4157
4166
  try {
4158
4167
  const m = JSON.parse(await Bun.file(manifestPath).text());
@@ -4194,7 +4203,7 @@ var init_lsp = __esm(() => {
4194
4203
 
4195
4204
  // src/validators/claude/monitors.ts
4196
4205
  import { existsSync as existsSync22 } from "fs";
4197
- import { resolve as resolve13 } from "path";
4206
+ import { resolve as resolve14 } from "path";
4198
4207
  var claudeMonitorsValidator;
4199
4208
  var init_monitors = __esm(() => {
4200
4209
  claudeMonitorsValidator = {
@@ -4203,7 +4212,7 @@ var init_monitors = __esm(() => {
4203
4212
  name: "Claude Monitors (experimental)",
4204
4213
  description: "Validates monitors/monitors.json (or experimental.monitors): array of {name, command, description, when?}; commands support ${CLAUDE_PLUGIN_*} subs. Monitors run only in interactive CLI sessions.",
4205
4214
  detect(dir) {
4206
- return existsSync22(resolve13(dir, "monitors", "monitors.json")) || existsSync22(resolve13(dir, "monitors.json")) || existsSync22(resolve13(dir, ".claude-plugin", "plugin.json"));
4215
+ return existsSync22(resolve14(dir, "monitors", "monitors.json")) || existsSync22(resolve14(dir, "monitors.json")) || existsSync22(resolve14(dir, ".claude-plugin", "plugin.json"));
4207
4216
  },
4208
4217
  async validate(dir, _opts) {
4209
4218
  const errors = [];
@@ -4211,8 +4220,8 @@ var init_monitors = __esm(() => {
4211
4220
  const passes = [];
4212
4221
  let arr = null;
4213
4222
  const candidates = [
4214
- resolve13(dir, "monitors", "monitors.json"),
4215
- resolve13(dir, "monitors.json")
4223
+ resolve14(dir, "monitors", "monitors.json"),
4224
+ resolve14(dir, "monitors.json")
4216
4225
  ];
4217
4226
  for (const p of candidates) {
4218
4227
  if (existsSync22(p)) {
@@ -4230,7 +4239,7 @@ var init_monitors = __esm(() => {
4230
4239
  }
4231
4240
  }
4232
4241
  if (!arr) {
4233
- const mp = resolve13(dir, ".claude-plugin", "plugin.json");
4242
+ const mp = resolve14(dir, ".claude-plugin", "plugin.json");
4234
4243
  if (existsSync22(mp)) {
4235
4244
  try {
4236
4245
  const m = JSON.parse(await Bun.file(mp).text());
@@ -4422,7 +4431,7 @@ __export(exports_validate_top, {
4422
4431
  default: () => validate_top_default
4423
4432
  });
4424
4433
  import { existsSync as existsSync24 } from "fs";
4425
- import { resolve as resolve14 } from "path";
4434
+ import { resolve as resolve15 } from "path";
4426
4435
  var import_picocolors13, validate_top_default;
4427
4436
  var init_validate_top = __esm(() => {
4428
4437
  init_dist();
@@ -4472,7 +4481,7 @@ var init_validate_top = __esm(() => {
4472
4481
  Cloning ${import_picocolors13.default.dim(args.path)}...`);
4473
4482
  try {
4474
4483
  const result = await cloneToTemp(remote);
4475
- fullPath = remote.subpath ? resolve14(result.dir, remote.subpath) : result.dir;
4484
+ fullPath = remote.subpath ? resolve15(result.dir, remote.subpath) : result.dir;
4476
4485
  cleanup = result.cleanup;
4477
4486
  } catch (err) {
4478
4487
  const msg = err instanceof Error ? err.message : String(err);
@@ -4485,7 +4494,7 @@ var init_validate_top = __esm(() => {
4485
4494
  process.exit(1);
4486
4495
  }
4487
4496
  } else {
4488
- fullPath = resolve14(args.path);
4497
+ fullPath = resolve15(args.path);
4489
4498
  if (!existsSync24(fullPath)) {
4490
4499
  ui.fail(`Path not found: ${args.path}
4491
4500
 
@@ -4822,62 +4831,121 @@ Project-specific decisions.
4822
4831
  });
4823
4832
 
4824
4833
  // src/core/update.ts
4825
- import { execSync } from "child_process";
4826
- import { existsSync as existsSync25 } from "fs";
4827
- import { resolve as resolve15 } from "path";
4834
+ import { resolve as resolve16 } from "path";
4828
4835
  import { homedir as homedir2 } from "os";
4829
- function isInPath(cmd) {
4836
+ function normalizePath(p) {
4837
+ return p.replace(/\\/g, "/").replace(/\/+$/, "");
4838
+ }
4839
+ function isInside(child, parent) {
4840
+ const c = normalizePath(child);
4841
+ const p = normalizePath(parent);
4842
+ return c === p || c.startsWith(`${p}/`);
4843
+ }
4844
+ async function realpathOrSelf(ctx, p) {
4830
4845
  try {
4831
- execSync(`which ${cmd}`, { stdio: "ignore" });
4832
- return true;
4846
+ return await ctx.realpath(p);
4833
4847
  } catch {
4834
- return false;
4848
+ return p;
4835
4849
  }
4836
4850
  }
4837
- async function autoDetect() {
4838
- const execPath = process.execPath;
4839
- const argv0 = process.argv[0] || "";
4840
- if (execPath.includes("/Cellar/") || execPath.includes("/homebrew/") || execPath.includes("/opt/homebrew/")) {
4841
- if (isInPath("brew"))
4842
- return { type: "homebrew" };
4851
+ function markerMatchesCurrentInstall(marker, realEntry) {
4852
+ if (marker.entrypointRealpath && normalizePath(marker.entrypointRealpath) === realEntry) {
4853
+ return true;
4843
4854
  }
4844
- if (execPath.includes("/.npm/") || argv0.includes("npm")) {
4845
- return { type: "npm" };
4855
+ if (marker.packageRoot && isInside(realEntry, normalizePath(marker.packageRoot))) {
4856
+ return true;
4846
4857
  }
4847
- if (execPath.includes("/.bun/") || argv0.includes("bun")) {
4848
- return { type: "bun" };
4858
+ return false;
4859
+ }
4860
+ async function detectHomebrew(ctx, entry, realEntry) {
4861
+ const prefix = await ctx.run("brew", ["--prefix", "doraval"]);
4862
+ if (!prefix.ok)
4863
+ return null;
4864
+ const brewPrefix = normalizePath(await realpathOrSelf(ctx, prefix.stdout.trim()));
4865
+ if (isInside(realEntry, brewPrefix) || realEntry.includes("/Cellar/doraval/")) {
4866
+ return { type: "homebrew", source: "probe" };
4849
4867
  }
4850
- const home = homedir2();
4851
- const possibleGlobals = [
4852
- resolve15(home, ".npm-global/bin/doraval"),
4853
- resolve15(home, ".bun/bin/doraval")
4854
- ];
4855
- for (const p of possibleGlobals) {
4856
- if (existsSync25(p)) {
4857
- if (p.includes(".npm"))
4858
- return { type: "npm" };
4859
- if (p.includes(".bun"))
4860
- return { type: "bun" };
4868
+ return null;
4869
+ }
4870
+ async function detectNpmGlobal(ctx, entry, realEntry) {
4871
+ const root = await ctx.run("npm", ["root", "-g"]);
4872
+ if (root.ok) {
4873
+ const npmRoot = normalizePath(await realpathOrSelf(ctx, root.stdout.trim()));
4874
+ if (isInside(realEntry, `${npmRoot}/@hacksmith/doraval`)) {
4875
+ return { type: "npm", source: "probe" };
4861
4876
  }
4862
4877
  }
4878
+ if (realEntry.includes("/lib/node_modules/@hacksmith/doraval/")) {
4879
+ return { type: "npm", source: "path" };
4880
+ }
4863
4881
  return null;
4864
4882
  }
4865
- async function detectInstallMethod(options) {
4883
+ async function detectBunGlobal(ctx, entry, realEntry) {
4884
+ const bunBin = await ctx.run("bun", ["pm", "bin", "-g"]);
4885
+ if (bunBin.ok) {
4886
+ for (const name of ["doraval", "dora"]) {
4887
+ const shim = normalizePath(`${bunBin.stdout.trim()}/${name}`);
4888
+ if (await ctx.exists(shim)) {
4889
+ const realShim = normalizePath(await realpathOrSelf(ctx, shim));
4890
+ if (realShim === realEntry || shim === entry) {
4891
+ return { type: "bun", source: "probe" };
4892
+ }
4893
+ }
4894
+ }
4895
+ }
4896
+ if (realEntry.includes("/.bun/install/global/node_modules/@hacksmith/doraval/")) {
4897
+ return { type: "bun", source: "path" };
4898
+ }
4899
+ return null;
4900
+ }
4901
+ function detectTransient(entry, realEntry) {
4902
+ if (realEntry.includes("/_npx/") && realEntry.includes("/node_modules/@hacksmith/doraval/")) {
4903
+ return { type: "transient", via: "npx", source: "path" };
4904
+ }
4905
+ if (realEntry.includes("/.bun/install/cache/")) {
4906
+ return { type: "transient", via: "bunx", source: "path" };
4907
+ }
4908
+ return null;
4909
+ }
4910
+ async function detectInstallMethod(ctx, options) {
4911
+ const env = ctx.env || {};
4912
+ if (env.DORAVAL_TEST) {
4913
+ return { type: "npm", source: "probe" };
4914
+ }
4866
4915
  if (options?.force) {
4867
- if (["homebrew", "npm", "bun"].includes(options.force)) {
4868
- return { type: options.force };
4916
+ const f = options.force;
4917
+ if (["homebrew", "npm", "bun"].includes(f)) {
4918
+ return { type: f, source: "probe" };
4869
4919
  }
4870
- if (options.force === "npx" || options.force === "bunx") {
4871
- return { type: "transient", via: options.force };
4920
+ if (f === "npx" || f === "bunx") {
4921
+ return { type: "transient", via: f, source: "path" };
4872
4922
  }
4873
4923
  }
4874
- const auto = await autoDetect();
4875
- if (auto)
4876
- return auto;
4877
- const marker = await readInstallMarker();
4878
- if (marker)
4879
- return marker;
4880
- return { type: "transient", via: "npx" };
4924
+ const rawEntry = ctx.entrypoint ?? ctx.argv?.[1];
4925
+ if (!rawEntry) {
4926
+ return { type: "unknown", reason: "No CLI entrypoint path available" };
4927
+ }
4928
+ const entry = normalizePath(rawEntry);
4929
+ const realEntry = normalizePath(await realpathOrSelf(ctx, rawEntry));
4930
+ const owners = await Promise.all([
4931
+ detectHomebrew(ctx, entry, realEntry),
4932
+ detectNpmGlobal(ctx, entry, realEntry),
4933
+ detectBunGlobal(ctx, entry, realEntry)
4934
+ ]);
4935
+ const owned = owners.filter(Boolean);
4936
+ if (owned.length === 1)
4937
+ return owned[0];
4938
+ const transient = detectTransient(entry, realEntry);
4939
+ if (transient)
4940
+ return transient;
4941
+ const marker = await ctx.readMarker();
4942
+ if (marker && markerMatchesCurrentInstall(marker, realEntry)) {
4943
+ return { type: marker.type, source: "marker" };
4944
+ }
4945
+ if (owned.length > 1) {
4946
+ return { type: "unknown", reason: "Multiple package managers appear to own this path" };
4947
+ }
4948
+ return { type: "unknown", reason: "Could not determine install method" };
4881
4949
  }
4882
4950
  async function fetchLatestVersionInfo() {
4883
4951
  const npmRes = await fetch("https://registry.npmjs.org/@hacksmith/doraval/latest");
@@ -4905,6 +4973,9 @@ async function fetchLatestVersionInfo() {
4905
4973
  return { version, summary };
4906
4974
  }
4907
4975
  function buildUpgradeCommand(method) {
4976
+ if (method.type === "transient" || method.type === "unknown") {
4977
+ throw new Error("Cannot build upgrade command for transient or unknown installs");
4978
+ }
4908
4979
  switch (method.type) {
4909
4980
  case "homebrew":
4910
4981
  return ["brew", "upgrade", "doraval"];
@@ -4912,8 +4983,6 @@ function buildUpgradeCommand(method) {
4912
4983
  return ["npm", "install", "-g", "@hacksmith/doraval@latest"];
4913
4984
  case "bun":
4914
4985
  return ["bun", "add", "-g", "@hacksmith/doraval@latest"];
4915
- default:
4916
- throw new Error("Cannot build upgrade command for transient installs");
4917
4986
  }
4918
4987
  }
4919
4988
  function shouldUpdate(current, latest) {
@@ -4929,27 +4998,26 @@ function shouldUpdate(current, latest) {
4929
4998
  }
4930
4999
  return false;
4931
5000
  }
4932
- async function readInstallMarker() {
5001
+ async function readMarker() {
4933
5002
  try {
4934
5003
  const { readFile } = await import("fs/promises");
4935
5004
  const data = await readFile(MARKER_PATH, "utf8");
4936
- const parsed = JSON.parse(data);
4937
- if (parsed && parsed.type)
4938
- return parsed;
4939
- } catch {}
4940
- return null;
5005
+ return JSON.parse(data);
5006
+ } catch {
5007
+ return null;
5008
+ }
4941
5009
  }
4942
- async function writeInstallMarker(method) {
5010
+ async function writeMarker(marker) {
4943
5011
  try {
4944
5012
  const { mkdir, writeFile } = await import("fs/promises");
4945
5013
  const { dirname: dirname2 } = await import("path");
4946
5014
  await mkdir(dirname2(MARKER_PATH), { recursive: true });
4947
- await writeFile(MARKER_PATH, JSON.stringify(method, null, 2));
5015
+ await writeFile(MARKER_PATH, JSON.stringify(marker, null, 2));
4948
5016
  } catch {}
4949
5017
  }
4950
5018
  var MARKER_PATH;
4951
5019
  var init_update2 = __esm(() => {
4952
- MARKER_PATH = resolve15(homedir2(), ".doraval", "install.json");
5020
+ MARKER_PATH = resolve16(homedir2(), ".doraval", "install.json");
4953
5021
  });
4954
5022
 
4955
5023
  // src/cli/commands/update.ts
@@ -4958,13 +5026,16 @@ __export(exports_update2, {
4958
5026
  default: () => update_default2
4959
5027
  });
4960
5028
  import { spawnSync as spawnSync6 } from "child_process";
5029
+ import { homedir as homedir3 } from "os";
5030
+ import { fileURLToPath } from "url";
5031
+ import { realpath, access } from "fs/promises";
4961
5032
  async function confirmUpdate() {
4962
5033
  const { createInterface } = await import("readline");
4963
5034
  const rl = createInterface({ input: process.stdin, output: process.stdout });
4964
- return new Promise((resolve16) => {
5035
+ return new Promise((resolve17) => {
4965
5036
  rl.question("Update now? (y/N) ", (answer) => {
4966
5037
  rl.close();
4967
- resolve16(answer.toLowerCase().startsWith("y"));
5038
+ resolve17(answer.toLowerCase().startsWith("y"));
4968
5039
  });
4969
5040
  });
4970
5041
  }
@@ -4988,14 +5059,37 @@ var init_update3 = __esm(() => {
4988
5059
  type: "boolean",
4989
5060
  description: "Skip confirmation prompt",
4990
5061
  default: false
5062
+ },
5063
+ via: {
5064
+ type: "string",
5065
+ description: 'Force install method detection: "homebrew" | "npm" | "bun"'
4991
5066
  }
4992
5067
  },
4993
5068
  async run({ args }) {
4994
5069
  const currentVersion = require_package().version;
4995
- const argv1 = process.argv[1] || "";
4996
- const isNpx = process.env.npm_execpath?.includes("npx") || argv1.includes("/.npm/") || process.env.npm_lifecycle_script?.includes("npx");
4997
- const isBunx = process.env.BUN_INSTALL || argv1.includes(".bun/bin/bunx") || argv1.includes("bunx");
4998
- if (isNpx || isBunx) {
5070
+ const entrypoint = fileURLToPath(import.meta.url);
5071
+ const ctx = {
5072
+ entrypoint,
5073
+ argv: process.argv,
5074
+ env: process.env,
5075
+ homeDir: homedir3(),
5076
+ realpath: (p) => realpath(p),
5077
+ exists: async (p) => {
5078
+ try {
5079
+ await access(p);
5080
+ return true;
5081
+ } catch {
5082
+ return false;
5083
+ }
5084
+ },
5085
+ run: async (cmd2, args2) => {
5086
+ const res = spawnSync6(cmd2, args2, { encoding: "utf8" });
5087
+ return { ok: res.status === 0, stdout: res.stdout || "" };
5088
+ },
5089
+ readMarker
5090
+ };
5091
+ const method = await detectInstallMethod(ctx, args.via ? { force: args.via } : undefined);
5092
+ if (method.type === "transient") {
4999
5093
  ui.info("It looks like you're using doraval via npx or bunx.");
5000
5094
  ui.info("These always fetch the latest version on the next run.");
5001
5095
  ui.info("");
@@ -5005,10 +5099,10 @@ var init_update3 = __esm(() => {
5005
5099
  ui.info(" bun add -g @hacksmith/doraval");
5006
5100
  process.exit(0);
5007
5101
  }
5008
- const method = await detectInstallMethod();
5009
- if (method.type === "transient") {
5010
- ui.info("Transient usage detected. Install globally for update support.");
5011
- process.exit(0);
5102
+ if (method.type === "unknown") {
5103
+ ui.fail(`Could not determine how doraval was installed: ${method.reason}`);
5104
+ ui.info("You can force it with --via homebrew|npm|bun");
5105
+ process.exit(2);
5012
5106
  }
5013
5107
  const latestInfo = await fetchLatestVersionInfo();
5014
5108
  if (!shouldUpdate(currentVersion, latestInfo.version)) {
@@ -5039,14 +5133,23 @@ var init_update3 = __esm(() => {
5039
5133
  if (result.status === 0) {
5040
5134
  ui.success(`Successfully updated to ${latestInfo.version}.`);
5041
5135
  ui.info("You may need to restart your shell to pick up the new version.");
5042
- await writeInstallMarker(method);
5136
+ const marker = {
5137
+ type: method.type,
5138
+ packageRoot: undefined,
5139
+ entrypointRealpath: await realpath(entrypoint).catch(() => entrypoint),
5140
+ version: latestInfo.version,
5141
+ writtenAt: new Date().toISOString()
5142
+ };
5143
+ await writeMarker(marker);
5043
5144
  } else {
5044
5145
  ui.fail("Update failed.");
5045
5146
  ui.info("Common fixes:");
5046
- if (cmd[0] === "brew")
5147
+ if (cmd[0] === "brew") {
5047
5148
  ui.info(" \u2022 Try: sudo brew upgrade doraval or ensure you are in the admin group");
5048
- if (cmd[0] === "npm" || cmd[0] === "bun")
5149
+ }
5150
+ if (cmd[0] === "npm" || cmd[0] === "bun") {
5049
5151
  ui.info(" \u2022 Try running with appropriate permissions or check network.");
5152
+ }
5050
5153
  ui.info(`
5051
5154
  Raw output above.`);
5052
5155
  process.exit(result.status ?? 1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hacksmith/doraval",
3
- "version": "0.2.26",
3
+ "version": "0.2.27",
4
4
  "author": "Saif",
5
5
  "repository": {
6
6
  "type": "git",