@orkestrel/scaffold 0.0.31 → 0.0.32
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/dist/bin/main.js +144 -23
- package/dist/bin/main.js.map +1 -1
- package/dist/host/agents/orchestration.md +20 -1
- package/dist/host/claude/agents/grok.md +17 -6
- package/dist/host/claude/rules/architecture.md +12 -0
- package/dist/host/claude/rules/documentation.md +2 -1
- package/dist/host/claude/rules/tests.md +57 -21
- package/dist/host/claude/rules/workspace.md +25 -20
- package/dist/host/dotfiles/oxlintrc.json +1 -0
- package/dist/host/guides/scaffold.md +142 -67
- package/dist/host/tests/config.test.ts +45 -17
- package/dist/host/tests/policy.test.ts +254 -2
- package/dist/host/tests/setupPolicy.ts +297 -34
- package/dist/src/core/index.cjs +176 -179
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +90 -33
- package/dist/src/core/index.d.ts +90 -33
- package/dist/src/core/index.js +171 -180
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +13 -1
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.js +14 -2
- package/dist/src/server/index.js.map +1 -1
- package/package.json +4 -4
package/dist/bin/main.js
CHANGED
|
@@ -3,7 +3,7 @@ import { execFileSync } from "node:child_process";
|
|
|
3
3
|
import { align, renderTable, strip, stripControls, width } from "@orkestrel/console";
|
|
4
4
|
import { attempt, isRecord, isString, parseJSON } from "@orkestrel/contract";
|
|
5
5
|
import { createMarkdown, flattenText, isTableNode } from "@orkestrel/markdown";
|
|
6
|
-
import { BIN_ENTRY_PATH, CATALOG_AGENT_PATH, CONFORMANCE_TEST_PATH, DEPENDENCY_NAME_PATTERN, ENVIRONMENTS, GLOBAL_SETUP_PATH, GROUPS, GUIDES_TEST_PATH, INTEGRATION_TEST_PATH, MAX_MANIFEST_BYTES, SERVICE_SETUP_PATH, SHOWCASE_CONFIG_PATH, ScaffoldError, blueprintToRootVite, createBlueprint, createCompiler, isScaffoldError, manifestToDependencies, manifestToName, nameToGuide } from "../src/core/index.js";
|
|
6
|
+
import { BIN_ENTRY_PATH, CATALOG_AGENT_PATH, CONFORMANCE_TEST_PATH, DEPENDENCY_NAME_PATTERN, DISTRIBUTION_TEST_PATH, ENVIRONMENTS, GLOBAL_SETUP_PATH, GROUPS, GUIDES_TEST_PATH, INTEGRATION_TEST_PATH, MAX_MANIFEST_BYTES, SERVICE_SETUP_PATH, SHOWCASE_CONFIG_PATH, ScaffoldError, blueprintToDevDependencies, blueprintToRootVite, blueprintToScripts, createBlueprint, createCompiler, isScaffoldError, manifestToDependencies, manifestToName, nameToGuide } from "../src/core/index.js";
|
|
7
7
|
import { createMaterializer, createUpstream, isExactCaseFile, isPhysicalDirectory, isRepository, readFileText, readSnapshot, resolveContainedPath } from "../src/server/index.js";
|
|
8
8
|
import { parseArgs } from "node:util";
|
|
9
9
|
//#region src/bin/constants.ts
|
|
@@ -620,13 +620,13 @@ var CLI = class CLI {
|
|
|
620
620
|
#inspect(command) {
|
|
621
621
|
const target = command.target ?? ".";
|
|
622
622
|
const blueprint = this.#derive(target);
|
|
623
|
-
const
|
|
623
|
+
const questions = this.#targetQuestions(target, blueprint);
|
|
624
624
|
const materializer = createMaterializer(command.from === void 0 ? void 0 : { host: command.from });
|
|
625
625
|
try {
|
|
626
626
|
const [measured] = this.#survey(materializer, blueprint, target, this.#groups(command.groups));
|
|
627
|
-
const audit =
|
|
627
|
+
const audit = questions.length === 0 ? measured : {
|
|
628
628
|
...measured,
|
|
629
|
-
questions: [...measured.questions,
|
|
629
|
+
questions: [...measured.questions, ...questions]
|
|
630
630
|
};
|
|
631
631
|
if (command.json === true) this.#report(audit);
|
|
632
632
|
else this.#present(audit);
|
|
@@ -639,7 +639,7 @@ var CLI = class CLI {
|
|
|
639
639
|
const target = command.target ?? ".";
|
|
640
640
|
const groups = this.#groups(command.groups);
|
|
641
641
|
const blueprint = this.#derive(target);
|
|
642
|
-
this.#
|
|
642
|
+
this.#assertTarget(target, blueprint);
|
|
643
643
|
const materializer = createMaterializer(command.from === void 0 ? void 0 : { host: command.from });
|
|
644
644
|
try {
|
|
645
645
|
const [audit, plan] = this.#survey(materializer, blueprint, target, groups);
|
|
@@ -657,6 +657,7 @@ var CLI = class CLI {
|
|
|
657
657
|
if (command.json === true) this.#report(outcome);
|
|
658
658
|
else {
|
|
659
659
|
this.#present(terminal);
|
|
660
|
+
this.#reportReplacements(audit, terminal);
|
|
660
661
|
this.#say(this.#tally(result));
|
|
661
662
|
}
|
|
662
663
|
return auditToExit(terminal);
|
|
@@ -691,7 +692,7 @@ var CLI = class CLI {
|
|
|
691
692
|
const target = command.target ?? ".";
|
|
692
693
|
const groups = this.#groups(command.groups);
|
|
693
694
|
const blueprint = this.#derive(target);
|
|
694
|
-
this.#
|
|
695
|
+
this.#assertTarget(target, blueprint);
|
|
695
696
|
const repository = this.#repository(target);
|
|
696
697
|
if (repository.dirty.length > 0 && command.dirty !== true) throw new ScaffoldError("TARGET", `The target at ${target} carries ${String(repository.dirty.length)} uncommitted change${repository.dirty.length === 1 ? "" : "s"}. Commit them, or pass --dirty to waive the refusal.`, {
|
|
697
698
|
target,
|
|
@@ -721,6 +722,7 @@ var CLI = class CLI {
|
|
|
721
722
|
if (command.json === true) this.#report(outcome);
|
|
722
723
|
else {
|
|
723
724
|
this.#present(terminal);
|
|
725
|
+
this.#reportReplacements(audit, terminal);
|
|
724
726
|
this.#recount(outcome);
|
|
725
727
|
if (online.note !== void 0) this.#warn(online.note);
|
|
726
728
|
}
|
|
@@ -820,6 +822,8 @@ var CLI = class CLI {
|
|
|
820
822
|
const declared = manifestToName(manifest);
|
|
821
823
|
if (declared === void 0) throw new ScaffoldError("TARGET", `The manifest at ${target} declares no package name.`, { target });
|
|
822
824
|
const bin = resolveContainedPath(target, BIN_ENTRY_PATH);
|
|
825
|
+
const guides = resolveContainedPath(target, GUIDES_TEST_PATH);
|
|
826
|
+
const distribution = resolveContainedPath(target, DISTRIBUTION_TEST_PATH);
|
|
823
827
|
const integration = resolveContainedPath(target, INTEGRATION_TEST_PATH);
|
|
824
828
|
const conformance = resolveContainedPath(target, CONFORMANCE_TEST_PATH);
|
|
825
829
|
const service = resolveContainedPath(target, SERVICE_SETUP_PATH);
|
|
@@ -830,6 +834,8 @@ var CLI = class CLI {
|
|
|
830
834
|
app: this.#probe(target, "app"),
|
|
831
835
|
dependencies: manifestToDependencies(manifest),
|
|
832
836
|
bin: bin !== void 0 && isExactCaseFile(bin),
|
|
837
|
+
guides: guides !== void 0 && isExactCaseFile(guides),
|
|
838
|
+
distribution: distribution !== void 0 && isExactCaseFile(distribution),
|
|
833
839
|
integration: integration !== void 0 && isExactCaseFile(integration),
|
|
834
840
|
conformance: conformance !== void 0 && isExactCaseFile(conformance),
|
|
835
841
|
service: service !== void 0 && isExactCaseFile(service),
|
|
@@ -837,7 +843,7 @@ var CLI = class CLI {
|
|
|
837
843
|
showcase: showcase !== void 0 && isExactCaseFile(showcase)
|
|
838
844
|
});
|
|
839
845
|
}
|
|
840
|
-
#
|
|
846
|
+
#invocations(script) {
|
|
841
847
|
const tokens = [];
|
|
842
848
|
let value = "";
|
|
843
849
|
let resolved = true;
|
|
@@ -912,9 +918,25 @@ var CLI = class CLI {
|
|
|
912
918
|
resolved
|
|
913
919
|
});
|
|
914
920
|
const projects = [];
|
|
921
|
+
const scripts = [];
|
|
915
922
|
for (let index = 0; index < tokens.length; index += 1) {
|
|
916
923
|
const token = tokens[index];
|
|
917
924
|
if (token === void 0) return void 0;
|
|
925
|
+
if (token.value === "npm" && tokens[index + 1]?.value === "run") {
|
|
926
|
+
const name = tokens[index + 2];
|
|
927
|
+
if (name === void 0 || !name.resolved || name.value.length === 0 || [
|
|
928
|
+
"&&",
|
|
929
|
+
"||",
|
|
930
|
+
";",
|
|
931
|
+
"|",
|
|
932
|
+
"&",
|
|
933
|
+
"(",
|
|
934
|
+
")"
|
|
935
|
+
].includes(name.value)) return void 0;
|
|
936
|
+
scripts.push(name.value);
|
|
937
|
+
index += 2;
|
|
938
|
+
continue;
|
|
939
|
+
}
|
|
918
940
|
if (token.value === "--project") {
|
|
919
941
|
const project = tokens[index + 1];
|
|
920
942
|
if (project === void 0 || !project.resolved || project.value.length === 0 || [
|
|
@@ -938,45 +960,128 @@ var CLI = class CLI {
|
|
|
938
960
|
}
|
|
939
961
|
if (!token.resolved && token.value.includes("--project")) return void 0;
|
|
940
962
|
}
|
|
941
|
-
return
|
|
963
|
+
return {
|
|
964
|
+
projects,
|
|
965
|
+
scripts
|
|
966
|
+
};
|
|
942
967
|
}
|
|
943
968
|
#projectQuestion(target, blueprint, writing = false) {
|
|
944
969
|
const manifest = this.#manifest(target);
|
|
945
|
-
const guides = resolveContainedPath(target, GUIDES_TEST_PATH);
|
|
946
970
|
const planned = blueprintToRootVite(blueprint);
|
|
947
971
|
const parsed = parseJSON(manifest);
|
|
948
|
-
const scripts = isRecord(parsed) && isRecord(parsed.scripts) ? parsed.scripts :
|
|
972
|
+
const scripts = isRecord(parsed) && isRecord(parsed.scripts) ? parsed.scripts : {};
|
|
973
|
+
const gates = !isRecord(parsed) || parsed.private !== true ? ["test", "prepublishOnly"] : ["test"];
|
|
974
|
+
const gateNames = gates.join(" or ");
|
|
949
975
|
const absent = /* @__PURE__ */ new Set();
|
|
950
976
|
let unresolved = false;
|
|
951
|
-
|
|
977
|
+
for (const script of Object.values(scripts)) {
|
|
952
978
|
if (!isString(script) || !script.includes("vitest")) continue;
|
|
953
|
-
const
|
|
954
|
-
if (
|
|
979
|
+
const invoked = this.#invocations(script);
|
|
980
|
+
if (invoked === void 0) {
|
|
955
981
|
unresolved = true;
|
|
956
982
|
continue;
|
|
957
983
|
}
|
|
958
|
-
for (const project of projects) {
|
|
959
|
-
const guide = project === "guides" && guides !== void 0 && isExactCaseFile(guides);
|
|
960
|
-
if (!planned.includes(`name: { label: '${project}',`) || project === "guides" && !guide) absent.add(project);
|
|
961
|
-
}
|
|
984
|
+
for (const project of invoked.projects) if (!planned.includes(`name: { label: '${project}',`)) absent.add(project);
|
|
962
985
|
}
|
|
963
|
-
if (absent.size === 0 && !unresolved) return void 0;
|
|
964
986
|
const projects = [...absent].sort();
|
|
965
987
|
if (unresolved) return {
|
|
966
988
|
field: "projects",
|
|
967
989
|
message: `The manifest at ${target} contains a Vitest project expression that cannot be resolved statically.${projects.length === 0 ? "" : ` It also names projects the planned configuration does not register: ${projects.join(", ")}.`} ${writing ? "Replace it with a literal --project value or remove the script before using a scaffold writing verb." : "Replace it with a literal --project value before relying on the planned configuration."}`,
|
|
968
990
|
blocking: false
|
|
969
991
|
};
|
|
970
|
-
return {
|
|
992
|
+
if (projects.length > 0) return {
|
|
971
993
|
field: "projects",
|
|
972
994
|
message: writing ? `The manifest at ${target} names ${projects.length === 1 ? "a Vitest project" : "Vitest projects"} the planned configuration does not register: ${projects.join(", ")}. To continue, remove the ${projects.length === 1 ? "script that names it" : "scripts that name them"} or do not use scaffold writing verbs on a workspace that needs ${projects.length === 1 ? "a custom Vitest project" : "custom Vitest projects"}.` : `The manifest at ${target} names ${projects.length === 1 ? "a Vitest project" : "Vitest projects"} the planned configuration does not register: ${projects.join(", ")}. Add ${projects.length === 1 ? "the project" : "each project"} to vite.config.ts or remove the ${projects.length === 1 ? "script that names it" : "scripts that name them"}.`,
|
|
973
995
|
blocking: false
|
|
974
996
|
};
|
|
997
|
+
const expected = blueprintToScripts(blueprint);
|
|
998
|
+
const expectedLines = /* @__PURE__ */ new Map();
|
|
999
|
+
for (const [name, script] of Object.entries(expected)) {
|
|
1000
|
+
if (!name.startsWith("test:") || name === "test:probe") continue;
|
|
1001
|
+
const invoked = this.#invocations(script);
|
|
1002
|
+
if (invoked === void 0) continue;
|
|
1003
|
+
for (const project of invoked.projects) {
|
|
1004
|
+
if (project === "probe") continue;
|
|
1005
|
+
const direct = `test:${project}`;
|
|
1006
|
+
const command = expected[direct];
|
|
1007
|
+
if (command !== void 0) expectedLines.set(project, `"${direct}": ${JSON.stringify(command)},`);
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
const reachable = /* @__PURE__ */ new Set();
|
|
1011
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1012
|
+
const pending = [...gates];
|
|
1013
|
+
while (pending.length > 0) {
|
|
1014
|
+
const name = pending.shift();
|
|
1015
|
+
if (name === void 0 || visited.has(name)) continue;
|
|
1016
|
+
visited.add(name);
|
|
1017
|
+
const script = scripts[name];
|
|
1018
|
+
if (!isString(script)) continue;
|
|
1019
|
+
const invoked = this.#invocations(script);
|
|
1020
|
+
if (invoked === void 0) {
|
|
1021
|
+
unresolved = true;
|
|
1022
|
+
continue;
|
|
1023
|
+
}
|
|
1024
|
+
for (const project of invoked.projects) reachable.add(project);
|
|
1025
|
+
for (const called of invoked.scripts) if (!visited.has(called)) pending.push(called);
|
|
1026
|
+
}
|
|
1027
|
+
if (unresolved) return {
|
|
1028
|
+
field: "projects",
|
|
1029
|
+
message: `The manifest at ${target} contains a ${gateNames} chain that cannot be resolved statically. ${writing ? "Replace it with literal npm run script names before using a scaffold writing verb." : "Replace it with literal npm run script names before relying on the planned configuration."}`,
|
|
1030
|
+
blocking: false
|
|
1031
|
+
};
|
|
1032
|
+
const missing = [...expectedLines].filter(([project]) => !reachable.has(project)).sort(([left], [right]) => left.localeCompare(right));
|
|
1033
|
+
if (missing.length === 0) return void 0;
|
|
1034
|
+
const names = missing.map(([project]) => project);
|
|
1035
|
+
const unscripted = missing.filter(([project]) => !isString(scripts[`test:${project}`]));
|
|
1036
|
+
const ungated = missing.filter(([project]) => isString(scripts[`test:${project}`]));
|
|
1037
|
+
const remedies = [];
|
|
1038
|
+
if (ungated.length > 0) {
|
|
1039
|
+
const declared = ungated.map(([project]) => `test:${project}`);
|
|
1040
|
+
remedies.push(`${declared.join(", ")} ${declared.length === 1 ? "is" : "are"} already declared, so the gate is missing rather than the script: invoke ${declared.length === 1 ? "it" : "each of them"} by name from the ${gateNames} chain.`);
|
|
1041
|
+
}
|
|
1042
|
+
if (unscripted.length > 0) {
|
|
1043
|
+
const lines = unscripted.map(([, line]) => line);
|
|
1044
|
+
remedies.push(`${writing ? "To continue, add" : "Add"} ${lines.length === 1 ? "this exact script line" : "these exact script lines"} to package.json: ${lines.join(" ")}`);
|
|
1045
|
+
}
|
|
1046
|
+
return {
|
|
1047
|
+
field: "projects",
|
|
1048
|
+
message: `The manifest at ${target} does not reach ${names.length === 1 ? "a Vitest project" : "Vitest projects"} the planned configuration registers: ${names.join(", ")}. No chain from ${gateNames} invokes ${names.length === 1 ? "it" : "them"}. ${remedies.join(" ")}`,
|
|
1049
|
+
blocking: false
|
|
1050
|
+
};
|
|
975
1051
|
}
|
|
976
|
-
#
|
|
977
|
-
const
|
|
978
|
-
if (
|
|
979
|
-
|
|
1052
|
+
#dependencyQuestion(target, blueprint, writing = false) {
|
|
1053
|
+
const parsed = parseJSON(this.#manifest(target));
|
|
1054
|
+
if (!isRecord(parsed)) return void 0;
|
|
1055
|
+
const malformed = ["dependencies", "devDependencies"].filter((section) => parsed[section] !== void 0 && !isRecord(parsed[section]));
|
|
1056
|
+
if (malformed.length > 0) return {
|
|
1057
|
+
field: "dependencies",
|
|
1058
|
+
message: `The manifest at ${target} declares ${malformed.join(" and ")} as ${malformed.length === 1 ? "a value" : "values"} that ${malformed.length === 1 ? "is" : "are"} not ${malformed.length === 1 ? "an object" : "objects"}. Replace ${malformed.length === 1 ? "it" : "them"} with ${malformed.length === 1 ? "an object" : "objects"} before ${writing ? "using a scaffold writing verb" : "relying on the planned dependency set"}.`,
|
|
1059
|
+
blocking: false
|
|
1060
|
+
};
|
|
1061
|
+
const dependencies = isRecord(parsed.dependencies) ? parsed.dependencies : {};
|
|
1062
|
+
const development = isRecord(parsed.devDependencies) ? parsed.devDependencies : {};
|
|
1063
|
+
const missing = Object.entries(blueprintToDevDependencies(blueprint)).filter(([name]) => !Object.hasOwn(dependencies, name) && !Object.hasOwn(development, name)).sort(([left], [right]) => left.localeCompare(right));
|
|
1064
|
+
if (missing.length === 0) return void 0;
|
|
1065
|
+
const names = missing.map(([name]) => name);
|
|
1066
|
+
const lines = missing.map(([name, range]) => `${JSON.stringify(name)}: ${JSON.stringify(range)},`);
|
|
1067
|
+
return {
|
|
1068
|
+
field: "dependencies",
|
|
1069
|
+
message: `The manifest at ${target} does not declare ${names.length === 1 ? "a planned dependency" : "planned dependencies"}: ${names.join(", ")}. ${writing ? "To continue, add" : "Add"} ${lines.length === 1 ? "this exact dependency line" : "these exact dependency lines"} to dependencies or devDependencies in package.json: ${lines.join(" ")}`,
|
|
1070
|
+
blocking: false
|
|
1071
|
+
};
|
|
1072
|
+
}
|
|
1073
|
+
#targetQuestions(target, blueprint, writing = false) {
|
|
1074
|
+
const questions = [];
|
|
1075
|
+
const project = this.#projectQuestion(target, blueprint, writing);
|
|
1076
|
+
if (project !== void 0) questions.push(project);
|
|
1077
|
+
const dependency = this.#dependencyQuestion(target, blueprint, writing);
|
|
1078
|
+
if (dependency !== void 0) questions.push(dependency);
|
|
1079
|
+
return questions;
|
|
1080
|
+
}
|
|
1081
|
+
#assertTarget(target, blueprint) {
|
|
1082
|
+
const questions = this.#targetQuestions(target, blueprint, true);
|
|
1083
|
+
if (questions.length === 0) return;
|
|
1084
|
+
throw new ScaffoldError("TARGET", questions.map((question) => question.message).join(" "), { target });
|
|
980
1085
|
}
|
|
981
1086
|
#manifest(target) {
|
|
982
1087
|
const manifest = readFileText(target, "package.json", MAX_MANIFEST_BYTES);
|
|
@@ -1100,6 +1205,22 @@ var CLI = class CLI {
|
|
|
1100
1205
|
}
|
|
1101
1206
|
this.#say(auditToSummary(audit));
|
|
1102
1207
|
}
|
|
1208
|
+
#reportReplacements(before, after) {
|
|
1209
|
+
const terminal = new Map(after.findings.map((finding) => [finding.path, finding]));
|
|
1210
|
+
for (const finding of before.findings) {
|
|
1211
|
+
if (finding.drift !== "stale") continue;
|
|
1212
|
+
const current = terminal.get(finding.path);
|
|
1213
|
+
if (current?.drift !== "aligned" || current.observed === void 0) continue;
|
|
1214
|
+
const previousLines = Buffer.from(finding.observed, "hex").toString("utf8").split(/\r\n|\r|\n/u).length;
|
|
1215
|
+
const delta = Buffer.from(current.observed, "hex").toString("utf8").split(/\r\n|\r|\n/u).length - previousLines;
|
|
1216
|
+
if (delta === 0) {
|
|
1217
|
+
this.#say(`${finding.path} replaced (0-line delta).`);
|
|
1218
|
+
continue;
|
|
1219
|
+
}
|
|
1220
|
+
const count = Math.abs(delta);
|
|
1221
|
+
this.#say(`${finding.path} replaced (${String(count)} line${count === 1 ? "" : "s"} ${delta > 0 ? "added" : "removed"}).`);
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1103
1224
|
#recount(result) {
|
|
1104
1225
|
this.#say(this.#tally(result));
|
|
1105
1226
|
this.#say(`${String(result.entries.length)} published, ${String(result.mirrors.filter((mirror) => mirror.lookup === "found").length)} guide${result.mirrors.length === 1 ? "" : "s"} fetched, ${String(result.dropped.length)} no longer listed.`);
|