@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,49 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Rewrite root package.json so @simpill deps point to local utils (file:./utils/@simpill-*.utils).
4
+ * Run from repo root. After this, run npm install so root uses built local packages.
5
+ * Use npm run sync:deps to restore npm ^version deps.
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
+ const dirs = fs
15
+ .readdirSync(UTILS, { withFileTypes: true })
16
+ .filter((d) => d.isDirectory() && d.name.startsWith("@simpill-") && d.name.endsWith(".utils"))
17
+ .map((d) => d.name)
18
+ .sort();
19
+
20
+ const deps = {};
21
+ for (const dir of dirs) {
22
+ const pkgPath = path.join(UTILS, dir, "package.json");
23
+ if (!fs.existsSync(pkgPath)) continue;
24
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
25
+ const name = pkg.name;
26
+ if (name && name.startsWith("@simpill/")) {
27
+ deps[name] = `file:./utils/${dir}`;
28
+ }
29
+ }
30
+
31
+ const rootPkg = JSON.parse(fs.readFileSync(ROOT_PKG, "utf8"));
32
+ rootPkg.dependencies = rootPkg.dependencies || {};
33
+ let changed = false;
34
+ for (const [name, spec] of Object.entries(deps)) {
35
+ if (rootPkg.dependencies[name] !== spec) {
36
+ rootPkg.dependencies[name] = spec;
37
+ changed = true;
38
+ }
39
+ }
40
+ if (changed) {
41
+ const keys = Object.keys(rootPkg.dependencies).sort();
42
+ const sorted = {};
43
+ for (const k of keys) sorted[k] = rootPkg.dependencies[k];
44
+ rootPkg.dependencies = sorted;
45
+ fs.writeFileSync(ROOT_PKG, JSON.stringify(rootPkg, null, 2) + "\n", "utf8");
46
+ console.log("Root package.json now uses file:./utils/@simpill-*.utils. Run: npm install");
47
+ } else {
48
+ console.log("Root already points to local utils.");
49
+ }
@@ -0,0 +1,152 @@
1
+ #!/usr/bin/env bash
2
+ # =============================================================================
3
+ # Publish all @simpill packages to GitHub (push) and npm in dependency order.
4
+ # Requires: gh (GitHub CLI) and npm logged in with access to @simpill scope.
5
+ # =============================================================================
6
+ # Usage:
7
+ # ./scripts/publish/publish-all.sh # GitHub push + npm publish (prompts)
8
+ # ./scripts/publish/publish-all.sh --dry-run # No push, npm publish --dry-run
9
+ # ./scripts/publish/publish-all.sh --skip-github # Only npm publish
10
+ # ./scripts/publish/publish-all.sh --yes # Skip confirmations
11
+ # =============================================================================
12
+
13
+ set -euo pipefail
14
+
15
+ REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
16
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
17
+ LIB_DIR="$REPO_ROOT/scripts/lib"
18
+
19
+ DRY_RUN=false
20
+ SKIP_GITHUB=false
21
+ YES=false
22
+
23
+ for arg in "$@"; do
24
+ case "$arg" in
25
+ --dry-run) DRY_RUN=true ;;
26
+ --skip-github) SKIP_GITHUB=true ;;
27
+ --npm-only) SKIP_GITHUB=true ;;
28
+ --yes|-y) YES=true ;;
29
+ *)
30
+ echo "Unknown option: $arg"
31
+ echo "Usage: $0 [--dry-run] [--skip-github|--npm-only] [--yes|-y]"
32
+ exit 1
33
+ ;;
34
+ esac
35
+ done
36
+
37
+ echo "============================================"
38
+ echo "Publish all @simpill packages"
39
+ echo "============================================"
40
+ echo " --dry-run: $DRY_RUN"
41
+ echo " --skip-github: $SKIP_GITHUB"
42
+ echo " --yes: $YES"
43
+ echo "============================================"
44
+
45
+ # -----------------------------------------------------------------------------
46
+ # 1. Check GitHub CLI and auth (unless skipped)
47
+ # -----------------------------------------------------------------------------
48
+ if [ "$SKIP_GITHUB" = false ]; then
49
+ if ! command -v gh &>/dev/null; then
50
+ echo "Error: GitHub CLI (gh) not found. Install it or use --skip-github."
51
+ exit 1
52
+ fi
53
+ if ! gh auth status &>/dev/null; then
54
+ echo "Error: Not logged in to GitHub. Run: gh auth login"
55
+ exit 1
56
+ fi
57
+ if [ "$DRY_RUN" = false ] && [ "$YES" = false ]; then
58
+ echo "GitHub: will push current branch. Continue? [y/N]"
59
+ read -r ans
60
+ if [ "$ans" != "y" ] && [ "$ans" != "Y" ]; then
61
+ echo "Aborted."
62
+ exit 0
63
+ fi
64
+ fi
65
+ if [ "$DRY_RUN" = false ]; then
66
+ echo "Pushing to GitHub..."
67
+ git -C "$REPO_ROOT" push
68
+ echo "GitHub push done."
69
+ else
70
+ echo "[dry-run] Skipping GitHub push."
71
+ fi
72
+ fi
73
+
74
+ # -----------------------------------------------------------------------------
75
+ # 2. Check npm auth and @simpill scope
76
+ # -----------------------------------------------------------------------------
77
+ if ! command -v npm &>/dev/null; then
78
+ echo "Error: npm not found."
79
+ exit 1
80
+ fi
81
+ if ! npm whoami &>/dev/null; then
82
+ echo "Error: Not logged in to npm. Run: npm login"
83
+ exit 1
84
+ fi
85
+ if [ "$DRY_RUN" = false ] && [ "$YES" = false ]; then
86
+ echo "Publish all packages to npm under @simpill? [y/N]"
87
+ read -r ans
88
+ if [ "$ans" != "y" ] && [ "$ans" != "Y" ]; then
89
+ echo "Aborted."
90
+ exit 0
91
+ fi
92
+ fi
93
+
94
+ # -----------------------------------------------------------------------------
95
+ # 3. Get publish order (topological)
96
+ # -----------------------------------------------------------------------------
97
+ ORDER=()
98
+ while IFS= read -r line; do
99
+ [ -n "$line" ] && ORDER+=("$line")
100
+ done < <(node "$LIB_DIR/publish-order.js" order "$REPO_ROOT")
101
+
102
+ echo "Publish order (${#ORDER[@]} packages):"
103
+ for d in "${ORDER[@]}"; do
104
+ echo " - $d"
105
+ done
106
+
107
+ # -----------------------------------------------------------------------------
108
+ # 4. Publish each package: backup package.json, rewrite file: -> ^ver, publish, restore
109
+ # -----------------------------------------------------------------------------
110
+ FAILED=()
111
+ for dir in "${ORDER[@]}"; do
112
+ pkg_path="$REPO_ROOT/utils/$dir/package.json"
113
+ if [ ! -f "$pkg_path" ]; then
114
+ echo " Skip $dir (no package.json)"
115
+ continue
116
+ fi
117
+ name=$(node -e "console.log(require('$pkg_path').name)")
118
+ echo "----------------------------------------"
119
+ echo "Publishing: $name ($dir)"
120
+ echo "----------------------------------------"
121
+ backup="$pkg_path.publish-backup"
122
+ cp "$pkg_path" "$backup"
123
+ trap "mv -f '$backup' '$pkg_path'" EXIT
124
+ node "$LIB_DIR/publish-order.js" rewrite "$dir" "$REPO_ROOT" > "$pkg_path"
125
+ set +e
126
+ if [ "$DRY_RUN" = true ]; then
127
+ (cd "$REPO_ROOT/utils/$dir" && npm publish --access public --dry-run)
128
+ else
129
+ (cd "$REPO_ROOT/utils/$dir" && npm publish --access public)
130
+ fi
131
+ ret=$?
132
+ set -e
133
+ mv -f "$backup" "$pkg_path"
134
+ trap - EXIT
135
+ if [ $ret -ne 0 ]; then
136
+ echo " FAILED: $name"
137
+ FAILED+=("$name")
138
+ else
139
+ echo " OK: $name"
140
+ fi
141
+ done
142
+
143
+ echo ""
144
+ echo "============================================"
145
+ echo "Publish summary"
146
+ echo "============================================"
147
+ if [ ${#FAILED[@]} -eq 0 ]; then
148
+ echo "All packages published successfully."
149
+ exit 0
150
+ fi
151
+ echo "Failed: ${FAILED[*]}"
152
+ exit 1
@@ -0,0 +1,61 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Fix repository, bugs, homepage in each utils package for standalone GitHub repos.
4
+ * Replaces file:../@simpill-*.utils deps with github:SkinnnyJay/<name>.utils.
5
+ * Run from repo root. Requires utils/@simpill-*.utils to exist.
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 OWNER = "SkinnnyJay";
13
+
14
+ const dirs = fs.readdirSync(UTILS, { withFileTypes: true })
15
+ .filter((d) => d.isDirectory() && d.name.startsWith("@simpill-") && d.name.endsWith(".utils"))
16
+ .map((d) => d.name);
17
+
18
+ for (const dir of dirs) {
19
+ const shortName = dir.replace(/^@simpill-/, "");
20
+ const pkgPath = path.join(UTILS, dir, "package.json");
21
+ if (!fs.existsSync(pkgPath)) continue;
22
+
23
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
24
+ let changed = false;
25
+
26
+ if (!pkg.repository || pkg.repository.url.includes("simpill.git")) {
27
+ pkg.repository = { type: "git", url: `https://github.com/${OWNER}/${shortName}.git` };
28
+ changed = true;
29
+ }
30
+ if (pkg.repository && pkg.repository.directory && pkg.repository.directory !== ".") {
31
+ pkg.repository.directory = ".";
32
+ changed = true;
33
+ }
34
+ if (!pkg.bugs || pkg.bugs.url.includes("simpill/issues")) {
35
+ pkg.bugs = { url: `https://github.com/${OWNER}/${shortName}/issues` };
36
+ changed = true;
37
+ }
38
+ if (!pkg.homepage || pkg.homepage.includes("simpill/tree")) {
39
+ pkg.homepage = `https://github.com/${OWNER}/${shortName}#readme`;
40
+ changed = true;
41
+ }
42
+
43
+ if (pkg.dependencies) {
44
+ for (const [dep, spec] of Object.entries(pkg.dependencies)) {
45
+ if (dep.startsWith("@simpill/") && typeof spec === "string" && spec.startsWith("file:../")) {
46
+ const match = spec.match(/file:\.\.\/@simpill-(.+\.utils)/);
47
+ if (match) {
48
+ pkg.dependencies[dep] = `github:${OWNER}/${match[1]}`;
49
+ changed = true;
50
+ }
51
+ }
52
+ }
53
+ }
54
+
55
+ if (changed) {
56
+ fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf8");
57
+ console.log("Updated:", dir);
58
+ }
59
+ }
60
+
61
+ console.log("Done. Check utils/@simpill-*.utils/package.json and commit + push each repo.");
@@ -0,0 +1,107 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # =============================================================================
5
+ # Prepare all utils packages for npm: install, audit --fix, typecheck, test, build
6
+ # =============================================================================
7
+ # Run from repo root: ./scripts/utils/utils-prepare-all.sh
8
+ # Exits 0 only if every package passes all steps.
9
+ # Fix any reported failures in the failing package, then re-run.
10
+ # =============================================================================
11
+
12
+ REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
13
+ UTILS_DIR="$REPO_ROOT/utils"
14
+ SCRIPTS_LIB="$REPO_ROOT/scripts/lib"
15
+ cd "$REPO_ROOT"
16
+
17
+ FAILED_PACKAGES=()
18
+ TOTAL=0
19
+ PASSED=0
20
+
21
+ # Process in dependency order so typecheck/build can resolve file:../@simpill-*.utils
22
+ ORDERED_DIRS=()
23
+ while IFS= read -r line; do
24
+ [ -n "$line" ] && ORDERED_DIRS+=("$UTILS_DIR/$line")
25
+ done < <(node "$SCRIPTS_LIB/publish-order.js" order "$REPO_ROOT" 2>/dev/null)
26
+ if [ ${#ORDERED_DIRS[@]} -eq 0 ]; then
27
+ # Fallback: alphabetical
28
+ for d in "$UTILS_DIR"/@simpill-*.utils; do
29
+ [ -d "$d" ] && [ -f "$d/package.json" ] && ORDERED_DIRS+=("$d")
30
+ done
31
+ fi
32
+
33
+ for d in "${ORDERED_DIRS[@]}"; do
34
+ [ -d "$d" ] || continue
35
+ [ -f "$d/package.json" ] || continue
36
+ name=$(basename "$d")
37
+ TOTAL=$((TOTAL + 1))
38
+ echo "----------------------------------------"
39
+ echo " $name"
40
+ echo "----------------------------------------"
41
+
42
+ failed=false
43
+ if ! (cd "$d" && npm install --no-audit --no-fund --silent 2>&1) >/dev/null; then
44
+ echo " install ✗"
45
+ failed=true
46
+ else
47
+ echo " install ✓"
48
+ fi
49
+
50
+ if [ "$failed" = false ]; then
51
+ if (cd "$d" && npm audit fix --force 2>&1) >/dev/null; then
52
+ echo " audit ✓"
53
+ else
54
+ echo " audit ⚠ (issues remain; run 'npm audit' in $d)"
55
+ # Do not set failed=true: audit often exits 1 for unfixable issues; still run typecheck/test/build
56
+ fi
57
+ fi
58
+
59
+ if [ "$failed" = false ]; then
60
+ if ! (cd "$d" && npm run typecheck --if-present 2>&1) >/dev/null; then
61
+ echo " typecheck ✗"
62
+ failed=true
63
+ else
64
+ echo " typecheck ✓"
65
+ fi
66
+ fi
67
+
68
+ if [ "$failed" = false ]; then
69
+ if ! (cd "$d" && npm test --silent 2>&1) >/dev/null; then
70
+ echo " test ✗"
71
+ failed=true
72
+ else
73
+ echo " test ✓"
74
+ fi
75
+ fi
76
+
77
+ if [ "$failed" = false ]; then
78
+ if ! (cd "$d" && npm run build --silent 2>&1) >/dev/null; then
79
+ echo " build ✗"
80
+ failed=true
81
+ else
82
+ echo " build ✓"
83
+ fi
84
+ fi
85
+
86
+ if [ "$failed" = true ]; then
87
+ FAILED_PACKAGES+=("$name")
88
+ else
89
+ PASSED=$((PASSED + 1))
90
+ fi
91
+ echo ""
92
+ done
93
+
94
+ echo "============================================"
95
+ echo "Prepare all utils – summary"
96
+ echo "============================================"
97
+ echo " Passed: $PASSED / $TOTAL"
98
+ if [ ${#FAILED_PACKAGES[@]} -gt 0 ]; then
99
+ echo " Failed: ${FAILED_PACKAGES[*]}"
100
+ echo ""
101
+ echo "Fix issues in each package above, then re-run: ./scripts/utils/utils-prepare-all.sh"
102
+ echo "============================================"
103
+ exit 1
104
+ fi
105
+ echo " All packages ready for npm."
106
+ echo "============================================"
107
+ exit 0
@@ -0,0 +1,132 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Set npm package.json "keywords" from each package's TOPICS.md (if present)
4
+ * or from the shared package-topics list. Run from repo root.
5
+ * Requires utils/@simpill-*.utils to exist.
6
+ *
7
+ * Usage:
8
+ * node scripts/utils/utils-set-npm-keywords.js # update package.json from TOPICS.md or fallback
9
+ * node scripts/utils/utils-set-npm-keywords.js --write-topics-md # write TOPICS.md in each package, then update keywords
10
+ * node scripts/utils/utils-set-npm-keywords.js --dry-run # print what would be set
11
+ */
12
+
13
+ const fs = require("fs");
14
+ const path = require("path");
15
+
16
+ const REPO_ROOT = path.resolve(__dirname, "..", "..");
17
+ const UTILS_DIR = path.join(REPO_ROOT, "utils");
18
+ const MAX_KEYWORDS = 20;
19
+
20
+ const { BASE_TOPICS, PACKAGE_TOPICS } = require("../lib/package-topics.js");
21
+
22
+ function getPackageDirs() {
23
+ if (!fs.existsSync(UTILS_DIR)) return [];
24
+ return fs
25
+ .readdirSync(UTILS_DIR, { withFileTypes: true })
26
+ .filter(
27
+ (d) =>
28
+ d.isDirectory() &&
29
+ d.name.startsWith("@simpill-") &&
30
+ d.name.endsWith(".utils")
31
+ )
32
+ .map((d) => ({ dir: d.name, shortName: d.name.replace(/^@simpill-/, "") }));
33
+ }
34
+
35
+ /**
36
+ * Parse TOPICS.md format: lines like `- \`topic\``
37
+ */
38
+ function parseTopicsMd(filePath) {
39
+ if (!fs.existsSync(filePath)) return null;
40
+ const content = fs.readFileSync(filePath, "utf8");
41
+ const names = [];
42
+ const re = /^\s*-\s*`([^`]+)`/gm;
43
+ let m;
44
+ while ((m = re.exec(content)) !== null) names.push(m[1]);
45
+ return names.length ? names : null;
46
+ }
47
+
48
+ function getKeywordsFromFallback(shortName) {
49
+ const extra = PACKAGE_TOPICS[shortName] || [shortName.replace(".utils", "")];
50
+ const combined = [...BASE_TOPICS, ...extra];
51
+ return [...new Set(combined)].slice(0, MAX_KEYWORDS);
52
+ }
53
+
54
+ function getKeywordsForPackage(pkgDir, shortName) {
55
+ const topicsPath = path.join(UTILS_DIR, pkgDir, "TOPICS.md");
56
+ const fromFile = parseTopicsMd(topicsPath);
57
+ if (fromFile && fromFile.length > 0) {
58
+ return [...new Set(["simpill", ...fromFile])].slice(0, MAX_KEYWORDS);
59
+ }
60
+ return getKeywordsFromFallback(shortName);
61
+ }
62
+
63
+ function writeTopicsMd(pkgDir, shortName) {
64
+ const keywords = getKeywordsFromFallback(shortName);
65
+ const topicsPath = path.join(UTILS_DIR, pkgDir, "TOPICS.md");
66
+ const lines = [
67
+ "# npm keywords (and GitHub topics)",
68
+ "",
69
+ "Used by `scripts/utils/utils-set-npm-keywords.js` to set package.json keywords.",
70
+ "Edit this file to customize; one topic per line in backticks.",
71
+ "",
72
+ ...keywords.map((k) => `- \`${k}\``),
73
+ "",
74
+ ];
75
+ fs.writeFileSync(topicsPath, lines.join("\n"), "utf8");
76
+ return keywords;
77
+ }
78
+
79
+ function main() {
80
+ const dryRun = process.argv.includes("--dry-run");
81
+ const writeTopicsMdFlag = process.argv.includes("--write-topics-md");
82
+
83
+ const packages = getPackageDirs();
84
+ if (packages.length === 0) {
85
+ console.log("No utils/@simpill-*.utils packages found. Exiting.");
86
+ process.exit(0);
87
+ }
88
+
89
+ console.log(
90
+ writeTopicsMdFlag
91
+ ? "Writing TOPICS.md per package and updating package.json keywords...\n"
92
+ : "Updating package.json keywords from each package's TOPICS.md (or fallback)...\n"
93
+ );
94
+
95
+ for (const { dir, shortName } of packages) {
96
+ const pkgPath = path.join(UTILS_DIR, dir, "package.json");
97
+ if (!fs.existsSync(pkgPath)) continue;
98
+
99
+ let keywords;
100
+ if (writeTopicsMdFlag) {
101
+ keywords = writeTopicsMd(dir, shortName);
102
+ console.log(` ${dir}: wrote TOPICS.md, keywords (${keywords.length})`);
103
+ } else {
104
+ keywords = getKeywordsForPackage(dir, shortName);
105
+ }
106
+
107
+ if (dryRun) {
108
+ console.log(` ${dir}: (dry run) ${keywords.slice(0, 8).join(", ")}${keywords.length > 8 ? "..." : ""}`);
109
+ continue;
110
+ }
111
+
112
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
113
+ const prev = pkg.keywords || [];
114
+ const same =
115
+ Array.isArray(prev) &&
116
+ prev.length === keywords.length &&
117
+ prev.every((k, i) => k === keywords[i]);
118
+ if (same) continue;
119
+
120
+ pkg.keywords = keywords;
121
+ fs.writeFileSync(
122
+ pkgPath,
123
+ JSON.stringify(pkg, null, 2) + "\n",
124
+ "utf8"
125
+ );
126
+ console.log(` ${dir}: updated keywords (${keywords.length})`);
127
+ }
128
+
129
+ console.log("\nDone.");
130
+ }
131
+
132
+ main();
@@ -0,0 +1,83 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Updates each utils package README.md:
4
+ * - Puts an Install section at the very top (npm + GitHub)
5
+ * - Adds npm and GitHub badges after the first line (banner) if missing
6
+ * - Removes duplicate ## Installation section from the body
7
+ *
8
+ * Usage: node scripts/utils/utils-update-readme-badges.js
9
+ */
10
+
11
+ const fs = require("fs");
12
+ const path = require("path");
13
+
14
+ const REPO = "SkinnnyJay/simpill-utils";
15
+ const REPO_URL = `https://github.com/${REPO}`;
16
+
17
+ const utilsDir = path.join(__dirname, "..", "..", "utils");
18
+ const dirs = fs
19
+ .readdirSync(utilsDir, { withFileTypes: true })
20
+ .filter((d) => d.isDirectory() && d.name.startsWith("@simpill-"))
21
+ .map((d) => d.name);
22
+
23
+ function removeSection(readme, heading) {
24
+ const idx = readme.indexOf(heading);
25
+ if (idx === -1) return readme;
26
+ const afterHeading = readme.slice(idx);
27
+ const nextH2 = afterHeading.slice(heading.length).match(/\n## /);
28
+ const end = nextH2 ? idx + heading.length + nextH2.index : readme.length;
29
+ const before = readme.slice(0, idx).trimEnd();
30
+ let after = readme.slice(end).trimStart();
31
+ after = after.replace(/^---\s*\n?/, "");
32
+ return before + (after ? "\n\n---\n\n" + after : "");
33
+ }
34
+
35
+ for (const dir of dirs) {
36
+ const pkgPath = path.join(utilsDir, dir, "package.json");
37
+ const readmePath = path.join(utilsDir, dir, "README.md");
38
+ if (!fs.existsSync(pkgPath) || !fs.existsSync(readmePath)) continue;
39
+
40
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
41
+ const name = pkg.name;
42
+ const npmName = name.replace("/", "%2f");
43
+ let readme = fs.readFileSync(readmePath, "utf8");
44
+
45
+ // Remove existing ## Installation / ## Install so we don't duplicate
46
+ readme = removeSection(readme, "## Installation");
47
+ readme = removeSection(readme, "## Install");
48
+
49
+ // Install block at the very top
50
+ const installBlock = `## Install
51
+
52
+ **npm**
53
+ \`\`\`bash
54
+ npm install ${name}
55
+ \`\`\`
56
+
57
+ **GitHub** (from monorepo)
58
+ \`\`\`bash
59
+ git clone https://github.com/${REPO}.git && cd simpill/utils/${dir} && npm install && npm run build
60
+ \`\`\`
61
+ Then in your project: \`npm install /path/to/simpill/utils/${dir}\` or \`npm link\` from that directory.
62
+
63
+ ---
64
+
65
+ `;
66
+
67
+ // Badges after first line (banner) if not already present
68
+ const npmBadge = `[![npm version](https://img.shields.io/npm/v/${npmName}.svg)](https://www.npmjs.com/package/${name})`;
69
+ const ghBadge = `[![GitHub](https://img.shields.io/badge/GitHub-source-blue?logo=github)](${REPO_URL}/tree/main/utils/${dir})`;
70
+ const badgeLine = `\n<p align="center">\n ${npmBadge}\n ${ghBadge}\n</p>\n`;
71
+
72
+ let out = installBlock + readme.trimStart();
73
+
74
+ if (!out.includes("img.shields.io/npm/v/")) {
75
+ const firstLineEnd = out.indexOf("\n");
76
+ out = out.slice(0, firstLineEnd + 1) + badgeLine + out.slice(firstLineEnd + 1);
77
+ }
78
+
79
+ fs.writeFileSync(readmePath, out, "utf8");
80
+ console.log("Updated:", dir);
81
+ }
82
+
83
+ console.log("Done.");
@@ -0,0 +1,43 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Rewrite each utils package to use file:../@simpill-<name>.utils for @simpill deps
4
+ * so that build/test use local packages. Run from repo root.
5
+ */
6
+ const fs = require("fs");
7
+ const path = require("path");
8
+
9
+ const REPO_ROOT = path.join(__dirname, "..", "..");
10
+ const UTILS = path.join(REPO_ROOT, "utils");
11
+ const OWNER = "SkinnnyJay";
12
+
13
+ const dirs = fs
14
+ .readdirSync(UTILS, { withFileTypes: true })
15
+ .filter((d) => d.isDirectory() && d.name.startsWith("@simpill-") && d.name.endsWith(".utils"))
16
+ .map((d) => d.name)
17
+ .sort();
18
+
19
+ let changed = 0;
20
+ for (const dir of dirs) {
21
+ const pkgPath = path.join(UTILS, dir, "package.json");
22
+ if (!fs.existsSync(pkgPath)) continue;
23
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
24
+
25
+ for (const key of ["dependencies", "devDependencies"]) {
26
+ const section = pkg[key];
27
+ if (!section || typeof section !== "object") continue;
28
+ for (const [name, spec] of Object.entries(section)) {
29
+ if (!name.startsWith("@simpill/") || typeof spec !== "string") continue;
30
+ const m = spec.match(/^github:([^/]+)\/(.+)$/);
31
+ if (m && m[1] === OWNER && m[2].endsWith(".utils")) {
32
+ const depDir = `@simpill-${m[2]}`;
33
+ if (depDir !== dir && dirs.includes(depDir)) {
34
+ section[name] = `file:../${depDir}`;
35
+ changed++;
36
+ }
37
+ }
38
+ }
39
+ }
40
+ fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf8");
41
+ }
42
+ console.log("Updated", changed, "dependency entries to file:../ in utils packages.");
43
+ process.exit(0);
@@ -0,0 +1,45 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # =============================================================================
5
+ # Verify all utils – build and test every package under utils/@simpill-*.utils
6
+ # =============================================================================
7
+ # Run from repo root: ./scripts/utils/utils-verify-all.sh
8
+ # Exits 0 if all packages build and test pass; 1 otherwise.
9
+ # =============================================================================
10
+
11
+ REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
12
+ cd "$REPO_ROOT"
13
+
14
+ FAILED_BUILD=()
15
+ FAILED_TEST=()
16
+
17
+ for d in utils/@simpill-*.utils; do
18
+ name=$(basename "$d")
19
+ if (cd "$d" && npm run build --silent 2>&1) >/dev/null; then
20
+ echo " build $name ✓"
21
+ else
22
+ echo " build $name ✗"
23
+ FAILED_BUILD+=("$name")
24
+ fi
25
+ if (cd "$d" && npm test --silent 2>&1) >/dev/null; then
26
+ echo " test $name ✓"
27
+ else
28
+ echo " test $name ✗"
29
+ FAILED_TEST+=("$name")
30
+ fi
31
+ done
32
+
33
+ echo ""
34
+ echo "============================================"
35
+ echo "Verify all utils – summary"
36
+ echo "============================================"
37
+ if [ ${#FAILED_BUILD[@]} -eq 0 ] && [ ${#FAILED_TEST[@]} -eq 0 ]; then
38
+ echo "All packages: build and test passed."
39
+ echo "============================================"
40
+ exit 0
41
+ fi
42
+ [ ${#FAILED_BUILD[@]} -gt 0 ] && echo "Build failed: ${FAILED_BUILD[*]}"
43
+ [ ${#FAILED_TEST[@]} -gt 0 ] && echo "Test failed: ${FAILED_TEST[*]}"
44
+ echo "============================================"
45
+ exit 1
package/tsconfig.json ADDED
@@ -0,0 +1,14 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "esModuleInterop": true,
7
+ "strict": true,
8
+ "skipLibCheck": true,
9
+ "noEmit": true,
10
+ "resolveJsonModule": true
11
+ },
12
+ "include": ["index.ts", "scripts/**/*.js", "__tests__/**/*.ts"],
13
+ "exclude": ["node_modules", "utils"]
14
+ }