@no-k/hermes 0.1.0 → 0.2.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/README.md +175 -12
- package/catalog.json +2592 -98
- package/dist/bin/hermes.js +87 -0
- package/dist/lib/catalog-query.js +63 -0
- package/dist/lib/catalog-selection.js +199 -0
- package/dist/lib/catalog.js +144 -0
- package/dist/lib/command.js +50 -0
- package/dist/lib/config-syntax.js +210 -0
- package/dist/lib/conflict-selection.js +89 -0
- package/dist/lib/contracts.js +4 -0
- package/dist/lib/files.js +94 -0
- package/dist/lib/install-prompts.js +106 -0
- package/dist/lib/install-view.js +61 -0
- package/dist/lib/install-wizard.js +105 -0
- package/dist/lib/install.js +184 -0
- package/dist/lib/installation-status.js +80 -0
- package/dist/lib/json.js +19 -0
- package/dist/lib/picker-view.js +81 -0
- package/dist/lib/picker.js +135 -0
- package/dist/lib/project-config.js +171 -0
- package/dist/lib/report.js +89 -0
- package/dist/lib/terminal-input.js +61 -0
- package/dist/lib/terminal-style.js +15 -0
- package/dist/lib/terminal-text.js +75 -0
- package/dist/lib/terminal.js +133 -0
- package/dist/lib/versions.js +251 -0
- package/docs/interactive-install.md +405 -0
- package/docs/module-versioning.md +104 -0
- package/package.json +24 -4
- package/bin/hermes.mjs +0 -159
- package/lib/catalog.mjs +0 -51
package/bin/hermes.mjs
DELETED
|
@@ -1,159 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { lstatSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import { fileURLToPath } from "node:url";
|
|
5
|
-
import { parseArgs } from "node:util";
|
|
6
|
-
import { validateCatalog } from "../lib/catalog.mjs";
|
|
7
|
-
|
|
8
|
-
const packageDirectory = fileURLToPath(new URL("../", import.meta.url));
|
|
9
|
-
const manifest = JSON.parse(readFileSync(path.join(packageDirectory, "package.json"), "utf8"));
|
|
10
|
-
const help = [
|
|
11
|
-
"Hermes " + manifest.version + " — copy TypeScript utilities into your project",
|
|
12
|
-
"",
|
|
13
|
-
"Usage:",
|
|
14
|
-
" hermes list",
|
|
15
|
-
" hermes add <item...> --dir <directory> [--dry-run] [--overwrite]",
|
|
16
|
-
"",
|
|
17
|
-
"Options:",
|
|
18
|
-
" -d, --dir Destination for utility folders and LICENSE (required for add)",
|
|
19
|
-
" --dry-run Show the planned files without writing",
|
|
20
|
-
" --overwrite Replace existing files whose contents differ",
|
|
21
|
-
" -h, --help Show help",
|
|
22
|
-
" -v, --version Show version",
|
|
23
|
-
"",
|
|
24
|
-
"Example:",
|
|
25
|
-
" npx @no-k/hermes add array-chunk --dir src/lib/hermes",
|
|
26
|
-
"",
|
|
27
|
-
"External npm dependencies are reported; they are not installed automatically.",
|
|
28
|
-
].join("\n");
|
|
29
|
-
|
|
30
|
-
function stat(file) {
|
|
31
|
-
try {
|
|
32
|
-
return lstatSync(file);
|
|
33
|
-
} catch (error) {
|
|
34
|
-
if (error.code === "ENOENT") return undefined;
|
|
35
|
-
throw error;
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function destinationStat(directory, relative) {
|
|
40
|
-
let current = directory;
|
|
41
|
-
const segments = relative.split("/");
|
|
42
|
-
for (let index = -1; index < segments.length; index += 1) {
|
|
43
|
-
if (index >= 0) current = path.join(current, segments[index]);
|
|
44
|
-
const info = stat(current);
|
|
45
|
-
if (info?.isSymbolicLink()) throw new Error("Refusing to write through a symbolic link: " + current);
|
|
46
|
-
if (index < segments.length - 1 && info && !info.isDirectory()) {
|
|
47
|
-
throw new Error("Expected a directory: " + current);
|
|
48
|
-
}
|
|
49
|
-
if (index === segments.length - 1) {
|
|
50
|
-
if (info && !info.isFile()) throw new Error("Expected a regular file: " + current);
|
|
51
|
-
return info;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function sourceContent(relative) {
|
|
57
|
-
const directory = realpathSync(path.join(packageDirectory, "templates"));
|
|
58
|
-
const file = realpathSync(path.join(directory, relative));
|
|
59
|
-
const fromRoot = path.relative(directory, file);
|
|
60
|
-
if (path.isAbsolute(fromRoot) || fromRoot === ".." || fromRoot.startsWith(".." + path.sep)) {
|
|
61
|
-
throw new Error("Source template escapes its directory: " + relative);
|
|
62
|
-
}
|
|
63
|
-
return readFileSync(file, "utf8");
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function add(items, options) {
|
|
67
|
-
if (!options.dir?.trim()) throw new Error("Specify a destination with --dir <directory>.");
|
|
68
|
-
const directory = path.resolve(options.dir);
|
|
69
|
-
const files = new Map();
|
|
70
|
-
for (const item of items) {
|
|
71
|
-
for (const file of item.files) {
|
|
72
|
-
if (!files.has(file)) files.set(file, sourceContent(file));
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
files.set("LICENSE", readFileSync(path.join(packageDirectory, "LICENSE"), "utf8"));
|
|
76
|
-
const plan = [...files]
|
|
77
|
-
.sort(([a], [b]) => a.localeCompare(b))
|
|
78
|
-
.map(([file, content]) => {
|
|
79
|
-
const destination = path.join(directory, file);
|
|
80
|
-
const existing = destinationStat(directory, file);
|
|
81
|
-
const same = existing && readFileSync(destination, "utf8") === content;
|
|
82
|
-
return { file, content, destination, action: same ? "keep" : existing ? "replace" : "create" };
|
|
83
|
-
});
|
|
84
|
-
const conflicts = plan.filter((file) => file.action === "replace");
|
|
85
|
-
if (conflicts.length && !options.overwrite) {
|
|
86
|
-
throw new Error(
|
|
87
|
-
"Existing files differ; no files were written:\n" +
|
|
88
|
-
conflicts.map((file) => " " + file.destination).join("\n") +
|
|
89
|
-
"\nReview the changes, then use --overwrite if you want to replace them.",
|
|
90
|
-
);
|
|
91
|
-
}
|
|
92
|
-
for (const file of plan) {
|
|
93
|
-
if (!options["dry-run"] && file.action !== "keep") {
|
|
94
|
-
mkdirSync(path.dirname(file.destination), { recursive: true });
|
|
95
|
-
writeFileSync(file.destination, file.content, { flag: file.action === "create" ? "wx" : "w" });
|
|
96
|
-
}
|
|
97
|
-
console.log((options["dry-run"] ? "[dry-run] " : "") + file.action + " " + file.destination);
|
|
98
|
-
}
|
|
99
|
-
const dependencies = [...new Set(items.flatMap((item) => item.dependencies))].sort();
|
|
100
|
-
const devDependencies = [...new Set(items.flatMap((item) => item.devDependencies ?? []))].sort();
|
|
101
|
-
if (dependencies.length || devDependencies.length) {
|
|
102
|
-
console.log("\nThese utilities need npm packages. Install them in your consuming project:");
|
|
103
|
-
if (dependencies.length)
|
|
104
|
-
console.log(" npm install " + dependencies.map((value) => JSON.stringify(value)).join(" "));
|
|
105
|
-
if (devDependencies.length) {
|
|
106
|
-
console.log(" npm install --save-dev " + devDependencies.map((value) => JSON.stringify(value)).join(" "));
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
for (const item of items) {
|
|
110
|
-
if (item.runtime === "node") console.log("\n" + item.name + ": requires Node.js (imports a Node.js builtin).");
|
|
111
|
-
if (item.runtime === "dom") console.log("\n" + item.name + ": requires DOM APIs and DOM TypeScript types.");
|
|
112
|
-
}
|
|
113
|
-
console.log("\n" + (options["dry-run"] ? "Planned" : "Added") + " " + items.map((item) => item.name).join(", "));
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function main() {
|
|
117
|
-
const { values, positionals } = parseArgs({
|
|
118
|
-
options: {
|
|
119
|
-
dir: { type: "string", short: "d" },
|
|
120
|
-
"dry-run": { type: "boolean" },
|
|
121
|
-
overwrite: { type: "boolean" },
|
|
122
|
-
help: { type: "boolean", short: "h" },
|
|
123
|
-
version: { type: "boolean", short: "v" },
|
|
124
|
-
},
|
|
125
|
-
allowPositionals: true,
|
|
126
|
-
});
|
|
127
|
-
if (values.help || (!positionals.length && !values.version)) {
|
|
128
|
-
console.log(help);
|
|
129
|
-
return;
|
|
130
|
-
}
|
|
131
|
-
if (values.version) {
|
|
132
|
-
console.log(manifest.version);
|
|
133
|
-
return;
|
|
134
|
-
}
|
|
135
|
-
const [command, ...names] = positionals;
|
|
136
|
-
if (!["list", "add"].includes(command)) throw new Error("Unknown command: " + command + ". Use --help.");
|
|
137
|
-
const catalog = validateCatalog(JSON.parse(readFileSync(path.join(packageDirectory, "catalog.json"), "utf8")));
|
|
138
|
-
if (catalog.version !== manifest.version)
|
|
139
|
-
throw new Error("The catalog version does not match the package. Rebuild the CLI.");
|
|
140
|
-
if (command === "list") {
|
|
141
|
-
if (names.length || values.dir || values.overwrite || values["dry-run"]) throw new Error("Usage: hermes list");
|
|
142
|
-
for (const item of catalog.items) console.log(item.name + "\t" + item.description);
|
|
143
|
-
return;
|
|
144
|
-
}
|
|
145
|
-
if (!names.length) throw new Error("Specify at least one item. Use 'hermes list' to see available items.");
|
|
146
|
-
const items = [...new Set(names)].map((name) => {
|
|
147
|
-
const item = catalog.items.find((candidate) => candidate.name === name);
|
|
148
|
-
if (!item) throw new Error("Unknown item: " + name + ". Use 'hermes list' to see available items.");
|
|
149
|
-
return item;
|
|
150
|
-
});
|
|
151
|
-
add(items, values);
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
try {
|
|
155
|
-
main();
|
|
156
|
-
} catch (error) {
|
|
157
|
-
console.error("Hermes: " + error.message);
|
|
158
|
-
process.exitCode = 1;
|
|
159
|
-
}
|
package/lib/catalog.mjs
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
const itemNamePattern = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
2
|
-
|
|
3
|
-
export function isSourcePath(value) {
|
|
4
|
-
return (
|
|
5
|
-
typeof value === "string" &&
|
|
6
|
-
value.endsWith(".ts") &&
|
|
7
|
-
!value.includes("\\") &&
|
|
8
|
-
!value.includes("\0") &&
|
|
9
|
-
!value.includes(":") &&
|
|
10
|
-
value.split("/").every((part) => part && part !== "." && part !== "..") &&
|
|
11
|
-
!/\.(test|spec|stories|config|d)\.ts$/.test(value)
|
|
12
|
-
);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export function validateCatalog(catalog) {
|
|
16
|
-
if (
|
|
17
|
-
catalog?.schemaVersion !== 1 ||
|
|
18
|
-
typeof catalog.version !== "string" ||
|
|
19
|
-
!Array.isArray(catalog.items) ||
|
|
20
|
-
catalog.items.length === 0
|
|
21
|
-
) {
|
|
22
|
-
throw new Error("Invalid Hermes catalog.");
|
|
23
|
-
}
|
|
24
|
-
const names = new Set();
|
|
25
|
-
for (const item of catalog.items) {
|
|
26
|
-
if (
|
|
27
|
-
!item ||
|
|
28
|
-
typeof item.name !== "string" ||
|
|
29
|
-
!itemNamePattern.test(item.name) ||
|
|
30
|
-
names.has(item.name) ||
|
|
31
|
-
typeof item.description !== "string" ||
|
|
32
|
-
!Array.isArray(item.files) ||
|
|
33
|
-
item.files.length === 0 ||
|
|
34
|
-
!item.files.every(isSourcePath) ||
|
|
35
|
-
new Set(item.files).size !== item.files.length ||
|
|
36
|
-
(item.runtime !== undefined && !["node", "dom"].includes(item.runtime))
|
|
37
|
-
) {
|
|
38
|
-
throw new Error("Invalid or duplicate Hermes item: " + item?.name);
|
|
39
|
-
}
|
|
40
|
-
for (const values of [item.dependencies, item.devDependencies ?? []]) {
|
|
41
|
-
if (
|
|
42
|
-
!Array.isArray(values) ||
|
|
43
|
-
!values.every((value) => typeof value === "string" && /^[^\s]+@[^\s]+$/.test(value))
|
|
44
|
-
) {
|
|
45
|
-
throw new Error("Invalid dependencies for " + item.name);
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
names.add(item.name);
|
|
49
|
-
}
|
|
50
|
-
return catalog;
|
|
51
|
-
}
|