@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 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 question = this.#projectQuestion(target, blueprint);
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 = question === void 0 ? measured : {
627
+ const audit = questions.length === 0 ? measured : {
628
628
  ...measured,
629
- questions: [...measured.questions, question]
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.#assertProjects(target, blueprint);
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.#assertProjects(target, blueprint);
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
- #scriptProjects(script) {
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 projects;
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 : void 0;
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
- if (scripts !== void 0) for (const script of Object.values(scripts)) {
977
+ for (const script of Object.values(scripts)) {
952
978
  if (!isString(script) || !script.includes("vitest")) continue;
953
- const projects = this.#scriptProjects(script);
954
- if (projects === void 0) {
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
- #assertProjects(target, blueprint) {
977
- const question = this.#projectQuestion(target, blueprint, true);
978
- if (question === void 0) return;
979
- throw new ScaffoldError("TARGET", question.message, { target });
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.`);