@simpill/utils 1.0.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.
Files changed (46) hide show
  1. package/CONTRIBUTING.md +787 -0
  2. package/README.md +186 -0
  3. package/__tests__/README.md +32 -0
  4. package/__tests__/e2e/all-packages-resolve.e2e.test.ts +40 -0
  5. package/__tests__/integration/env-and-async.integration.test.ts +12 -0
  6. package/__tests__/integration/errors-and-uuid.integration.test.ts +14 -0
  7. package/__tests__/integration/object-and-array.integration.test.ts +15 -0
  8. package/__tests__/unit/@simpill/_resolver/resolve-packages.unit.test.ts +47 -0
  9. package/__tests__/unit/@simpill/array.utils/array.utils.unit.test.ts +11 -0
  10. package/__tests__/unit/@simpill/async.utils/async.utils.unit.test.ts +12 -0
  11. package/__tests__/unit/@simpill/cache.utils/cache.utils.unit.test.ts +21 -0
  12. package/__tests__/unit/@simpill/env.utils/env.utils.unit.test.ts +13 -0
  13. package/__tests__/unit/@simpill/errors.utils/errors.utils.unit.test.ts +13 -0
  14. package/__tests__/unit/@simpill/object.utils/object.utils.unit.test.ts +11 -0
  15. package/__tests__/unit/@simpill/patterns.utils/patterns.utils.unit.test.ts +23 -0
  16. package/__tests__/unit/@simpill/string.utils/string.utils.unit.test.ts +11 -0
  17. package/__tests__/unit/@simpill/time.utils/time.utils.unit.test.ts +12 -0
  18. package/__tests__/unit/@simpill/uuid.utils/uuid.utils.unit.test.ts +12 -0
  19. package/docs/PUBLISHING_AND_PACKAGES.md +258 -0
  20. package/docs/template/.env.sample +0 -0
  21. package/docs/template/README.md +0 -0
  22. package/docs/template/TEMPLATE.md +1040 -0
  23. package/docs/template/assets/logo-banner.svg +20 -0
  24. package/docs/template/package.json +14 -0
  25. package/index.ts +89 -0
  26. package/package.json +87 -0
  27. package/scripts/README.md +57 -0
  28. package/scripts/github/github-set-all-topics.js +120 -0
  29. package/scripts/github/github-set-repo-topics.sh +33 -0
  30. package/scripts/github/github-set-repos-public.sh +71 -0
  31. package/scripts/lib/package-topics.js +57 -0
  32. package/scripts/lib/publish-order.js +140 -0
  33. package/scripts/lib/sync-repo-links.js +75 -0
  34. package/scripts/monorepo/install-hooks.sh +64 -0
  35. package/scripts/monorepo/monorepo-clean.sh +7 -0
  36. package/scripts/monorepo/monorepo-sync-deps.js +81 -0
  37. package/scripts/monorepo/monorepo-verify-deps.js +37 -0
  38. package/scripts/monorepo/use-local-utils-at-root.js +49 -0
  39. package/scripts/publish/publish-all.sh +152 -0
  40. package/scripts/utils/utils-fix-repo-metadata.js +61 -0
  41. package/scripts/utils/utils-prepare-all.sh +107 -0
  42. package/scripts/utils/utils-set-npm-keywords.js +132 -0
  43. package/scripts/utils/utils-update-readme-badges.js +83 -0
  44. package/scripts/utils/utils-use-local-deps.js +43 -0
  45. package/scripts/utils/utils-verify-all.sh +45 -0
  46. package/tsconfig.json +14 -0
@@ -0,0 +1,120 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Set GitHub repository topics for this monorepo and all @simpill package repos
4
+ * listed in package.json dependencies. Uses gh API (requires gh CLI authenticated).
5
+ * Run from repo root.
6
+ */
7
+
8
+ const { execSync } = require("child_process");
9
+ const path = require("path");
10
+ const fs = require("fs");
11
+
12
+ const REPO_ROOT = path.resolve(__dirname, "..", "..");
13
+ const PACKAGE_JSON = path.join(REPO_ROOT, "package.json");
14
+ const TOPICS_FILE = path.join(REPO_ROOT, ".github", "TOPICS.md");
15
+ const MAX_TOPICS = 20;
16
+ const ACCEPT_HEADER = "Accept: application/vnd.github.mercy-preview+json";
17
+
18
+ const { BASE_TOPICS, PACKAGE_TOPICS } = require("../lib/package-topics.js");
19
+
20
+ function run(cmd, options = {}) {
21
+ try {
22
+ return execSync(cmd, { encoding: "utf8", ...options });
23
+ } catch (e) {
24
+ if (options.ignoreStderr) return e.stdout || "";
25
+ throw e;
26
+ }
27
+ }
28
+
29
+ function getTopicsFromTopicsFile() {
30
+ const content = fs.readFileSync(TOPICS_FILE, "utf8");
31
+ const names = [];
32
+ const re = /^\s*-\s*`([^`]+)`/gm;
33
+ let m;
34
+ while ((m = re.exec(content)) !== null) names.push(m[1]);
35
+ return names.slice(0, MAX_TOPICS);
36
+ }
37
+
38
+ function setRepoTopics(repo, names) {
39
+ const payload = JSON.stringify({ names });
40
+ run(
41
+ `gh api -X PUT -H "${ACCEPT_HEADER}" "repos/${repo}/topics" --input -`,
42
+ { input: payload, cwd: REPO_ROOT }
43
+ );
44
+ }
45
+
46
+ function getRepoTopics(repo) {
47
+ const out = run(
48
+ `gh api "repos/${repo}/topics" -H "${ACCEPT_HEADER}" -q '.names[]'`,
49
+ { cwd: REPO_ROOT, ignoreStderr: true }
50
+ );
51
+ return out
52
+ .trim()
53
+ ? out.trim().split("\n").filter(Boolean)
54
+ : [];
55
+ }
56
+
57
+ function getPackageRepos() {
58
+ const pkg = JSON.parse(fs.readFileSync(PACKAGE_JSON, "utf8"));
59
+ const repos = [];
60
+ const deps = { ...pkg.dependencies, ...(pkg.devDependencies || {}) };
61
+ for (const spec of Object.values(deps)) {
62
+ const match = spec && spec.match(/github:([^#]+)/);
63
+ if (match) repos.push(match[1]);
64
+ }
65
+ return [...new Set(repos)];
66
+ }
67
+
68
+ function topicsForPackageRepo(repoName) {
69
+ // repoName is like "SkinnnyJay/env.utils" -> package "env.utils"
70
+ const packageName = repoName.split("/").pop();
71
+ const extra = PACKAGE_TOPICS[packageName] || [packageName.replace(".utils", "")];
72
+ const combined = [...BASE_TOPICS, ...extra];
73
+ return [...new Set(combined)].slice(0, MAX_TOPICS);
74
+ }
75
+
76
+ function main() {
77
+ const dryRun = process.argv.includes("--dry-run");
78
+ const onlyPackages = process.argv.includes("--packages-only");
79
+
80
+ console.log("Setting GitHub topics for @simpill repos...\n");
81
+
82
+ // 1) This monorepo (e.g. SkinnnyJay/simpill-utils)
83
+ if (!onlyPackages) {
84
+ const repo = run("gh repo view --json nameWithOwner -q .nameWithOwner", {
85
+ cwd: REPO_ROOT,
86
+ encoding: "utf8",
87
+ }).trim();
88
+ const topics = getTopicsFromTopicsFile();
89
+ console.log(`${repo}: ${topics.length} topics`);
90
+ if (dryRun) {
91
+ console.log(" (dry run)", topics.join(", "));
92
+ } else {
93
+ setRepoTopics(repo, topics);
94
+ console.log(" Set:", getRepoTopics(repo).join(", "));
95
+ }
96
+ console.log("");
97
+ }
98
+
99
+ // 2) Package repos from package.json
100
+ const packageRepos = getPackageRepos();
101
+ console.log(`Found ${packageRepos.length} package repos in package.json\n`);
102
+
103
+ for (const repo of packageRepos) {
104
+ const topics = topicsForPackageRepo(repo);
105
+ process.stdout.write(`${repo}: ${topics.length} topics ... `);
106
+ if (dryRun) {
107
+ console.log("(dry run)", topics.join(", "));
108
+ continue;
109
+ }
110
+ try {
111
+ setRepoTopics(repo, topics);
112
+ const current = getRepoTopics(repo);
113
+ console.log("OK:", current.slice(0, 8).join(", ") + (current.length > 8 ? "..." : ""));
114
+ } catch (e) {
115
+ console.log("FAIL:", e.message.split("\n")[0]);
116
+ }
117
+ }
118
+ }
119
+
120
+ main();
@@ -0,0 +1,33 @@
1
+ #!/usr/bin/env bash
2
+ # Set this repository's GitHub topics from .github/TOPICS.md using the GitHub API.
3
+ # Requires: gh CLI (authenticated), jq. GitHub allows at most 20 topics.
4
+ set -e
5
+
6
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
7
+ REPO_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
8
+ TOPICS_FILE="${REPO_ROOT}/.github/TOPICS.md"
9
+ MAX_TOPICS=20
10
+
11
+ if [[ ! -f "$TOPICS_FILE" ]]; then
12
+ echo "Missing $TOPICS_FILE" >&2
13
+ exit 1
14
+ fi
15
+
16
+ # Extract topic names from lines like: - `topic-name`
17
+ topics_raw=$(grep -E '^\s*-\s*`[^`]+`' "$TOPICS_FILE" | sed -n 's/.*`\([^`]*\)`.*/\1/p')
18
+ # Take first MAX_TOPICS (GitHub limit)
19
+ topics_list=$(echo "$topics_raw" | head -n "$MAX_TOPICS" | jq -R -s -c 'split("\n") | map(select(length > 0))')
20
+
21
+ payload=$(jq -n --argjson names "$topics_list" '{ names: $names }')
22
+
23
+ cd "$REPO_ROOT"
24
+ repo=$(gh repo view --json nameWithOwner -q .nameWithOwner)
25
+
26
+ echo "Setting ${repo} topics (from .github/TOPICS.md, max ${MAX_TOPICS})..."
27
+ gh api -X PUT \
28
+ -H "Accept: application/vnd.github.mercy-preview+json" \
29
+ "repos/${repo}/topics" \
30
+ --input - <<< "$payload"
31
+ echo "Done. Current topics:"
32
+ gh api "repos/${repo}/topics" -H "Accept: application/vnd.github.mercy-preview+json" -q '.names[]' | paste -sd ' ' -
33
+ echo
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env bash
2
+ # Set each @simpill package GitHub repo from private to public.
3
+ # Usage: run from repo root. Requires: gh CLI. Set GITHUB_OWNER if not using your user.
4
+ # DRY_RUN=1 ./scripts/github/github-set-repos-public.sh # list only, no changes
5
+ set -euo pipefail
6
+
7
+ GITHUB_OWNER="${GITHUB_OWNER:-$(gh api user -q .login 2>/dev/null || echo '')}"
8
+ if [[ -z "$GITHUB_OWNER" ]]; then
9
+ echo "Error: Could not get GitHub user. Run: gh auth login or set GITHUB_OWNER"
10
+ exit 1
11
+ fi
12
+
13
+ REPOS=(
14
+ adapters.utils
15
+ algorithms.utils
16
+ annotations.utils
17
+ api.utils
18
+ array.utils
19
+ async.utils
20
+ cache.utils
21
+ collections.utils
22
+ crypto.utils
23
+ data.utils
24
+ env.utils
25
+ enum.utils
26
+ errors.utils
27
+ events.utils
28
+ factories.utils
29
+ file.utils
30
+ function.utils
31
+ http.utils
32
+ logger.utils
33
+ middleware.utils
34
+ misc.utils
35
+ nextjs.utils
36
+ number.utils
37
+ object.utils
38
+ observability.utils
39
+ patterns.utils
40
+ protocols.utils
41
+ react.utils
42
+ request-context.utils
43
+ resilience.utils
44
+ socket.utils
45
+ string.utils
46
+ test.utils
47
+ time.utils
48
+ token-optimizer.utils
49
+ uuid.utils
50
+ zod.utils
51
+ zustand.utils
52
+ )
53
+
54
+ echo "Owner: $GITHUB_OWNER"
55
+ [[ -n "${DRY_RUN:-}" ]] && echo "DRY RUN (no visibility changes)"
56
+
57
+ for repo in "${REPOS[@]}"; do
58
+ full="$GITHUB_OWNER/$repo"
59
+ if ! gh repo view "$full" &>/dev/null; then
60
+ echo "Skip $full (repo not found)"
61
+ continue
62
+ fi
63
+ if [[ -n "${DRY_RUN:-}" ]]; then
64
+ echo "Would set $full -> public"
65
+ continue
66
+ fi
67
+ gh repo edit "$full" --visibility public --accept-visibility-change-consequences
68
+ echo "Set $full to public"
69
+ done
70
+
71
+ echo "Done."
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Shared package → topics/keywords for GitHub topics and npm package.json keywords.
3
+ * Used by github-set-all-topics.js and utils-set-npm-keywords.js.
4
+ */
5
+
6
+ const BASE_TOPICS = [
7
+ "simpill",
8
+ "typescript",
9
+ "utilities",
10
+ "type-safe",
11
+ "esm",
12
+ "tree-shakeable",
13
+ ];
14
+
15
+ /** Package short name (e.g. env.utils) → extra topics for that package */
16
+ const PACKAGE_TOPICS = {
17
+ "adapters.utils": ["adapters", "nodejs"],
18
+ "algorithms.utils": ["algorithms", "nodejs"],
19
+ "annotations.utils": ["annotations", "metadata", "nodejs"],
20
+ "api.utils": ["api", "http", "nodejs"],
21
+ "array.utils": ["array", "nodejs"],
22
+ "async.utils": ["async", "promises", "nodejs"],
23
+ "cache.utils": ["cache", "nodejs"],
24
+ "collections.utils": ["collections", "data-structures", "nodejs"],
25
+ "crypto.utils": ["crypto", "security", "nodejs"],
26
+ "data.utils": ["data", "validation", "nodejs"],
27
+ "env.utils": ["env", "dotenv", "nodejs", "edge-runtime"],
28
+ "enum.utils": ["enum", "nodejs"],
29
+ "errors.utils": ["errors", "nodejs"],
30
+ "events.utils": ["events", "nodejs"],
31
+ "factories.utils": ["factories", "nodejs"],
32
+ "file.utils": ["file", "nodejs"],
33
+ "function.utils": ["function", "nodejs"],
34
+ "http.utils": ["http", "nodejs"],
35
+ "logger.utils": ["logging", "nodejs"],
36
+ "middleware.utils": ["middleware", "nodejs"],
37
+ "misc.utils": ["nodejs"],
38
+ "nextjs.utils": ["nextjs", "react", "nodejs"],
39
+ "number.utils": ["number", "nodejs"],
40
+ "object.utils": ["object", "nodejs"],
41
+ "observability.utils": ["observability", "logging", "nodejs"],
42
+ "patterns.utils": ["patterns", "nodejs"],
43
+ "protocols.utils": ["protocols", "nodejs"],
44
+ "react.utils": ["react", "nodejs"],
45
+ "request-context.utils": ["request-context", "nodejs"],
46
+ "resilience.utils": ["resilience", "nodejs"],
47
+ "socket.utils": ["socket", "websocket", "nodejs"],
48
+ "string.utils": ["string", "nodejs"],
49
+ "test.utils": ["testing", "nodejs"],
50
+ "time.utils": ["time", "date", "nodejs"],
51
+ "token-optimizer.utils": ["tokens", "nodejs"],
52
+ "uuid.utils": ["uuid", "nodejs"],
53
+ "zod.utils": ["zod", "validation", "nodejs"],
54
+ "zustand.utils": ["zustand", "react", "state", "nodejs"],
55
+ };
56
+
57
+ module.exports = { BASE_TOPICS, PACKAGE_TOPICS };
@@ -0,0 +1,140 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Publish order and package.json rewrite for @simpill monorepo.
4
+ *
5
+ * Usage:
6
+ * node publish-order.js order [REPO_ROOT]
7
+ * Prints package directory names in topological publish order (one per line).
8
+ *
9
+ * node publish-order.js rewrite <package-dir> [REPO_ROOT]
10
+ * Reads package-dir/package.json, replaces file:../<x> with ^<version> for
11
+ * @simpill deps, prints result to stdout. Use with backup/restore when publishing.
12
+ *
13
+ * REPO_ROOT defaults to parent of scripts/lib (repo root).
14
+ */
15
+
16
+ const fs = require("fs");
17
+ const path = require("path");
18
+
19
+ const DEFAULT_REPO_ROOT = path.resolve(__dirname, "../..");
20
+
21
+ function getPackageDirs(utilsDir) {
22
+ const dirs = [];
23
+ try {
24
+ const entries = fs.readdirSync(utilsDir, { withFileTypes: true });
25
+ for (const e of entries) {
26
+ if (e.isDirectory() && e.name.endsWith(".utils")) {
27
+ const pkgPath = path.join(utilsDir, e.name, "package.json");
28
+ if (fs.existsSync(pkgPath)) dirs.push(e.name);
29
+ }
30
+ }
31
+ } catch (err) {
32
+ console.error("Failed to read utils:", err.message);
33
+ process.exit(1);
34
+ }
35
+ return dirs.sort();
36
+ }
37
+
38
+ function readPackageJson(utilsDir, dir) {
39
+ const p = path.join(utilsDir, dir, "package.json");
40
+ const raw = fs.readFileSync(p, "utf8");
41
+ return { obj: JSON.parse(raw), raw };
42
+ }
43
+
44
+ function collectDeps(obj) {
45
+ const deps = [];
46
+ for (const key of ["dependencies", "devDependencies", "peerDependencies"]) {
47
+ const section = obj[key];
48
+ if (!section || typeof section !== "object") continue;
49
+ for (const [name, value] of Object.entries(section)) {
50
+ if (name.startsWith("@simpill/") && typeof value === "string" && value.startsWith("file:../")) {
51
+ const depDir = value.replace(/^file:\.\.\//, "").replace(/\/$/, "");
52
+ if (depDir.endsWith(".utils")) deps.push(depDir);
53
+ }
54
+ }
55
+ }
56
+ return [...new Set(deps)];
57
+ }
58
+
59
+ function topologicalOrder(utilsDir) {
60
+ const dirs = getPackageDirs(utilsDir);
61
+ const dirToDeps = new Map();
62
+ for (const dir of dirs) {
63
+ const { obj } = readPackageJson(utilsDir, dir);
64
+ const depDirs = collectDeps(obj).filter((d) => dirs.includes(d));
65
+ dirToDeps.set(dir, depDirs);
66
+ }
67
+ // inDegree[dir] = number of @simpill deps (must publish deps before dir)
68
+ const inDegree = new Map();
69
+ for (const dir of dirs) inDegree.set(dir, dirToDeps.get(dir).length);
70
+ const order = [];
71
+ let queue = dirs.filter((d) => inDegree.get(d) === 0);
72
+ while (queue.length) {
73
+ const d = queue.shift();
74
+ order.push(d);
75
+ for (const [dir, deps] of dirToDeps) {
76
+ if (deps.includes(d)) {
77
+ const newDeg = inDegree.get(dir) - 1;
78
+ inDegree.set(dir, newDeg);
79
+ if (newDeg === 0) queue.push(dir);
80
+ }
81
+ }
82
+ }
83
+ const remaining = dirs.filter((d) => !order.includes(d));
84
+ if (remaining.length) {
85
+ console.error("Circular dependency among:", remaining.join(", "));
86
+ process.exit(1);
87
+ }
88
+ return order;
89
+ }
90
+
91
+ function rewritePackageJsonForPublish(utilsDir, packageDir) {
92
+ const packagePath = path.join(utilsDir, packageDir, "package.json");
93
+ if (!fs.existsSync(packagePath)) {
94
+ console.error("Not found:", packagePath);
95
+ process.exit(1);
96
+ }
97
+ const pkg = JSON.parse(fs.readFileSync(packagePath, "utf8"));
98
+ const dirs = getPackageDirs(utilsDir);
99
+ const versions = new Map();
100
+ for (const d of dirs) {
101
+ const obj = JSON.parse(fs.readFileSync(path.join(utilsDir, d, "package.json"), "utf8"));
102
+ if (obj.name && obj.version) versions.set(obj.name, obj.version);
103
+ }
104
+ for (const key of ["dependencies", "devDependencies", "peerDependencies"]) {
105
+ const section = pkg[key];
106
+ if (!section || typeof section !== "object") continue;
107
+ for (const [name, value] of Object.entries(section)) {
108
+ if (name.startsWith("@simpill/") && typeof value === "string" && value.startsWith("file:../")) {
109
+ const ver = versions.get(name);
110
+ if (ver) section[name] = `^${ver}`;
111
+ }
112
+ }
113
+ }
114
+ return JSON.stringify(pkg, null, 2);
115
+ }
116
+
117
+ const cmd = process.argv[2];
118
+ const repoRoot = cmd === "rewrite" ? (process.argv[4] || DEFAULT_REPO_ROOT) : (process.argv[3] || DEFAULT_REPO_ROOT);
119
+ const utilsDir = path.join(repoRoot, "utils");
120
+
121
+ if (cmd === "order") {
122
+ topologicalOrder(utilsDir).forEach((d) => console.log(d));
123
+ } else if (cmd === "rewrite") {
124
+ const packageDir = process.argv[3];
125
+ if (!packageDir || !packageDir.endsWith(".utils")) {
126
+ console.error("Usage: node publish-order.js rewrite <package-dir> [REPO_ROOT]");
127
+ console.error(" package-dir must end with .utils (e.g. async.utils)");
128
+ process.exit(1);
129
+ }
130
+ const dirName = path.basename(packageDir);
131
+ if (!fs.existsSync(path.join(utilsDir, dirName, "package.json"))) {
132
+ console.error("Not found:", path.join(utilsDir, dirName, "package.json"));
133
+ process.exit(1);
134
+ }
135
+ console.log(rewritePackageJsonForPublish(utilsDir, dirName));
136
+ } else {
137
+ console.error("Usage: node publish-order.js order [REPO_ROOT]");
138
+ console.error(" node publish-order.js rewrite <package-dir> [REPO_ROOT]");
139
+ process.exit(1);
140
+ }
@@ -0,0 +1,75 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Set repository, homepage, and bugs to a single monorepo base for all utils packages.
4
+ * Run from repo root or with REPO_ROOT; writes in place. Commit after running.
5
+ *
6
+ * Usage:
7
+ * REPO_BASE="https://github.com/owner/repo" [BRANCH=main] node scripts/lib/sync-repo-links.js
8
+ *
9
+ * REPO_BASE Base URL without trailing slash (e.g. https://github.com/SkinnnyJay/simpill-utils).
10
+ * BRANCH Default branch for homepage links (default: main).
11
+ *
12
+ * If REPO_BASE is not set, exits without changing anything.
13
+ */
14
+
15
+ const fs = require("fs");
16
+ const path = require("path");
17
+
18
+ const DEFAULT_REPO_ROOT = path.resolve(__dirname, "../..");
19
+ const REPO_ROOT = process.env.REPO_ROOT || DEFAULT_REPO_ROOT;
20
+ const UTILS = path.join(REPO_ROOT, "utils");
21
+ const REPO_BASE = process.env.REPO_BASE || "";
22
+ const BRANCH = process.env.BRANCH || "main";
23
+
24
+ if (!REPO_BASE) {
25
+ console.error("REPO_BASE is not set. Example:");
26
+ console.error(' REPO_BASE="https://github.com/SkinnnyJay/simpill-utils" BRANCH=main node scripts/lib/sync-repo-links.js');
27
+ process.exit(1);
28
+ }
29
+
30
+ const base = REPO_BASE.replace(/\/$/, "");
31
+ const repoGit = base + ".git";
32
+ const repoIssues = base + "/issues";
33
+
34
+ let dirs;
35
+ try {
36
+ dirs = fs.readdirSync(UTILS, { withFileTypes: true })
37
+ .filter((e) => e.isDirectory() && e.name.endsWith(".utils"))
38
+ .map((e) => e.name);
39
+ } catch (err) {
40
+ console.error("Failed to read utils:", err.message);
41
+ process.exit(1);
42
+ }
43
+
44
+ let updated = 0;
45
+ for (const dir of dirs) {
46
+ const pkgPath = path.join(UTILS, dir, "package.json");
47
+ if (!fs.existsSync(pkgPath)) continue;
48
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
49
+ const repo = {
50
+ type: "git",
51
+ url: repoGit,
52
+ directory: dir,
53
+ };
54
+ const homepage = `${base}/tree/${BRANCH}/utils/${dir}#readme`;
55
+ const bugs = { url: repoIssues };
56
+ let changed = false;
57
+ if (JSON.stringify(pkg.repository) !== JSON.stringify(repo)) {
58
+ pkg.repository = repo;
59
+ changed = true;
60
+ }
61
+ if (pkg.homepage !== homepage) {
62
+ pkg.homepage = homepage;
63
+ changed = true;
64
+ }
65
+ if (JSON.stringify(pkg.bugs) !== JSON.stringify(bugs)) {
66
+ pkg.bugs = bugs;
67
+ changed = true;
68
+ }
69
+ if (changed) {
70
+ fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf8");
71
+ console.log("Updated:", dir);
72
+ updated++;
73
+ }
74
+ }
75
+ console.log("Done. Updated", updated, "packages.");
@@ -0,0 +1,64 @@
1
+ #!/usr/bin/env bash
2
+ # =============================================================================
3
+ # Install git hooks at repo root. Pre-commit runs per-package checks when
4
+ # utils/ is present. This script is the source of truth; generated hook
5
+ # always uses a valid "then exit 1" block to avoid bash syntax errors.
6
+ # =============================================================================
7
+ set -euo pipefail
8
+
9
+ REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
10
+ HOOKS_DIR="$REPO_ROOT/.git/hooks"
11
+
12
+ if [ ! -d "$REPO_ROOT/.git" ] || [ ! -d "$HOOKS_DIR" ]; then
13
+ echo "Not a git repository or .git/hooks missing. Skipping hook installation."
14
+ exit 0
15
+ fi
16
+
17
+ # Package dir names from root package.json (@simpill/name.utils -> @simpill-name.utils)
18
+ PKG_NAMES=""
19
+ if [ -f "$REPO_ROOT/package.json" ]; then
20
+ PKG_NAMES=$(node -e "
21
+ const p = require('$REPO_ROOT/package.json');
22
+ const deps = p.dependencies || {};
23
+ const names = Object.keys(deps)
24
+ .filter(k => k.startsWith('@simpill/') && k.endsWith('.utils'))
25
+ .map(k => '@simpill-' + k.replace('@simpill/', '').replace('.utils', '') + '.utils');
26
+ console.log(names.sort().join('\n'));
27
+ " 2>/dev/null || true)
28
+ fi
29
+
30
+ # Build pre-commit hook. Each block must have "exit 1" in the then clause (no empty then).
31
+ PRE_COMMIT="$HOOKS_DIR/pre-commit"
32
+ cat > "$PRE_COMMIT" << 'HEADER'
33
+ #!/usr/bin/env bash
34
+ set -euo pipefail
35
+
36
+ # Package Pre-Commit Hook (generated by scripts/monorepo/install-hooks.sh)
37
+ REPO_ROOT="$(git rev-parse --show-toplevel)"
38
+
39
+ HEADER
40
+
41
+ while IFS= read -r dir; do
42
+ [ -z "$dir" ] && continue
43
+ # Use a single, valid block template: then clause always contains exit 1
44
+ name="${dir#@simpill-}"
45
+ name="${name%.utils}"
46
+ cat >> "$PRE_COMMIT" << BLOCK
47
+
48
+ # --- BEGIN ${name}.utils ---
49
+ if [ -f "\$REPO_ROOT/utils/${dir}/scripts/pre-commit.sh" ]; then
50
+ if git diff --cached --name-only | grep -q "^utils/${dir}/"; then
51
+ if ! "\$REPO_ROOT/utils/${dir}/scripts/pre-commit.sh"; then
52
+ exit 1
53
+ fi
54
+ fi
55
+ fi
56
+ # --- END ${name}.utils ---
57
+ BLOCK
58
+ done << EOF
59
+ $PKG_NAMES
60
+ EOF
61
+
62
+ echo "exit 0" >> "$PRE_COMMIT"
63
+ chmod +x "$PRE_COMMIT"
64
+ echo "Git hooks installed (pre-commit)."
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env bash
2
+ # Clean monorepo root: node_modules, lockfile, and build artifacts.
3
+ set -euo pipefail
4
+ REPO_ROOT="${1:-$(cd "$(dirname "$0")/../.." && pwd)}"
5
+ cd "$REPO_ROOT"
6
+ rm -rf node_modules package-lock.json dist coverage .next .jest-cache 2>/dev/null || true
7
+ echo "Monorepo root cleaned (node_modules, package-lock.json, dist, coverage, .next, .jest-cache)."
@@ -0,0 +1,81 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Sync monorepo root package.json @simpill dependencies to npm versions.
4
+ * If utils/@simpill-*.utils exists, uses each package's version (^version); otherwise uses ^1.0.0.
5
+ * Run from repo root. Use npm run use:local to switch to file:./utils/ for local development.
6
+ */
7
+ const fs = require("fs");
8
+ const path = require("path");
9
+
10
+ const REPO_ROOT = path.join(__dirname, "..", "..");
11
+ const UTILS = path.join(REPO_ROOT, "utils");
12
+ const ROOT_PKG = path.join(REPO_ROOT, "package.json");
13
+
14
+ function getPackageDirs() {
15
+ if (!fs.existsSync(UTILS)) return [];
16
+ return fs
17
+ .readdirSync(UTILS, { withFileTypes: true })
18
+ .filter(
19
+ (d) =>
20
+ d.isDirectory() &&
21
+ d.name.startsWith("@simpill-") &&
22
+ d.name.endsWith(".utils")
23
+ )
24
+ .map((d) => d.name)
25
+ .sort();
26
+ }
27
+
28
+ const dirs = getPackageDirs();
29
+ const deps = {};
30
+
31
+ for (const dir of dirs) {
32
+ const pkgPath = path.join(UTILS, dir, "package.json");
33
+ if (!fs.existsSync(pkgPath)) continue;
34
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
35
+ const name = pkg.name;
36
+ const version = (pkg.version && String(pkg.version).trim()) || "1.0.0";
37
+ if (name && name.startsWith("@simpill/")) {
38
+ deps[name] = `^${version}`;
39
+ }
40
+ }
41
+
42
+ // If no utils/ or empty, preserve existing @simpill names with ^1.0.0
43
+ const rootPkg = JSON.parse(fs.readFileSync(ROOT_PKG, "utf8"));
44
+ const existing = rootPkg.dependencies || {};
45
+ if (Object.keys(deps).length === 0) {
46
+ for (const [name, spec] of Object.entries(existing)) {
47
+ if (name.startsWith("@simpill/")) {
48
+ const v = spec.match(/^\^?([0-9.]+)/);
49
+ deps[name] = v ? `^${v[1]}` : "^1.0.0";
50
+ }
51
+ }
52
+ }
53
+
54
+ const rootDeps = rootPkg.dependencies || {};
55
+ let changed = false;
56
+ for (const [name, spec] of Object.entries(deps)) {
57
+ if (rootDeps[name] !== spec) {
58
+ rootDeps[name] = spec;
59
+ changed = true;
60
+ }
61
+ }
62
+ const utilNames = new Set(Object.keys(deps));
63
+ for (const name of Object.keys(rootDeps)) {
64
+ if (name.startsWith("@simpill/") && !utilNames.has(name)) {
65
+ delete rootDeps[name];
66
+ changed = true;
67
+ }
68
+ }
69
+ rootPkg.dependencies = rootDeps;
70
+
71
+ if (changed) {
72
+ const keys = Object.keys(rootPkg.dependencies).sort();
73
+ const sorted = {};
74
+ for (const k of keys) sorted[k] = rootPkg.dependencies[k];
75
+ rootPkg.dependencies = sorted;
76
+ fs.writeFileSync(ROOT_PKG, JSON.stringify(rootPkg, null, 2) + "\n", "utf8");
77
+ console.log("Updated root package.json dependencies to npm versions.");
78
+ } else {
79
+ console.log("Root package.json already in sync.");
80
+ }
81
+ console.log("Monorepo deps:", Object.keys(deps).length, "packages (npm ^version).");
@@ -0,0 +1,37 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Verify all @simpill dependencies: resolve and load each package.
4
+ * Run from repo root after npm install.
5
+ */
6
+ const path = require("path");
7
+ const pkg = require(path.join(process.cwd(), "package.json"));
8
+ const deps = Object.keys(pkg.dependencies || {}).filter((d) => d.startsWith("@simpill/"));
9
+
10
+ let resolved = 0;
11
+ let loaded = 0;
12
+ const loadErrors = [];
13
+
14
+ for (const d of deps) {
15
+ try {
16
+ require.resolve(d);
17
+ resolved++;
18
+ } catch (e) {
19
+ console.error("Resolve failed:", d, e.message);
20
+ continue;
21
+ }
22
+ try {
23
+ require(d);
24
+ loaded++;
25
+ } catch (e) {
26
+ loadErrors.push({ name: d, message: e.message });
27
+ }
28
+ }
29
+
30
+ console.log("Resolvable:", resolved + "/" + deps.length);
31
+ console.log("Loadable (require):", loaded + "/" + deps.length);
32
+ if (loadErrors.length > 0) {
33
+ console.error("Packages that did not load (may be ESM-only or need build):");
34
+ loadErrors.forEach(({ name, message }) => console.error(" ", name, message));
35
+ }
36
+ const ok = resolved === deps.length;
37
+ process.exit(ok ? 0 : 1);