owlrunkit 0.14.0 → 0.15.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.
@@ -89,6 +89,98 @@ function ensureRegularDirectory(directoryPath, label) {
89
89
  return regularDirectory(directoryPath, label);
90
90
  }
91
91
 
92
+ function verificationExecutableGate(executable) {
93
+ if (!path.isAbsolute(executable)) {
94
+ return {
95
+ valid: false,
96
+ issueCode: "verify_executable_absolute_path_required",
97
+ message: "verify executable after -- must be an absolute path.",
98
+ };
99
+ }
100
+ let stat;
101
+ try {
102
+ stat = lstatSync(executable);
103
+ } catch {
104
+ return {
105
+ valid: false,
106
+ issueCode: "verify_executable_missing",
107
+ message: "verify executable must be a regular file, not a symlink.",
108
+ };
109
+ }
110
+ if (stat.isSymbolicLink()) {
111
+ return {
112
+ valid: false,
113
+ issueCode: "verify_executable_symlink_rejected",
114
+ message: "verify executable must be a regular file, not a symlink.",
115
+ };
116
+ }
117
+ if (!stat.isFile()) {
118
+ return {
119
+ valid: false,
120
+ issueCode: "verify_executable_not_regular_file",
121
+ message: "verify executable must be a regular file, not a symlink.",
122
+ };
123
+ }
124
+ return { valid: true };
125
+ }
126
+
127
+ function recordRejectedVerificationPreflight({
128
+ workspaceRoot,
129
+ executionRoot,
130
+ runId,
131
+ verificationId,
132
+ executable,
133
+ commandArgv,
134
+ issueCode,
135
+ }) {
136
+ const preflightsRoot = path.join(executionRoot, "verification-preflights");
137
+ const realRoot = ensureRegularDirectory(
138
+ preflightsRoot,
139
+ "Verification preflight directory",
140
+ );
141
+ if (
142
+ realRoot !== path.resolve(preflightsRoot)
143
+ || !within(realpathSync(executionRoot), realRoot)
144
+ || !within(realpathSync(workspaceRoot), realRoot)
145
+ ) {
146
+ throw new Error("Verification preflight directory must remain inside the execution.");
147
+ }
148
+ writeJsonExclusiveAtomically(path.join(realRoot, `${verificationId}.json`), {
149
+ schemaVersion: "OwlCodaRunKitVerificationPreflightV1",
150
+ runId,
151
+ verificationId,
152
+ status: "rejected",
153
+ issueCode,
154
+ executable,
155
+ commandArgvSha256: sha256(JSON.stringify(commandArgv)),
156
+ authorizationGranted: false,
157
+ });
158
+ }
159
+
160
+ function recordRejectedFinalizeAttempt({
161
+ workspaceRoot,
162
+ runId,
163
+ verificationId,
164
+ outputRoot,
165
+ finalizeRequestPath,
166
+ finalized,
167
+ }) {
168
+ const relativeRequestPath = relativeToWorkspace(workspaceRoot, finalizeRequestPath);
169
+ const artifactPath = path.join(outputRoot, "finalize-result.json");
170
+ writeJsonExclusiveAtomically(artifactPath, {
171
+ schemaVersion: "OwlCodaRunKitVerificationFinalizeAttemptV1",
172
+ runId,
173
+ verificationId,
174
+ status: "rejected",
175
+ gateStatus: finalized.status,
176
+ finalizeRequestPath: relativeRequestPath,
177
+ finalizeRequestSha256: sha256(readFileSync(finalizeRequestPath)),
178
+ result: finalized,
179
+ authorizationGranted: false,
180
+ });
181
+ return relativeToWorkspace(workspaceRoot, artifactPath);
182
+ }
183
+
92
184
  function gitVersion() {
93
185
  return execFileSync("git", ["--version"], { encoding: "utf8" })
94
186
  .trim()
@@ -203,6 +295,20 @@ function runHighLevelVerifyWithinControlTransaction({
203
295
  const { executionRoot, pinGate } = loadActiveExecution(workspaceRoot, runId);
204
296
  if (pinGate.status !== "valid") return { ...pinGate, authorizationGranted: false };
205
297
  regularDirectory(executionRoot, "Execution directory");
298
+ const [executable] = commandArgv;
299
+ const executableGate = verificationExecutableGate(executable);
300
+ if (!executableGate.valid) {
301
+ recordRejectedVerificationPreflight({
302
+ workspaceRoot,
303
+ executionRoot,
304
+ runId,
305
+ verificationId,
306
+ executable,
307
+ commandArgv,
308
+ issueCode: executableGate.issueCode,
309
+ });
310
+ throw new Error(executableGate.message);
311
+ }
206
312
  for (const [name, label, create] of [
207
313
  ["delivery-packets", "Delivery packet directory", false],
208
314
  ["snapshots", "Snapshot directory", true],
@@ -213,9 +319,6 @@ function runHighLevelVerifyWithinControlTransaction({
213
319
  if (create) ensureRegularDirectory(directoryPath, label);
214
320
  else regularDirectory(directoryPath, label);
215
321
  }
216
- const [executable] = commandArgv;
217
- if (!path.isAbsolute(executable)) throw new Error("verify executable after -- must be an absolute path.");
218
- regularFile(executable, "verify executable");
219
322
  const realCwd = realpathSync(cwd === "." ? workspaceRoot : path.join(workspaceRoot, cwd));
220
323
  if (!within(workspaceRoot, realCwd)) throw new Error("verify cwd escapes the workspace through a symlink.");
221
324
  for (const outputPath of [
@@ -269,7 +372,19 @@ function runHighLevelVerifyWithinControlTransaction({
269
372
  };
270
373
  writeJsonExclusiveAtomically(finalizeRequestPath, finalizeRequest);
271
374
  const finalized = runFinalize({ workspaceRoot, runId, request: finalizeRequest });
272
- if (finalized.status !== "accepted_passed") return finalized;
375
+ if (finalized.status !== "accepted_passed") {
376
+ return {
377
+ ...finalized,
378
+ finalizeResultPath: recordRejectedFinalizeAttempt({
379
+ workspaceRoot,
380
+ runId,
381
+ verificationId,
382
+ outputRoot,
383
+ finalizeRequestPath,
384
+ finalized,
385
+ }),
386
+ };
387
+ }
273
388
  return {
274
389
  status: "verified",
275
390
  exitCode: 0,
@@ -9,16 +9,18 @@ import {
9
9
  import path from "node:path";
10
10
 
11
11
  import {
12
+ CORE_VERSION,
12
13
  currentCoreIdentity,
13
14
  validateProjectConfigV2,
14
15
  } from "./core-contract.mjs";
15
16
  import {
16
17
  expectedRegistryTarballUrl,
17
18
  isCanonicalSha512Integrity,
19
+ validateStoredRegistryAdoptionEvidence,
18
20
  } from "./registry-adoption-gate.mjs";
19
21
 
20
22
  const PACKAGE_NAME = "owlrunkit";
21
- const EXPECTED_VERSION = "0.14.0";
23
+ const EXPECTED_VERSION = CORE_VERSION;
22
24
  const MAX_JSON_BYTES = 1_048_576;
23
25
  const MAX_JSON_DEPTH = 64;
24
26
  const MAX_JSON_NODES = 50_000;
@@ -473,6 +475,119 @@ function readProjectScripts(root) {
473
475
  }
474
476
  }
475
477
 
478
+ function ignoreFileCoversRuntime(root, ignoreFile) {
479
+ let text;
480
+ try {
481
+ text = readWorkspaceFileBounded(root, ignoreFile, { optional: true });
482
+ } catch {
483
+ return false;
484
+ }
485
+ if (text === null) return false;
486
+ let ignored = false;
487
+ for (const rawLine of text.split(/\r?\n/)) {
488
+ const line = rawLine.trim();
489
+ if (line.length === 0 || line.startsWith("#")) continue;
490
+ const negated = line.startsWith("!");
491
+ const pattern = (negated ? line.slice(1) : line)
492
+ .replace(/^\/+/, "")
493
+ .replace(/^\*\*\//, "")
494
+ .replace(/\/\*\*$/, "")
495
+ .replace(/\/+$/, "");
496
+ if (pattern === ".owlcoda" || pattern === ".owlcoda/runkit") {
497
+ ignored = !negated;
498
+ } else if (negated && pattern.startsWith(".owlcoda/runkit/")) {
499
+ ignored = false;
500
+ }
501
+ }
502
+ return ignored;
503
+ }
504
+
505
+ const BROAD_COLLECTION_TOOLS = Object.freeze([
506
+ {
507
+ tool: "biome",
508
+ command: /\bbiome\s+(?:check|format|lint)\b/,
509
+ ignoreFile: "biome.json",
510
+ ignoreFileSupported: false,
511
+ },
512
+ {
513
+ tool: "eslint",
514
+ command: /\beslint\b/,
515
+ ignoreFile: ".eslintignore",
516
+ ignoreFileSupported: true,
517
+ },
518
+ {
519
+ tool: "jest",
520
+ command: /\bjest\b/,
521
+ ignoreFile: "jest.config.*",
522
+ ignoreFileSupported: false,
523
+ },
524
+ {
525
+ tool: "markdownlint",
526
+ command: /\bmarkdownlint(?:-cli2)?\b/,
527
+ ignoreFile: ".markdownlintignore",
528
+ ignoreFileSupported: true,
529
+ },
530
+ {
531
+ tool: "prettier",
532
+ command: /\bprettier\b/,
533
+ ignoreFile: ".prettierignore",
534
+ ignoreFileSupported: true,
535
+ },
536
+ {
537
+ tool: "stylelint",
538
+ command: /\bstylelint\b/,
539
+ ignoreFile: ".stylelintignore",
540
+ ignoreFileSupported: true,
541
+ },
542
+ {
543
+ tool: "vitest",
544
+ command: /\bvitest\b/,
545
+ ignoreFile: "vitest.config.*",
546
+ ignoreFileSupported: false,
547
+ },
548
+ ]);
549
+
550
+ function hasBroadDotTarget(command) {
551
+ return /(?:^|\s|&&|\|\||;)(?:["']?\.\/?["']?)(?=\s|$|&&|\|\||;)/.test(command);
552
+ }
553
+
554
+ function runtimeIsolationCheck(root) {
555
+ let manifest;
556
+ try {
557
+ manifest = readWorkspaceJsonBounded(root, "package.json");
558
+ } catch {
559
+ return {
560
+ status: "unknown",
561
+ affectedScripts: [],
562
+ recommendedIgnore: ".owlcoda/runkit/",
563
+ };
564
+ }
565
+ const affectedScripts = [];
566
+ for (const [script, value] of Object.entries(manifest.scripts ?? {}).sort(([left], [right]) =>
567
+ left.localeCompare(right))) {
568
+ if (typeof value !== "string" || !hasBroadDotTarget(value)) continue;
569
+ for (const descriptor of BROAD_COLLECTION_TOOLS) {
570
+ if (!descriptor.command.test(value)) continue;
571
+ if (
572
+ descriptor.ignoreFileSupported
573
+ && ignoreFileCoversRuntime(root, descriptor.ignoreFile)
574
+ ) {
575
+ continue;
576
+ }
577
+ affectedScripts.push({
578
+ ignoreFile: descriptor.ignoreFile,
579
+ script,
580
+ tool: descriptor.tool,
581
+ });
582
+ }
583
+ }
584
+ return {
585
+ status: affectedScripts.length === 0 ? "ok" : "warning",
586
+ affectedScripts,
587
+ recommendedIgnore: ".owlcoda/runkit/",
588
+ };
589
+ }
590
+
476
591
  function configCheck(root) {
477
592
  try {
478
593
  const config = readWorkspaceJsonBounded(
@@ -496,14 +611,81 @@ function profilesCheck(root) {
496
611
  { optional: true },
497
612
  );
498
613
  if (profiles === null) return { status: "missing" };
499
- return profiles.schemaVersion === "OwlCodaRunKitProfilesV1" && Array.isArray(profiles.profiles)
500
- ? { status: "ok", count: profiles.profiles.length }
501
- : { status: "invalid" };
614
+ if (
615
+ profiles.schemaVersion !== "OwlCodaRunKitProfilesV1"
616
+ || !Array.isArray(profiles.profiles)
617
+ ) {
618
+ return { status: "invalid" };
619
+ }
620
+ const launcherWarnings = [];
621
+ for (const profile of profiles.profiles) {
622
+ for (const command of profile?.commands ?? []) {
623
+ const identity = `${profile?.id ?? "unknown"}:${command?.id ?? "unknown"}`;
624
+ if (typeof command?.executable !== "string" || !path.isAbsolute(command.executable)) {
625
+ launcherWarnings.push(`profile_launcher_not_absolute:${identity}`);
626
+ continue;
627
+ }
628
+ try {
629
+ const stat = lstatSync(command.executable);
630
+ if (stat.isSymbolicLink()) {
631
+ launcherWarnings.push(`profile_launcher_symlink_rejected:${identity}`);
632
+ } else if (!stat.isFile() || realpathSync(command.executable) !== path.resolve(command.executable)) {
633
+ launcherWarnings.push(`profile_launcher_not_regular_file:${identity}`);
634
+ }
635
+ } catch {
636
+ launcherWarnings.push(`profile_launcher_unavailable:${identity}`);
637
+ }
638
+ }
639
+ }
640
+ return {
641
+ status: "ok",
642
+ count: profiles.profiles.length,
643
+ launcherWarnings: [...new Set(launcherWarnings)].sort(),
644
+ };
502
645
  } catch {
503
646
  return { status: "invalid" };
504
647
  }
505
648
  }
506
649
 
650
+ export function readProfileLauncherReadiness({ workspaceRoot } = {}) {
651
+ const profiles = profilesCheck(resolveBoundedWorkspaceRoot(workspaceRoot));
652
+ const launcherWarnings = profiles.launcherWarnings ?? [];
653
+ return {
654
+ status: profiles.status,
655
+ launcherWarnings,
656
+ ready: profiles.status === "ok" && launcherWarnings.length === 0,
657
+ };
658
+ }
659
+
660
+ function adoptionCheck(root, localInstall = readLocalInstallBinding({ workspaceRoot: root })) {
661
+ try {
662
+ const evidence = readWorkspaceJsonBounded(
663
+ root,
664
+ path.join(".owlcoda", "runkit", "adoption", `${PACKAGE_NAME}-${EXPECTED_VERSION}.json`),
665
+ { optional: true },
666
+ );
667
+ if (evidence === null) return { status: "missing", version: EXPECTED_VERSION };
668
+ const validation = validateStoredRegistryAdoptionEvidence({
669
+ evidence,
670
+ packageName: PACKAGE_NAME,
671
+ version: EXPECTED_VERSION,
672
+ localInstall,
673
+ });
674
+ return {
675
+ status: validation.valid ? "ok" : "invalid",
676
+ version: EXPECTED_VERSION,
677
+ issueCodes: validation.issueCodes,
678
+ };
679
+ } catch {
680
+ return { status: "invalid", version: EXPECTED_VERSION };
681
+ }
682
+ }
683
+
684
+ export function readAdoptionReadiness({ workspaceRoot } = {}) {
685
+ const root = resolveBoundedWorkspaceRoot(workspaceRoot);
686
+ return adoptionCheck(root);
687
+ }
688
+
507
689
  export async function runDoctor({
508
690
  workspaceRoot,
509
691
  registryUrl,
@@ -524,6 +706,8 @@ export async function runDoctor({
524
706
  const nodeOk = Number(process.versions.node.split(".")[0]) >= 20;
525
707
  const config = configCheck(root);
526
708
  const profiles = profilesCheck(root);
709
+ const adoption = adoptionCheck(root, installed);
710
+ const runtimeIsolation = runtimeIsolationCheck(root);
527
711
  const checks = {
528
712
  node: {
529
713
  status: nodeOk ? "ok" : "unsupported",
@@ -542,10 +726,12 @@ export async function runDoctor({
542
726
  ? { status: "ok", version: registry.version }
543
727
  : { status: "issue", issueCodes: registry.issueCodes ?? [] },
544
728
  profiles,
729
+ adoption,
545
730
  projectScripts: {
546
731
  status: "observed",
547
732
  detected: readProjectScripts(root),
548
733
  },
734
+ runtimeIsolation,
549
735
  };
550
736
  const issueCodes = [...(registry.issueCodes ?? [])];
551
737
  if (!nodeOk) issueCodes.push("node_version_unsupported");
@@ -569,8 +755,17 @@ export async function runDoctor({
569
755
  issueCodes.push("registry_install_binding_mismatch");
570
756
  }
571
757
  const uniqueIssues = [...new Set(issueCodes)].sort();
758
+ const warningCodes = [
759
+ ...runtimeIsolation.affectedScripts
760
+ .map(({ script, tool }) => `runkit_runtime_collection_risk:${tool}:${script}`),
761
+ ...(profiles.launcherWarnings ?? []),
762
+ ].sort();
572
763
  const nextAllowedAction = uniqueIssues.length === 0
573
- ? "detect_profiles"
764
+ ? (profiles.launcherWarnings ?? []).length > 0
765
+ ? "review_profile_launchers"
766
+ : adoption.status === "ok"
767
+ ? "plan_new_execution"
768
+ : "adopt_current_release"
574
769
  : uniqueIssues.some((code) => code === "node_version_unsupported")
575
770
  ? "upgrade_node"
576
771
  : uniqueIssues.some((code) => code === "core_version_mismatch")
@@ -585,6 +780,7 @@ export async function runDoctor({
585
780
  status: uniqueIssues.length === 0 ? "ready" : "issues_found",
586
781
  checks,
587
782
  issueCodes: uniqueIssues,
783
+ warningCodes,
588
784
  nextAllowedAction,
589
785
  authorizationGranted: false,
590
786
  };
@@ -598,9 +794,11 @@ export function formatDoctorHuman(report) {
598
794
  `Installed package: ${report.checks.installedPackage.status}`,
599
795
  `Registry: ${report.checks.registry.status}`,
600
796
  `Profiles: ${report.checks.profiles.status}`,
797
+ `Adoption: ${report.checks.adoption.status}`,
601
798
  ...(report.issueCodes.length > 0 ? [`Issues: ${report.issueCodes.join(", ")}`] : []),
602
- `Next: ${report.nextAllowedAction === "detect_profiles"
603
- ? "owlrunkit profiles detect --workspace . --dry-run"
799
+ ...(report.warningCodes.length > 0 ? [`Warnings: ${report.warningCodes.join(", ")}`] : []),
800
+ `Next: ${report.nextAllowedAction === "adopt_current_release"
801
+ ? `owlrunkit adopt --workspace . --exact ${PACKAGE_NAME}@${EXPECTED_VERSION}`
604
802
  : report.nextAllowedAction}`,
605
803
  "",
606
804
  ].join("\n");
@@ -1,5 +1,8 @@
1
1
  import {
2
2
  existsSync,
3
+ lstatSync,
4
+ readdirSync,
5
+ realpathSync,
3
6
  } from "node:fs";
4
7
  import path from "node:path";
5
8
 
@@ -13,20 +16,172 @@ import {
13
16
  safeRelativePath,
14
17
  } from "./provenance-common.mjs";
15
18
 
16
- function command(id, script) {
19
+ function resolveRegularExecutable(name) {
20
+ const pathEntries = (process.env.PATH ?? "").split(path.delimiter);
21
+ const suffixes = process.platform === "win32"
22
+ ? (process.env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";")
23
+ : [""];
24
+ for (const directory of pathEntries) {
25
+ for (const suffix of suffixes) {
26
+ const candidate = path.resolve(directory || ".", `${name}${suffix}`);
27
+ try {
28
+ const resolved = realpathSync(candidate);
29
+ const stat = lstatSync(resolved);
30
+ if (stat.isFile() && !stat.isSymbolicLink()) return resolved;
31
+ } catch {
32
+ // Continue through PATH without treating a missing or unsafe entry as authority.
33
+ }
34
+ }
35
+ }
36
+ return null;
37
+ }
38
+
39
+ function command(id, script, executable) {
17
40
  return {
18
41
  id,
19
42
  cwd: ".",
20
- executable: "npm",
43
+ executable,
21
44
  argv: ["run", script],
22
45
  };
23
46
  }
24
47
 
48
+ function workspaceCommand(id, workspacePath, script, executable) {
49
+ return {
50
+ id,
51
+ cwd: ".",
52
+ executable,
53
+ argv: ["run", "--workspace", workspacePath, script],
54
+ };
55
+ }
56
+
25
57
  function detectedScripts(manifest) {
26
58
  return Object.keys(manifest.scripts ?? {}).sort();
27
59
  }
28
60
 
29
- function profileCandidates(root, manifest) {
61
+ function workspacePatterns(manifest) {
62
+ if (Array.isArray(manifest.workspaces)) return manifest.workspaces;
63
+ if (
64
+ manifest.workspaces
65
+ && typeof manifest.workspaces === "object"
66
+ && Array.isArray(manifest.workspaces.packages)
67
+ ) {
68
+ return manifest.workspaces.packages;
69
+ }
70
+ return [];
71
+ }
72
+
73
+ function safeWorkspacePattern(value) {
74
+ if (
75
+ typeof value !== "string"
76
+ || value.length === 0
77
+ || path.isAbsolute(value)
78
+ || value.includes("\\")
79
+ || value.includes("\0")
80
+ ) {
81
+ return null;
82
+ }
83
+ const segments = value.split("/");
84
+ if (segments.some((segment) => segment.length === 0 || segment === "." || segment === "..")) {
85
+ return null;
86
+ }
87
+ const wildcardCount = [...value].filter((character) => character === "*").length;
88
+ if (wildcardCount === 0) return { kind: "literal", path: value };
89
+ if (wildcardCount === 1 && value.endsWith("/*")) {
90
+ return { kind: "children", path: value.slice(0, -2) };
91
+ }
92
+ return null;
93
+ }
94
+
95
+ function discoverWorkspaces(root, manifest) {
96
+ const warnings = [];
97
+ const paths = new Set();
98
+ for (const rawPattern of workspacePatterns(manifest)) {
99
+ const pattern = safeWorkspacePattern(rawPattern);
100
+ if (pattern === null) {
101
+ warnings.push(`unsupported_workspace_pattern:${String(rawPattern)}`);
102
+ continue;
103
+ }
104
+ if (pattern.kind === "literal") {
105
+ paths.add(pattern.path);
106
+ continue;
107
+ }
108
+ const parent = path.join(root, pattern.path);
109
+ try {
110
+ const parentStat = lstatSync(parent);
111
+ if (
112
+ parentStat.isSymbolicLink()
113
+ || !parentStat.isDirectory()
114
+ || realpathSync(parent) !== path.resolve(parent)
115
+ ) {
116
+ warnings.push(`unsafe_workspace_parent:${pattern.path}`);
117
+ continue;
118
+ }
119
+ for (const entry of readdirSync(parent, { withFileTypes: true })
120
+ .sort((left, right) => left.name.localeCompare(right.name))) {
121
+ if (entry.isDirectory() && !entry.isSymbolicLink()) {
122
+ paths.add(`${pattern.path}/${entry.name}`);
123
+ }
124
+ }
125
+ } catch {
126
+ // A declared workspace pattern may legitimately have no current matches.
127
+ }
128
+ }
129
+
130
+ const workspaces = [];
131
+ for (const workspacePath of [...paths].sort()) {
132
+ try {
133
+ const workspaceManifest = readWorkspaceJsonBounded(
134
+ root,
135
+ path.join(workspacePath, "package.json"),
136
+ );
137
+ workspaces.push({
138
+ name: typeof workspaceManifest.name === "string"
139
+ ? workspaceManifest.name
140
+ : workspacePath,
141
+ path: workspacePath,
142
+ scripts: detectedScripts(workspaceManifest),
143
+ });
144
+ } catch {
145
+ warnings.push(`workspace_manifest_unreadable:${workspacePath}`);
146
+ }
147
+ }
148
+ return {
149
+ workspaces,
150
+ warnings: [...new Set(warnings)].sort(),
151
+ };
152
+ }
153
+
154
+ function profileIdForWorkspace(workspacePath) {
155
+ return `workspace-${workspacePath.replace(/[^A-Za-z0-9._-]+/g, "-")}`;
156
+ }
157
+
158
+ function workspaceProfiles(workspaces, npmExecutable) {
159
+ return workspaces
160
+ .map((workspace) => {
161
+ const qualityScripts = workspace.scripts.filter((script) =>
162
+ ["build", "e2e", "lint", "test", "typecheck"].includes(script));
163
+ if (qualityScripts.length === 0) return null;
164
+ const profileId = profileIdForWorkspace(workspace.path);
165
+ return {
166
+ id: profileId,
167
+ paths: [`${workspace.path}/**`],
168
+ role: "primary",
169
+ primary: false,
170
+ requiresProfileIds: [],
171
+ commands: npmExecutable
172
+ ? qualityScripts.map((script) => workspaceCommand(
173
+ `npm-${profileId}-${script}`,
174
+ workspace.path,
175
+ script,
176
+ npmExecutable,
177
+ ))
178
+ : [],
179
+ };
180
+ })
181
+ .filter(Boolean);
182
+ }
183
+
184
+ function profileCandidates(root, manifest, npmExecutable, workspaces) {
30
185
  const scripts = new Set(detectedScripts(manifest));
31
186
  const candidates = [];
32
187
  if (
@@ -35,7 +190,10 @@ function profileCandidates(root, manifest) {
35
190
  ) {
36
191
  const commands = [...scripts]
37
192
  .filter((script) => /^(db:|prisma)/.test(script))
38
- .map((script) => command(`npm-${script.replaceAll(":", "-")}`, script));
193
+ .map((script) => npmExecutable
194
+ ? command(`npm-${script.replaceAll(":", "-")}`, script, npmExecutable)
195
+ : null)
196
+ .filter(Boolean);
39
197
  candidates.push({
40
198
  id: "database",
41
199
  paths: ["prisma/**"],
@@ -51,7 +209,8 @@ function profileCandidates(root, manifest) {
51
209
  ) {
52
210
  const commands = ["build", "lint", "test", "typecheck"]
53
211
  .filter((script) => scripts.has(script))
54
- .map((script) => command(`npm-${script}`, script));
212
+ .map((script) => npmExecutable ? command(`npm-${script}`, script, npmExecutable) : null)
213
+ .filter(Boolean);
55
214
  candidates.push({
56
215
  id: "node-quality",
57
216
  paths: ["src/**", "tests/**"],
@@ -66,7 +225,9 @@ function profileCandidates(root, manifest) {
66
225
  || existsSync(path.join(root, "playwright.config.js"))
67
226
  || scripts.has("e2e")
68
227
  ) {
69
- const commands = scripts.has("e2e") ? [command("npm-e2e", "e2e")] : [];
228
+ const commands = scripts.has("e2e") && npmExecutable
229
+ ? [command("npm-e2e", "e2e", npmExecutable)]
230
+ : [];
70
231
  candidates.push({
71
232
  id: "web-e2e",
72
233
  paths: ["playwright.config.ts", "tests/e2e/**"],
@@ -76,14 +237,32 @@ function profileCandidates(root, manifest) {
76
237
  commands,
77
238
  });
78
239
  }
240
+ candidates.push(...workspaceProfiles(workspaces, npmExecutable));
79
241
  return candidates.sort((left, right) => left.id.localeCompare(right.id));
80
242
  }
81
243
 
82
244
  export function detectProfiles({ workspaceRoot } = {}) {
83
245
  const root = resolveBoundedWorkspaceRoot(workspaceRoot);
84
246
  const manifest = readWorkspaceJsonBounded(root, "package.json");
85
- const candidates = profileCandidates(root, manifest);
247
+ const npmExecutable = resolveRegularExecutable("npm");
248
+ const discovered = discoverWorkspaces(root, manifest);
249
+ const candidates = profileCandidates(root, manifest, npmExecutable, discovered.workspaces);
86
250
  const primaryCandidates = candidates.filter((profile) => profile.role === "primary");
251
+ const warnings = primaryCandidates.length > 1
252
+ ? [`ambiguous_primary_profile:${primaryCandidates.map((profile) => profile.id).join(",")}`]
253
+ : [];
254
+ warnings.push(...discovered.warnings);
255
+ if (
256
+ npmExecutable === null
257
+ && (
258
+ detectedScripts(manifest).some((script) =>
259
+ /^(build|db:|e2e|lint|prisma|test|typecheck)/.test(script))
260
+ || discovered.workspaces.some((workspace) => workspace.scripts.some((script) =>
261
+ /^(build|e2e|lint|test|typecheck)$/.test(script)))
262
+ )
263
+ ) {
264
+ warnings.push("npm_regular_executable_unavailable");
265
+ }
87
266
  return {
88
267
  schemaVersion: "OwlCodaRunKitProfileDetectionV1",
89
268
  status: "profiles_detected",
@@ -91,10 +270,9 @@ export function detectProfiles({ workspaceRoot } = {}) {
91
270
  writesPerformed: 0,
92
271
  candidates,
93
272
  primaryProfileId: primaryCandidates.length === 1 ? primaryCandidates[0].id : null,
94
- warnings: primaryCandidates.length > 1
95
- ? [`ambiguous_primary_profile:${primaryCandidates.map((profile) => profile.id).join(",")}`]
96
- : [],
273
+ warnings,
97
274
  detectedScripts: detectedScripts(manifest),
275
+ detectedWorkspaces: discovered.workspaces,
98
276
  authorizationGranted: false,
99
277
  };
100
278
  }