isotone 0.4.0 → 0.4.2

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/cli.js CHANGED
@@ -13,26 +13,26 @@ import {
13
13
  syncVariant,
14
14
  variantDir,
15
15
  writeActive
16
- } from "./chunk-7WGLOXRQ.js";
16
+ } from "./chunk-DD6BKC3R.js";
17
17
  import {
18
18
  overlayGroups,
19
19
  variantAppliesTo
20
- } from "./chunk-SNGBNVH2.js";
20
+ } from "./chunk-OSETWKGE.js";
21
21
  import {
22
- checkUpdate,
23
- version
24
- } from "./chunk-QT27DAPQ.js";
22
+ checkUpdate
23
+ } from "./chunk-LHCQL5PA.js";
25
24
  import {
26
25
  readNotes,
27
26
  updateNotes
28
- } from "./chunk-DD767R7E.js";
27
+ } from "./chunk-AP7O357L.js";
28
+ import "./chunk-A4DX2V4C.js";
29
29
 
30
30
  // src/cli/index.ts
31
31
  import { realpathSync } from "fs";
32
32
  import { fileURLToPath } from "url";
33
33
 
34
34
  // src/cli/run.ts
35
- import { resolve as resolve3 } from "path";
35
+ import { resolve as resolve2 } from "path";
36
36
 
37
37
  // src/cli/init.ts
38
38
  import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "fs";
@@ -124,7 +124,7 @@ function runInit(args, io) {
124
124
  }
125
125
  const { value: approachArg, rest: r1 } = extractFlag(args, "--approach");
126
126
  const { present: yes } = extractBoolFlag(r1, "--yes");
127
- const json2 = args.includes("--json");
127
+ const json = args.includes("--json");
128
128
  if (help) {
129
129
  out(io, "Usage: isotone init look at this repo and recommend an approach");
130
130
  out(io, " isotone init --approach <name> print the setup for overlay, host or serve");
@@ -142,7 +142,7 @@ function runInit(args, io) {
142
142
  errOut(io, `Unknown approach "${approach}". Use host, overlay or serve.`);
143
143
  return 1;
144
144
  }
145
- if (json2) {
145
+ if (json) {
146
146
  const instructions = [];
147
147
  printApproach({ ...io, stdout: { write: (line) => instructions.push(line) } }, approach, host);
148
148
  out(io, JSON.stringify({
@@ -757,308 +757,10 @@ async function runCheckUpdate(args, io) {
757
757
  return result.status === "unavailable" ? 1 : 0;
758
758
  }
759
759
 
760
- // src/cli/migrate.ts
761
- import { execFileSync } from "child_process";
762
- import { createHash, randomUUID } from "crypto";
763
- import { existsSync as existsSync3, lstatSync, readFileSync as readFileSync2, readdirSync as readdirSync2, renameSync, unlinkSync, writeFileSync as writeFileSync2 } from "fs";
764
- import { basename, dirname, extname, join as join3, relative as relative2, resolve as resolve2 } from "path";
765
- var OLD = "iso-iterate";
766
- var NEW = "isotone";
767
- var TEXT = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts", ".json", ".jsonc", ".md", ".mdx", ".html", ".css", ".scss", ".sass", ".less", ".vue", ".svelte", ".yaml", ".yml", ".toml", ".sh"]);
768
- var SKIP = /* @__PURE__ */ new Set([".git", "node_modules", "dist", "build", ".next", ".nuxt", ".output", "coverage", ".turbo"]);
769
- var LOCKS = /* @__PURE__ */ new Set(["package-lock.json", "npm-shrinkwrap.json", "pnpm-lock.yaml", "yarn.lock", "bun.lock", "bun.lockb"]);
770
- function git(cwd, args, input) {
771
- return execFileSync("git", args, { cwd, input, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"], maxBuffer: 32 * 1024 * 1024 }).trimEnd();
772
- }
773
- function renamed(text) {
774
- const keys = [];
775
- const protect = (match, prefix, quote, value) => {
776
- if (value === "iso-iterate-feedback" || value === "iso-iterate:knobs") return match;
777
- const index = keys.push(value) - 1;
778
- return `${prefix}${quote}\0KEY${index}\0${quote}`;
779
- };
780
- const protectedText = text.replace(/(\b(?:key|storageKey)\s*:\s*|["'](?:key|storageKey)["']\s*:\s*)(["'`])([\s\S]*?)\2/g, protect).replace(/(\b(?:localStorage|sessionStorage)\.(?:getItem|setItem|removeItem)\(\s*)(["'`])([\s\S]*?)\2/g, protect);
781
- let result = protectedText.replace(/\biso-iterate@\d+\.\d+\.\d+(?:-[\da-zA-Z.-]+)?(?:\+[\da-zA-Z.-]+)?/g, `${NEW}@${version}`).replaceAll(OLD, NEW).replaceAll("iso-iter-", "isotone-").replaceAll("isoIterate", "isotone").replaceAll("IsoIterate", "Isotone").replaceAll("ISO_ITERATE", "ISOTONE").replaceAll("ISO_VARIANT", "ISOTONE_VARIANT");
782
- keys.forEach((value, index) => {
783
- result = result.replaceAll(`\0KEY${index}\0`, value);
784
- });
785
- return result;
786
- }
787
- function present(path) {
788
- try {
789
- lstatSync(path);
790
- return true;
791
- } catch {
792
- return false;
793
- }
794
- }
795
- function textContents(buffer) {
796
- if (buffer.includes(0)) return null;
797
- const text = buffer.toString("utf8");
798
- return Buffer.from(text).compare(buffer) === 0 && !feedback(text) ? text : null;
799
- }
800
- function safeFile(root, path) {
801
- const parts = relative2(root, path).split(/[\\/]/);
802
- if (parts.includes("..") || parts.some((part) => SKIP.has(part))) return false;
803
- let current = root;
804
- for (const part of parts) {
805
- current = join3(current, part);
806
- try {
807
- if (lstatSync(current).isSymbolicLink()) return false;
808
- } catch {
809
- return false;
810
- }
811
- }
812
- return lstatSync(path).isFile();
813
- }
814
- function walk(directory, visit, legacy, skipped) {
815
- for (const entry of readdirSync2(directory, { withFileTypes: true })) {
816
- const path = join3(directory, entry.name);
817
- if (entry.isSymbolicLink()) {
818
- if (entry.name === `.${OLD}`) legacy(path);
819
- continue;
820
- }
821
- if (entry.isDirectory()) {
822
- if (SKIP.has(entry.name)) continue;
823
- if (present(join3(path, ".git"))) {
824
- skipped?.(path);
825
- continue;
826
- }
827
- if (entry.name === `.${OLD}`) legacy(path);
828
- else if (entry.name !== ".isotone") walk(path, visit, legacy, skipped);
829
- } else if (entry.isFile()) visit(path);
830
- }
831
- }
832
- function json(text) {
833
- try {
834
- const data = JSON.parse(text);
835
- return data && typeof data === "object" && !Array.isArray(data) ? data : null;
836
- } catch {
837
- return null;
838
- }
839
- }
840
- function feedback(text) {
841
- try {
842
- const data = JSON.parse(text);
843
- return Array.isArray(data) && data.some((note) => note && typeof note === "object" && typeof note.feedback === "string" && typeof note.id === "string");
844
- } catch {
845
- return false;
846
- }
847
- }
848
- function packageText(text, path, conflicts) {
849
- const pkg = json(text);
850
- if (!pkg) {
851
- conflicts.push(`Invalid package.json: ${path}`);
852
- return text;
853
- }
854
- for (const key of ["overrides", "resolutions", "pnpm"]) {
855
- if (JSON.stringify(pkg[key])?.includes(OLD)) conflicts.push(`Legacy package in ${path} (${key}). Review custom overrides/resolutions before migrating.`);
856
- }
857
- let changed = false;
858
- const sections = ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"];
859
- if (sections.some((key) => Object.hasOwn(Object(pkg[key]), OLD)) && sections.some((key) => Object.hasOwn(Object(pkg[key]), NEW))) {
860
- conflicts.push(`Both package names exist in ${path}. Resolve their dependency declarations before migrating.`);
861
- return text;
862
- }
863
- for (const key of ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"]) {
864
- const deps = pkg[key];
865
- if (!deps || typeof deps !== "object" || Array.isArray(deps) || !Object.hasOwn(deps, OLD)) continue;
866
- const values = deps;
867
- values[NEW] = version;
868
- delete values[OLD];
869
- changed = true;
870
- }
871
- if (!changed) return renamed(text);
872
- const indent = /\n([\t ]+)"/.exec(text)?.[1] ?? " ";
873
- return renamed(JSON.stringify(pkg, null, indent)) + (text.endsWith("\n") ? "\n" : "");
874
- }
875
- function installStep(root, override) {
876
- let directory = root;
877
- let manager = override;
878
- while (true) {
879
- const pkg = existsSync3(join3(directory, "package.json")) ? json(readFileSync2(join3(directory, "package.json"), "utf8")) : null;
880
- const declared = typeof pkg?.packageManager === "string" ? /^(npm|pnpm|yarn|bun)@/.exec(pkg.packageManager)?.[1] : void 0;
881
- const found = /* @__PURE__ */ new Set();
882
- if (existsSync3(join3(directory, "package-lock.json")) || existsSync3(join3(directory, "npm-shrinkwrap.json"))) found.add("npm");
883
- if (existsSync3(join3(directory, "pnpm-lock.yaml"))) found.add("pnpm");
884
- if (existsSync3(join3(directory, "yarn.lock"))) found.add("yarn");
885
- if (existsSync3(join3(directory, "bun.lock")) || existsSync3(join3(directory, "bun.lockb"))) found.add("bun");
886
- if (declared || found.size || dirname(directory) === directory || existsSync3(join3(directory, ".git"))) {
887
- if (!manager && typeof pkg?.packageManager === "string" && !declared) return { install: null, warning: "Unsupported packageManager; run its install command to regenerate the lockfile." };
888
- if (!manager && !declared && found.size > 1) return { install: null, warning: "Multiple lockfiles: pass --manager npm, pnpm, yarn or bun for an install command." };
889
- manager ??= declared ?? [...found][0] ?? "npm";
890
- const command = `${manager} install`;
891
- return { install: { cwd: pkg || found.size ? directory : root, command } };
892
- }
893
- directory = dirname(directory);
894
- }
895
- }
896
- function planMigration(cwd, manager) {
897
- let root = resolve2(cwd);
898
- let tracked = null;
899
- try {
900
- root = git(root, ["rev-parse", "--show-toplevel"]);
901
- tracked = git(root, ["ls-files", "-z"]).split("\0").filter(Boolean);
902
- } catch {
903
- }
904
- const plan = { root, edits: [], moves: [], blobs: /* @__PURE__ */ new Map(), conflicts: [], install: null, installs: [], warnings: [] };
905
- const candidates = /* @__PURE__ */ new Set();
906
- const legacyRoots = /* @__PURE__ */ new Set();
907
- const addLegacy = (path) => {
908
- if (legacyRoots.has(path)) return;
909
- legacyRoots.add(path);
910
- if (lstatSync(path).isSymbolicLink()) {
911
- plan.conflicts.push(`Legacy directory is a symlink: ${relative2(root, path)}. Migrate its target separately.`);
912
- return;
913
- }
914
- const destination = join3(dirname(path), ".isotone");
915
- if (present(destination)) plan.conflicts.push(`Both ${relative2(root, path)} and ${relative2(root, destination)} exist; nothing will be overwritten.`);
916
- plan.moves.push({ from: path, to: destination });
917
- walk(path, (file) => candidates.add(file), (nested) => plan.conflicts.push(`Nested legacy directory: ${relative2(root, nested)}`));
918
- };
919
- if (tracked) for (const file of tracked) candidates.add(join3(root, file));
920
- walk(root, (path) => {
921
- const name = basename(path);
922
- if (!tracked || dirname(path) === root && (["AGENTS.md", "package.json", ".gitignore", ".npmignore"].includes(name) || /^(?:vite|next|react-router|svelte|astro|nuxt)\.config\./.test(name))) candidates.add(path);
923
- }, addLegacy, (path) => plan.warnings.push(`Separate Git checkout skipped: ${relative2(root, path)}. Migrate it separately.`));
924
- if (tracked) {
925
- const exclude = resolve2(root, git(root, ["rev-parse", "--git-path", "info/exclude"]));
926
- if (existsSync3(exclude) && lstatSync(exclude).isFile() && !lstatSync(exclude).isSymbolicLink()) {
927
- const before = readFileSync2(exclude, "utf8");
928
- const after = renamed(before);
929
- if (before !== after) plan.edits.push({ path: exclude, before, after, mode: lstatSync(exclude).mode, gitExclude: true });
930
- }
931
- }
932
- for (const file of candidates) {
933
- const name = basename(file);
934
- if (LOCKS.has(name) || name === ".iteration-feedback.json" || !safeFile(root, file)) continue;
935
- if (relative2(root, file).split(/[\\/]/).some((part) => part !== `.${OLD}` && renamed(part) !== part)) {
936
- plan.conflicts.push(`Legacy name in a file path: ${relative2(root, file)}. Rename that file or directory and its local references before migrating.`);
937
- continue;
938
- }
939
- if (!TEXT.has(extname(file)) && ![".gitignore", ".npmignore"].includes(name)) continue;
940
- const before = textContents(readFileSync2(file));
941
- if (before === null) continue;
942
- for (const match of before.matchAll(/\biso-iterate@([^\s"'`;,)}\]]+)/g)) {
943
- if (!/^(?:latest|\d+\.\d+\.\d+(?:-[\da-zA-Z.-]+)?(?:\+[\da-zA-Z.-]+)?)$/.test(match[1])) {
944
- plan.conflicts.push(`Custom package version constraint in ${relative2(root, file)}. Replace it with the desired Isotone version before migrating.`);
945
- }
946
- }
947
- const after = name === "package.json" ? packageText(before, relative2(root, file), plan.conflicts) : renamed(before);
948
- if (after !== before) plan.edits.push({ path: file, before, after, mode: lstatSync(file).mode });
949
- }
950
- if (tracked) for (const file of candidates) {
951
- if (basename(file) !== "variant.json" || !safeFile(root, file) || ![...legacyRoots].some((path) => file.startsWith(`${path}/`))) continue;
952
- const before = readFileSync2(file, "utf8");
953
- const existing = plan.edits.find((edit) => edit.path === file);
954
- const data = json(existing?.after ?? before);
955
- if (!data?.files || typeof data.files !== "object") continue;
956
- let changed = false;
957
- for (const [key, entry] of Object.entries(data.files)) {
958
- if (!TEXT.has(extname(key)) && ![".gitignore", ".npmignore"].includes(basename(key))) continue;
959
- if (!entry || typeof entry !== "object" || typeof entry.base !== "string" || !/^[a-f0-9]{40}(?:[a-f0-9]{24})?$/.test(entry.base)) continue;
960
- let base;
961
- try {
962
- base = textContents(execFileSync("git", ["cat-file", "blob", entry.base], { cwd: root, stdio: ["pipe", "pipe", "pipe"], maxBuffer: 32 * 1024 * 1024 }));
963
- } catch {
964
- continue;
965
- }
966
- if (base === null || basename(key) === ".iteration-feedback.json") continue;
967
- const after = basename(key) === "package.json" ? packageText(base, key, []) : renamed(base);
968
- if (after === base) continue;
969
- const hash = createHash(entry.base.length === 64 ? "sha256" : "sha1").update(`blob ${Buffer.byteLength(after)}\0`).update(after).digest("hex");
970
- entry.base = hash;
971
- plan.blobs.set(hash, after);
972
- changed = true;
973
- }
974
- if (changed) {
975
- const after = `${JSON.stringify(data, null, 2)}
976
- `;
977
- if (existing) existing.after = after;
978
- else plan.edits.push({ path: file, before, after, mode: lstatSync(file).mode });
979
- }
980
- }
981
- const packages = plan.edits.filter((edit) => basename(edit.path) === "package.json" && edit.before.includes(`"${OLD}"`));
982
- for (const edit of packages) {
983
- const step = installStep(dirname(edit.path), manager);
984
- if (step.install && !plan.installs.some((existing) => existing.cwd === step.install?.cwd && existing.command === step.install.command)) plan.installs.push(step.install);
985
- if (step.warning) plan.warnings.push(`${relative2(root, edit.path)}: ${step.warning}`);
986
- }
987
- plan.install = plan.installs.length === 1 ? plan.installs[0] : null;
988
- if (!tracked) plan.warnings.push("No Git repository: review the changed text files before installing.");
989
- return plan;
990
- }
991
- function atomicWrite(path, text, mode) {
992
- const temporary = `${path}.${randomUUID()}.migrate`;
993
- try {
994
- writeFileSync2(temporary, text, { flag: "wx", mode });
995
- renameSync(temporary, path);
996
- } finally {
997
- if (existsSync3(temporary)) unlinkSync(temporary);
998
- }
999
- }
1000
- function apply(plan) {
1001
- for (const edit of plan.edits) if (!(edit.gitExclude ? lstatSync(edit.path).isFile() && !lstatSync(edit.path).isSymbolicLink() : safeFile(plan.root, edit.path)) || readFileSync2(edit.path, "utf8") !== edit.before) throw new Error(`File changed during migration: ${relative2(plan.root, edit.path)}. Run the preview again.`);
1002
- for (const move of plan.moves) if (present(move.to) || lstatSync(move.from).isSymbolicLink()) throw new Error(`Directory changed during migration: ${relative2(plan.root, move.from)}.`);
1003
- const written = [], moved = [];
1004
- try {
1005
- for (const [hash, contents] of plan.blobs) if (git(plan.root, ["hash-object", "--no-filters", "-w", "--stdin"], contents) !== hash) throw new Error("Could not preserve a variant merge base.");
1006
- for (const edit of plan.edits) {
1007
- atomicWrite(edit.path, edit.after, edit.mode);
1008
- written.push(edit);
1009
- }
1010
- for (const move of plan.moves) {
1011
- renameSync(move.from, move.to);
1012
- moved.push(move);
1013
- }
1014
- } catch (error) {
1015
- for (const move of moved.reverse()) renameSync(move.to, move.from);
1016
- for (const edit of written.reverse()) atomicWrite(edit.path, edit.before, edit.mode);
1017
- throw error;
1018
- }
1019
- }
1020
- function runMigrate(args, io) {
1021
- if (args.some((arg) => arg === "--help" || arg === "-h")) {
1022
- out(io, "Usage: isotone migrate [--write] [--json] [--manager npm|pnpm|yarn|bun]");
1023
- out(io, "Preview migration from iso-iterate. --write applies it; installing remains an explicit next step.");
1024
- return 0;
1025
- }
1026
- if (parseArgs(args, ["--manager"], ["--write", "--json"]).length) throw new Error("migrate does not take positional arguments.");
1027
- const manager = valueFor(args, "--manager");
1028
- if (manager && !["npm", "pnpm", "yarn", "bun"].includes(manager)) throw new Error("--manager must be npm, pnpm, yarn or bun.");
1029
- const plan = planMigration(io.cwd, manager);
1030
- const write = args.includes("--write");
1031
- const changed = plan.edits.length > 0 || plan.moves.length > 0;
1032
- if (write && !plan.conflicts.length) apply(plan);
1033
- const summary = {
1034
- root: plan.root,
1035
- version,
1036
- applied: write && !plan.conflicts.length && changed,
1037
- edits: plan.edits.map((edit) => relative2(plan.root, edit.path)),
1038
- moves: plan.moves.map((move) => ({ from: relative2(plan.root, move.from), to: relative2(plan.root, move.to) })),
1039
- conflicts: plan.conflicts,
1040
- warnings: plan.warnings,
1041
- install: changed ? plan.install : null,
1042
- installs: changed ? plan.installs : []
1043
- };
1044
- if (args.includes("--json")) out(io, JSON.stringify(summary, null, 2));
1045
- else {
1046
- out(io, `${plan.conflicts.length ? "Blocked" : write ? "Applied" : "Preview"}: ${plan.root}`);
1047
- for (const file of summary.edits) out(io, ` edit ${file}`);
1048
- for (const move of summary.moves) out(io, ` move ${move.from} \u2192 ${move.to}`);
1049
- for (const message of [...plan.conflicts, ...plan.warnings]) out(io, message);
1050
- if (!changed && !plan.conflicts.length) out(io, "No migration changes needed.");
1051
- else if (!write && !plan.conflicts.length) out(io, "Review this plan, then run isotone migrate --write.");
1052
- if (!plan.conflicts.length) for (const step of summary.installs) out(io, `After applying, run ${step.command} from ${step.cwd}, then restart the dev server. Lockfiles and feedback are unchanged by migration.`);
1053
- }
1054
- return plan.conflicts.length ? 1 : 0;
1055
- }
1056
-
1057
760
  // src/cli/run.ts
1058
761
  async function runCli(argv, ioInput = {}) {
1059
762
  const io = makeIo(ioInput);
1060
763
  try {
1061
- if (argv[0] === "migrate") return runMigrate(argv.slice(1), io);
1062
764
  if (argv[0] === "check-update") return await runCheckUpdate(argv.slice(1), io);
1063
765
  if (argv[0] === "serve") return await runServe(argv.slice(1), io);
1064
766
  if (argv[0] === "variant") return await runVariant(argv.slice(1), io);
@@ -1100,7 +802,6 @@ function printHelp(io) {
1100
802
  out(io);
1101
803
  out(io, "Setting a repo up");
1102
804
  out(io, " isotone init [--approach overlay|host|serve] [--yes] [--json]");
1103
- out(io, " isotone migrate [--write] [--json] [--manager npm|pnpm|yarn|bun]");
1104
805
  }
1105
806
  async function runServe(args, io) {
1106
807
  const { serveIteration, DEFAULT_PORT } = await import("./serve.js");
@@ -1116,7 +817,7 @@ async function runServe(args, io) {
1116
817
  errOut(io, error.message);
1117
818
  return 1;
1118
819
  }
1119
- const file = resolve3(io.cwd, valueFor(args, "--file") ?? ".iteration-feedback.json");
820
+ const file = resolve2(io.cwd, valueFor(args, "--file") ?? ".iteration-feedback.json");
1120
821
  const port = Number(valueFor(args, "--port") ?? DEFAULT_PORT);
1121
822
  const host = valueFor(args, "--host");
1122
823
  const theme = valueFor(args, "--theme") ?? "auto";
@@ -1170,7 +871,7 @@ function runNotes(argv, io) {
1170
871
  errOut(io, "--days must be a positive number.");
1171
872
  return 1;
1172
873
  }
1173
- const file = resolve3(io.cwd, valueFor(argv, "--file") ?? ".iteration-feedback.json");
874
+ const file = resolve2(io.cwd, valueFor(argv, "--file") ?? ".iteration-feedback.json");
1174
875
  const all = has(argv, "--all");
1175
876
  const doneIdx = argv.indexOf("--done");
1176
877
  const undoneIdx = argv.indexOf("--undone");