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.
- package/CHANGELOG.md +27 -0
- package/README.md +46 -2
- package/integrations/codex/skills/owlcoda-runkit/SKILL.md +76 -14
- package/integrations/codex/skills/owlcoda-runkit/assets/templates/config.json +1 -1
- package/package.json +1 -1
- package/packages/attest/distribution-manifest-v1.json +5 -0
- package/packages/attest/src/quick.mjs +5 -0
- package/registry-release-policy-v1.json +1 -1
- package/sbom.spdx.json +5 -5
- package/scripts/runkit-contract/core-contract.mjs +140 -4
- package/scripts/runkit-contract/install-codex-skill.mjs +505 -11
- package/scripts/runkit-contract/lifecycle-orchestration.mjs +119 -4
- package/scripts/runkit-contract/onboarding-doctor.mjs +205 -7
- package/scripts/runkit-contract/profile-onboarding.mjs +188 -10
- package/scripts/runkit-contract/registry-adoption-gate.mjs +111 -0
- package/scripts/runkit-contract/registry-adoption.mjs +2 -1
- package/scripts/runkit-contract/runkit-cli.mjs +65 -23
- package/scripts/runkit-contract/schemas/verification-finalize-attempt-v1.schema.json +52 -0
- package/scripts/runkit-contract/schemas/verification-preflight-v1.schema.json +52 -0
|
@@ -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")
|
|
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 =
|
|
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
|
-
|
|
500
|
-
|
|
501
|
-
|
|
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
|
-
?
|
|
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
|
-
`
|
|
603
|
-
|
|
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
|
|
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
|
|
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
|
|
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) =>
|
|
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")
|
|
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
|
|
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
|
|
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
|
}
|