sqlite-hub 0.1.3

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 (76) hide show
  1. package/.npmingnore +4 -0
  2. package/README.md +46 -0
  3. package/assets/images/logo.webp +0 -0
  4. package/assets/images/logo_extrasmall.webp +0 -0
  5. package/assets/images/logo_raw.png +0 -0
  6. package/assets/images/logo_small.webp +0 -0
  7. package/assets/mockups/connections.png +0 -0
  8. package/assets/mockups/data.png +0 -0
  9. package/assets/mockups/data_edit.png +0 -0
  10. package/assets/mockups/home.png +0 -0
  11. package/assets/mockups/overview.png +0 -0
  12. package/assets/mockups/sql_editor.png +0 -0
  13. package/assets/mockups/structure.png +0 -0
  14. package/bin/sqlite-hub.js +116 -0
  15. package/changelog.md +3 -0
  16. package/data/.gitkeep +0 -0
  17. package/index.html +100 -0
  18. package/js/api.js +193 -0
  19. package/js/app.js +520 -0
  20. package/js/components/actionBar.js +8 -0
  21. package/js/components/appShell.js +17 -0
  22. package/js/components/badges.js +5 -0
  23. package/js/components/bottomTabs.js +37 -0
  24. package/js/components/connectionCard.js +106 -0
  25. package/js/components/dataGrid.js +47 -0
  26. package/js/components/emptyState.js +159 -0
  27. package/js/components/metricCard.js +32 -0
  28. package/js/components/modal.js +317 -0
  29. package/js/components/pageHeader.js +33 -0
  30. package/js/components/queryEditor.js +121 -0
  31. package/js/components/queryResults.js +107 -0
  32. package/js/components/rowEditorPanel.js +164 -0
  33. package/js/components/sidebar.js +57 -0
  34. package/js/components/statusBar.js +39 -0
  35. package/js/components/toast.js +39 -0
  36. package/js/components/topNav.js +27 -0
  37. package/js/router.js +66 -0
  38. package/js/store.js +1092 -0
  39. package/js/utils/format.js +179 -0
  40. package/js/views/connections.js +133 -0
  41. package/js/views/data.js +400 -0
  42. package/js/views/editor.js +259 -0
  43. package/js/views/landing.js +11 -0
  44. package/js/views/overview.js +220 -0
  45. package/js/views/settings.js +109 -0
  46. package/js/views/structure.js +242 -0
  47. package/package.json +18 -0
  48. package/publish_brew.sh +444 -0
  49. package/publish_npm.sh +241 -0
  50. package/server/routes/connections.js +146 -0
  51. package/server/routes/data.js +59 -0
  52. package/server/routes/export.js +25 -0
  53. package/server/routes/overview.js +39 -0
  54. package/server/routes/settings.js +50 -0
  55. package/server/routes/sql.js +50 -0
  56. package/server/routes/structure.js +38 -0
  57. package/server/server.js +136 -0
  58. package/server/services/sqlite/connectionManager.js +306 -0
  59. package/server/services/sqlite/dataBrowserService.js +255 -0
  60. package/server/services/sqlite/exportService.js +34 -0
  61. package/server/services/sqlite/importService.js +111 -0
  62. package/server/services/sqlite/introspection.js +302 -0
  63. package/server/services/sqlite/overviewService.js +109 -0
  64. package/server/services/sqlite/sqlExecutor.js +434 -0
  65. package/server/services/sqlite/structureService.js +60 -0
  66. package/server/services/storage/appStateStore.js +530 -0
  67. package/server/utils/csv.js +34 -0
  68. package/server/utils/errors.js +175 -0
  69. package/server/utils/fileValidation.js +135 -0
  70. package/server/utils/identifier.js +38 -0
  71. package/server/utils/sqliteTypes.js +112 -0
  72. package/styles/base.css +176 -0
  73. package/styles/components.css +323 -0
  74. package/styles/layout.css +101 -0
  75. package/styles/tokens.css +49 -0
  76. package/styles/views.css +84 -0
@@ -0,0 +1,444 @@
1
+ #!/usr/bin/env bash
2
+
3
+ set -euo pipefail
4
+
5
+ ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
6
+ cd "$ROOT_DIR"
7
+
8
+ DRY_RUN=0
9
+ SKIP_AUDIT=0
10
+ ALLOW_DIRTY=0
11
+ VERSION_OVERRIDE=""
12
+ TAP_REPO_OVERRIDE=""
13
+ TAP_DIR_OVERRIDE=""
14
+ FORMULA_NAME_OVERRIDE=""
15
+ GH_REMOTE="origin"
16
+
17
+ usage() {
18
+ cat <<'EOF'
19
+ Usage:
20
+ ./publish.sh [options]
21
+
22
+ Options:
23
+ --version X.Y.Z Override the package.json version for this publish run.
24
+ --tap-repo OWNER/REPO
25
+ Homebrew tap repository. Default: <origin-owner>/homebrew-tap
26
+ --tap-dir PATH Local clone path for the tap repo. Default: ../homebrew-tap
27
+ --formula-name NAME Formula name. Default: package.json name
28
+ --allow-dirty Allow publishing with uncommitted changes in the source repo.
29
+ --skip-audit Skip `brew audit` for the generated formula.
30
+ --dry-run Print the steps without pushing or writing changes.
31
+ --help Show this help text.
32
+
33
+ Requirements:
34
+ - git
35
+ - gh (authenticated)
36
+ - curl
37
+ - shasum
38
+ - node
39
+ - brew (optional, only for audit)
40
+ EOF
41
+ }
42
+
43
+ while [[ $# -gt 0 ]]; do
44
+ case "$1" in
45
+ --version)
46
+ VERSION_OVERRIDE="${2:-}"
47
+ shift 2
48
+ ;;
49
+ --tap-repo)
50
+ TAP_REPO_OVERRIDE="${2:-}"
51
+ shift 2
52
+ ;;
53
+ --tap-dir)
54
+ TAP_DIR_OVERRIDE="${2:-}"
55
+ shift 2
56
+ ;;
57
+ --formula-name)
58
+ FORMULA_NAME_OVERRIDE="${2:-}"
59
+ shift 2
60
+ ;;
61
+ --allow-dirty)
62
+ ALLOW_DIRTY=1
63
+ shift
64
+ ;;
65
+ --skip-audit)
66
+ SKIP_AUDIT=1
67
+ shift
68
+ ;;
69
+ --dry-run)
70
+ DRY_RUN=1
71
+ shift
72
+ ;;
73
+ --help|-h)
74
+ usage
75
+ exit 0
76
+ ;;
77
+ *)
78
+ echo "Unknown argument: $1" >&2
79
+ usage
80
+ exit 1
81
+ ;;
82
+ esac
83
+ done
84
+
85
+ require_cmd() {
86
+ if ! command -v "$1" >/dev/null 2>&1; then
87
+ echo "Missing required command: $1" >&2
88
+ exit 1
89
+ fi
90
+ }
91
+
92
+ run() {
93
+ if [[ "$DRY_RUN" == "1" ]]; then
94
+ printf '+'
95
+ printf ' %q' "$@"
96
+ printf '\n'
97
+ return 0
98
+ fi
99
+
100
+ "$@"
101
+ }
102
+
103
+ info() {
104
+ printf '==> %s\n' "$*"
105
+ }
106
+
107
+ parse_github_slug() {
108
+ node - "$1" <<'NODE'
109
+ const remote = process.argv[2] ?? "";
110
+ const normalized = remote
111
+ .replace(/^git@github\.com:/, "")
112
+ .replace(/^https?:\/\/github\.com\//, "")
113
+ .replace(/\.git$/, "")
114
+ .replace(/\/+$/, "");
115
+
116
+ if (!/^[^/]+\/[^/]+$/.test(normalized)) {
117
+ console.error(`Could not derive GitHub owner/repo from remote: ${remote}`);
118
+ process.exit(1);
119
+ }
120
+
121
+ process.stdout.write(normalized);
122
+ NODE
123
+ }
124
+
125
+ json_field() {
126
+ node - "$1" <<'NODE'
127
+ const field = process.argv[2];
128
+ const pkg = require("./package.json");
129
+ const value = pkg[field];
130
+
131
+ if (value === undefined || value === null) {
132
+ process.exit(1);
133
+ }
134
+
135
+ process.stdout.write(String(value));
136
+ NODE
137
+ }
138
+
139
+ ruby_string() {
140
+ node - "$1" <<'NODE'
141
+ process.stdout.write(JSON.stringify(process.argv[2] ?? ""));
142
+ NODE
143
+ }
144
+
145
+ formula_class_name() {
146
+ node - "$1" <<'NODE'
147
+ const name = process.argv[2] ?? "";
148
+ const value = name
149
+ .split(/[^A-Za-z0-9]+/)
150
+ .filter(Boolean)
151
+ .map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1))
152
+ .join("");
153
+
154
+ process.stdout.write(value);
155
+ NODE
156
+ }
157
+
158
+ ensure_clean_worktree() {
159
+ if [[ "$ALLOW_DIRTY" == "1" ]]; then
160
+ return
161
+ fi
162
+
163
+ if ! git diff --quiet --ignore-submodules HEAD -- || ! git diff --cached --quiet --ignore-submodules --; then
164
+ echo "Working tree is not clean. Commit or stash changes first, or rerun with --allow-dirty." >&2
165
+ exit 1
166
+ fi
167
+ }
168
+
169
+ wait_for_tap_repo() {
170
+ local repo_url="https://github.com/${TAP_REPO}.git"
171
+ local attempts=12
172
+
173
+ for ((attempt = 1; attempt <= attempts; attempt += 1)); do
174
+ if gh repo view "$TAP_REPO" >/dev/null 2>&1 || git ls-remote "$repo_url" >/dev/null 2>&1; then
175
+ return 0
176
+ fi
177
+
178
+ sleep 2
179
+ done
180
+
181
+ echo "Tap repository did not become available in time: $TAP_REPO" >&2
182
+ exit 1
183
+ }
184
+
185
+ clone_tap_repo() {
186
+ local repo_url="https://github.com/${TAP_REPO}.git"
187
+ run git clone "$repo_url" "$TAP_DIR"
188
+ }
189
+
190
+ ensure_tap_repo() {
191
+ if [[ -d "$TAP_DIR/.git" ]]; then
192
+ info "Using existing tap checkout at $TAP_DIR"
193
+ return
194
+ fi
195
+
196
+ if gh repo view "$TAP_REPO" >/dev/null 2>&1; then
197
+ info "Cloning existing tap repo $TAP_REPO"
198
+ clone_tap_repo
199
+ return
200
+ fi
201
+
202
+ info "Creating tap repo $TAP_REPO"
203
+ run gh repo create "$TAP_REPO" --public --clone=false --description "Homebrew tap for $OWNER tools"
204
+ if [[ "$DRY_RUN" != "1" ]]; then
205
+ wait_for_tap_repo
206
+ fi
207
+ clone_tap_repo
208
+ }
209
+
210
+ ensure_git_tag() {
211
+ if git rev-parse "$TAG" >/dev/null 2>&1; then
212
+ info "Tag $TAG already exists locally"
213
+ else
214
+ info "Creating git tag $TAG"
215
+ run git tag -a "$TAG" -m "Release $TAG"
216
+ fi
217
+
218
+ if git ls-remote --exit-code --tags "$GH_REMOTE" "refs/tags/$TAG" >/dev/null 2>&1; then
219
+ info "Tag $TAG already exists on $GH_REMOTE"
220
+ else
221
+ info "Pushing tag $TAG to $GH_REMOTE"
222
+ run git push "$GH_REMOTE" "$TAG"
223
+ fi
224
+ }
225
+
226
+ ensure_github_release() {
227
+ if gh release view "$TAG" --repo "$SOURCE_REPO" >/dev/null 2>&1; then
228
+ info "GitHub release $TAG already exists"
229
+ return
230
+ fi
231
+
232
+ info "Creating GitHub release $TAG"
233
+ run gh release create "$TAG" \
234
+ --repo "$SOURCE_REPO" \
235
+ --title "$TAG" \
236
+ --generate-notes
237
+ }
238
+
239
+ download_source_archive() {
240
+ ARCHIVE_DIR="$(mktemp -d)"
241
+ ARCHIVE_PATH="$ARCHIVE_DIR/${PACKAGE_NAME}-${VERSION}.tar.gz"
242
+ trap 'rm -rf "$ARCHIVE_DIR"' EXIT
243
+
244
+ info "Downloading source archive $ARCHIVE_URL"
245
+ run curl -LfsS "$ARCHIVE_URL" -o "$ARCHIVE_PATH"
246
+
247
+ if [[ "$DRY_RUN" == "1" ]]; then
248
+ ARCHIVE_SHA256="DRY_RUN_SHA256"
249
+ else
250
+ ARCHIVE_SHA256="$(shasum -a 256 "$ARCHIVE_PATH" | awk '{print $1}')"
251
+ fi
252
+ }
253
+
254
+ write_formula() {
255
+ local formula_dir="$TAP_DIR/Formula"
256
+ local formula_path="$formula_dir/${FORMULA_NAME}.rb"
257
+
258
+ mkdir -p "$formula_dir"
259
+
260
+ cat >"$formula_path" <<EOF
261
+ class ${FORMULA_CLASS} < Formula
262
+ desc ${DESC_RUBY}
263
+ homepage ${HOMEPAGE_RUBY}
264
+ url ${ARCHIVE_URL_RUBY}
265
+ sha256 ${SHA256_RUBY}
266
+
267
+ depends_on "node"
268
+ depends_on "python" => :build
269
+
270
+ def install
271
+ ENV["npm_config_build_from_source"] = "true"
272
+ system "npm", "install", *std_npm_args
273
+ cd libexec/"lib/node_modules/${PACKAGE_NAME}" do
274
+ system "npm", "rebuild", "better-sqlite3"
275
+ end
276
+ bin.install_symlink libexec.glob("bin/*")
277
+ end
278
+
279
+ test do
280
+ output = shell_output(
281
+ "cd #{libexec}/lib/node_modules/${PACKAGE_NAME} && #{Formula["node"].opt_bin}/node -e " \
282
+ "'const Database = require(\"better-sqlite3\"); const db = new Database(\":memory:\"); " \
283
+ "console.log(db.prepare(\"select 1 as value\").get().value); db.close();'"
284
+ )
285
+ assert_equal "1\n", output
286
+ end
287
+ end
288
+ EOF
289
+
290
+ FORMULA_PATH="$formula_path"
291
+ }
292
+
293
+ commit_and_push_tap() {
294
+ local formula_rel_path="Formula/${FORMULA_NAME}.rb"
295
+
296
+ if [[ -z "$(git -C "$TAP_DIR" status --porcelain -- "$formula_rel_path")" ]]; then
297
+ info "Formula already up to date in tap repo"
298
+ return
299
+ fi
300
+
301
+ info "Committing formula update to tap repo"
302
+ run git -C "$TAP_DIR" add "$formula_rel_path"
303
+ run git -C "$TAP_DIR" commit -m "${FORMULA_NAME} ${VERSION}"
304
+ run git -C "$TAP_DIR" push origin HEAD
305
+ }
306
+
307
+ run_brew_audit() {
308
+ if [[ "$SKIP_AUDIT" == "1" ]]; then
309
+ info "Skipping brew audit"
310
+ return
311
+ fi
312
+
313
+ if [[ "$DRY_RUN" == "1" ]]; then
314
+ info "Dry run, skipping brew audit"
315
+ return
316
+ fi
317
+
318
+ if ! command -v brew >/dev/null 2>&1; then
319
+ info "brew not found, skipping audit"
320
+ return
321
+ fi
322
+
323
+ local audit_tap="sqlite-hub/publish-audit"
324
+ local brew_repo
325
+ local audit_owner
326
+ local audit_repo
327
+ local audit_tap_path
328
+ local formula_ref
329
+ local status=0
330
+
331
+ brew_repo="$(brew --repository)"
332
+ audit_owner="${audit_tap%%/*}"
333
+ audit_repo="${audit_tap##*/}"
334
+ audit_tap_path="${brew_repo}/Library/Taps/${audit_owner}/homebrew-${audit_repo}"
335
+ formula_ref="${audit_tap}/${FORMULA_NAME}"
336
+
337
+ mkdir -p "$(dirname "$audit_tap_path")"
338
+ rm -rf "$audit_tap_path"
339
+ ln -s "$TAP_DIR" "$audit_tap_path"
340
+
341
+ info "Running brew audit on generated formula"
342
+ brew audit --strict --formula "$formula_ref" || status=$?
343
+
344
+ rm -f "$audit_tap_path"
345
+
346
+ if [[ "$status" -ne 0 ]]; then
347
+ exit "$status"
348
+ fi
349
+ }
350
+
351
+ require_cmd git
352
+ require_cmd gh
353
+ require_cmd curl
354
+ require_cmd shasum
355
+ require_cmd node
356
+
357
+ ensure_clean_worktree
358
+
359
+ SOURCE_REMOTE_URL="$(git config --get remote.${GH_REMOTE}.url)"
360
+ SOURCE_REPO="$(parse_github_slug "$SOURCE_REMOTE_URL")"
361
+ OWNER="${SOURCE_REPO%%/*}"
362
+ REPO_NAME="${SOURCE_REPO##*/}"
363
+ PACKAGE_NAME="$(json_field name)"
364
+ VERSION="${VERSION_OVERRIDE:-$(json_field version)}"
365
+ DESCRIPTION="$(json_field description)"
366
+ HOMEPAGE="https://github.com/${SOURCE_REPO}"
367
+ TAG="v${VERSION}"
368
+ ARCHIVE_URL="https://github.com/${SOURCE_REPO}/archive/refs/tags/${TAG}.tar.gz"
369
+ TAP_REPO="${TAP_REPO_OVERRIDE:-${OWNER}/homebrew-tap}"
370
+ TAP_DIR="${TAP_DIR_OVERRIDE:-$(cd "$ROOT_DIR/.." && pwd)/homebrew-tap}"
371
+ TAP_OWNER="${TAP_REPO%%/*}"
372
+ TAP_REPO_NAME="${TAP_REPO##*/}"
373
+ BREW_TAP_NAME="${TAP_OWNER}/${TAP_REPO_NAME#homebrew-}"
374
+ FORMULA_NAME="${FORMULA_NAME_OVERRIDE:-$PACKAGE_NAME}"
375
+ FORMULA_CLASS="$(formula_class_name "$FORMULA_NAME")"
376
+ CURRENT_BRANCH="$(git rev-parse --abbrev-ref HEAD)"
377
+
378
+ DESC_RUBY="$(ruby_string "$DESCRIPTION")"
379
+ HOMEPAGE_RUBY="$(ruby_string "$HOMEPAGE")"
380
+ ARCHIVE_URL_RUBY="$(ruby_string "$ARCHIVE_URL")"
381
+
382
+ info "Publishing ${PACKAGE_NAME} ${VERSION}"
383
+ info "Source repo: ${SOURCE_REPO}"
384
+ info "Tap repo: ${TAP_REPO}"
385
+ info "Brew tap: ${BREW_TAP_NAME}"
386
+ info "Tap dir: ${TAP_DIR}"
387
+ info "Formula: ${FORMULA_NAME}"
388
+
389
+ if git ls-remote --exit-code --heads "$GH_REMOTE" "$CURRENT_BRANCH" >/dev/null 2>&1; then
390
+ info "Pushing branch $CURRENT_BRANCH to $GH_REMOTE"
391
+ run git push "$GH_REMOTE" "$CURRENT_BRANCH"
392
+ else
393
+ info "Branch $CURRENT_BRANCH does not exist on $GH_REMOTE yet, pushing it"
394
+ run git push -u "$GH_REMOTE" "$CURRENT_BRANCH"
395
+ fi
396
+
397
+ ensure_git_tag
398
+ ensure_github_release
399
+ download_source_archive
400
+
401
+ SHA256_RUBY="$(ruby_string "$ARCHIVE_SHA256")"
402
+
403
+ if [[ "$DRY_RUN" == "1" ]]; then
404
+ info "Dry run formula preview"
405
+ FORMULA_PATH="${TAP_DIR}/Formula/${FORMULA_NAME}.rb"
406
+ cat <<EOF
407
+ class ${FORMULA_CLASS} < Formula
408
+ desc ${DESC_RUBY}
409
+ homepage ${HOMEPAGE_RUBY}
410
+ url ${ARCHIVE_URL_RUBY}
411
+ sha256 ${SHA256_RUBY}
412
+
413
+ depends_on "node"
414
+ depends_on "python" => :build
415
+
416
+ def install
417
+ ENV["npm_config_build_from_source"] = "true"
418
+ system "npm", "install", *std_npm_args
419
+ cd libexec/"lib/node_modules/${PACKAGE_NAME}" do
420
+ system "npm", "rebuild", "better-sqlite3"
421
+ end
422
+ bin.install_symlink libexec.glob("bin/*")
423
+ end
424
+
425
+ test do
426
+ output = shell_output(
427
+ "cd #{libexec}/lib/node_modules/${PACKAGE_NAME} && #{Formula["node"].opt_bin}/node -e " \
428
+ "'const Database = require(\"better-sqlite3\"); const db = new Database(\":memory:\"); " \
429
+ "console.log(db.prepare(\"select 1 as value\").get().value); db.close();'"
430
+ )
431
+ assert_equal "1\n", output
432
+ end
433
+ end
434
+ EOF
435
+ exit 0
436
+ fi
437
+
438
+ ensure_tap_repo
439
+ write_formula
440
+ run_brew_audit
441
+ commit_and_push_tap
442
+
443
+ info "Published ${FORMULA_NAME} ${VERSION}"
444
+ info "Install with: brew install ${BREW_TAP_NAME}/${FORMULA_NAME}"
package/publish_npm.sh ADDED
@@ -0,0 +1,241 @@
1
+ #!/usr/bin/env bash
2
+
3
+ set -euo pipefail
4
+
5
+ ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
6
+ cd "$ROOT_DIR"
7
+
8
+ DRY_RUN=0
9
+ ALLOW_DIRTY=0
10
+ DIST_TAG=""
11
+ ACCESS=""
12
+ OTP=""
13
+ GH_REMOTE="origin"
14
+
15
+ usage() {
16
+ cat <<'EOF'
17
+ Usage:
18
+ ./publish_npm.sh [options]
19
+
20
+ Options:
21
+ --tag NAME Publish to a custom npm dist-tag instead of `latest`.
22
+ --access LEVEL Forward `--access` to `npm publish` (for scoped packages).
23
+ --otp CODE One-time password for npm 2FA protected publishes.
24
+ --allow-dirty Allow publishing from a dirty worktree.
25
+ --dry-run Build the tarball and run `npm publish --dry-run`.
26
+ --help Show this help text.
27
+
28
+ Requirements:
29
+ - git
30
+ - node
31
+ - npm
32
+
33
+ Notes:
34
+ - A clean worktree is required by default so the published package matches git.
35
+ - In `--allow-dirty` mode, git branch/tag sync is skipped on purpose.
36
+ EOF
37
+ }
38
+
39
+ while [[ $# -gt 0 ]]; do
40
+ case "$1" in
41
+ --tag)
42
+ DIST_TAG="${2:-}"
43
+ shift 2
44
+ ;;
45
+ --access)
46
+ ACCESS="${2:-}"
47
+ shift 2
48
+ ;;
49
+ --otp)
50
+ OTP="${2:-}"
51
+ shift 2
52
+ ;;
53
+ --allow-dirty)
54
+ ALLOW_DIRTY=1
55
+ shift
56
+ ;;
57
+ --dry-run)
58
+ DRY_RUN=1
59
+ shift
60
+ ;;
61
+ --help|-h)
62
+ usage
63
+ exit 0
64
+ ;;
65
+ *)
66
+ echo "Unknown argument: $1" >&2
67
+ usage
68
+ exit 1
69
+ ;;
70
+ esac
71
+ done
72
+
73
+ require_cmd() {
74
+ if ! command -v "$1" >/dev/null 2>&1; then
75
+ echo "Missing required command: $1" >&2
76
+ exit 1
77
+ fi
78
+ }
79
+
80
+ run() {
81
+ if [[ "$DRY_RUN" == "1" ]]; then
82
+ printf '+'
83
+ printf ' %q' "$@"
84
+ printf '\n'
85
+ return 0
86
+ fi
87
+
88
+ "$@"
89
+ }
90
+
91
+ info() {
92
+ printf '==> %s\n' "$*"
93
+ }
94
+
95
+ json_field() {
96
+ node - "$1" <<'NODE'
97
+ const field = process.argv[2];
98
+ const pkg = require("./package.json");
99
+ const value = pkg[field];
100
+
101
+ if (value === undefined || value === null) {
102
+ process.exit(1);
103
+ }
104
+
105
+ process.stdout.write(String(value));
106
+ NODE
107
+ }
108
+
109
+ is_clean_worktree() {
110
+ git diff --quiet --ignore-submodules HEAD -- && git diff --cached --quiet --ignore-submodules --
111
+ }
112
+
113
+ ensure_clean_worktree() {
114
+ if [[ "$ALLOW_DIRTY" == "1" ]]; then
115
+ return
116
+ fi
117
+
118
+ if ! is_clean_worktree; then
119
+ echo "Working tree is not clean. Commit or stash changes first, or rerun with --allow-dirty." >&2
120
+ exit 1
121
+ fi
122
+ }
123
+
124
+ ensure_branch_on_remote() {
125
+ local current_branch
126
+ current_branch="$(git rev-parse --abbrev-ref HEAD)"
127
+
128
+ if [[ "$current_branch" == "HEAD" ]]; then
129
+ info "Detached HEAD detected, skipping branch push"
130
+ return
131
+ fi
132
+
133
+ if git ls-remote --exit-code --heads "$GH_REMOTE" "$current_branch" >/dev/null 2>&1; then
134
+ info "Pushing branch $current_branch to $GH_REMOTE"
135
+ run git push "$GH_REMOTE" "$current_branch"
136
+ else
137
+ info "Branch $current_branch does not exist on $GH_REMOTE yet, pushing it"
138
+ run git push -u "$GH_REMOTE" "$current_branch"
139
+ fi
140
+ }
141
+
142
+ ensure_git_tag() {
143
+ if git rev-parse "$GIT_TAG" >/dev/null 2>&1; then
144
+ info "Tag $GIT_TAG already exists locally"
145
+ else
146
+ info "Creating git tag $GIT_TAG"
147
+ run git tag -a "$GIT_TAG" -m "Release $GIT_TAG"
148
+ fi
149
+
150
+ if git ls-remote --exit-code --tags "$GH_REMOTE" "refs/tags/$GIT_TAG" >/dev/null 2>&1; then
151
+ info "Tag $GIT_TAG already exists on $GH_REMOTE"
152
+ else
153
+ info "Pushing tag $GIT_TAG to $GH_REMOTE"
154
+ run git push "$GH_REMOTE" "$GIT_TAG"
155
+ fi
156
+ }
157
+
158
+ ensure_npm_auth() {
159
+ if [[ "$DRY_RUN" == "1" ]]; then
160
+ info "Dry run, skipping npm auth check"
161
+ return
162
+ fi
163
+
164
+ info "Checking npm auth"
165
+ npm whoami >/dev/null
166
+ }
167
+
168
+ ensure_version_not_published() {
169
+ if [[ "$DRY_RUN" == "1" ]]; then
170
+ info "Dry run, skipping npm registry version check"
171
+ return
172
+ fi
173
+
174
+ if npm view "${PACKAGE_NAME}@${VERSION}" version >/dev/null 2>&1; then
175
+ echo "Version ${PACKAGE_NAME}@${VERSION} is already published on npm." >&2
176
+ exit 1
177
+ fi
178
+ }
179
+
180
+ pack_package() {
181
+ PACK_DIR="$(mktemp -d)"
182
+ trap 'rm -rf "$PACK_DIR"' EXIT
183
+
184
+ info "Packing npm tarball"
185
+ TARBALL_NAME="$(cd "$PACK_DIR" && npm pack "$ROOT_DIR")"
186
+ TARBALL_PATH="$PACK_DIR/$TARBALL_NAME"
187
+ }
188
+
189
+ publish_tarball() {
190
+ local publish_cmd=(npm publish "$TARBALL_PATH")
191
+
192
+ if [[ -n "$DIST_TAG" ]]; then
193
+ publish_cmd+=(--tag "$DIST_TAG")
194
+ fi
195
+
196
+ if [[ -n "$ACCESS" ]]; then
197
+ publish_cmd+=(--access "$ACCESS")
198
+ fi
199
+
200
+ if [[ -n "$OTP" ]]; then
201
+ publish_cmd+=(--otp "$OTP")
202
+ fi
203
+
204
+ if [[ "$DRY_RUN" == "1" ]]; then
205
+ publish_cmd+=(--dry-run)
206
+ fi
207
+
208
+ info "Publishing ${PACKAGE_NAME}@${VERSION} to npm"
209
+ "${publish_cmd[@]}"
210
+ }
211
+
212
+ require_cmd git
213
+ require_cmd node
214
+ require_cmd npm
215
+
216
+ ensure_clean_worktree
217
+
218
+ PACKAGE_NAME="$(json_field name)"
219
+ VERSION="$(json_field version)"
220
+ GIT_TAG="v${VERSION}"
221
+
222
+ info "Preparing npm publish for ${PACKAGE_NAME}@${VERSION}"
223
+
224
+ if is_clean_worktree; then
225
+ ensure_branch_on_remote
226
+ ensure_git_tag
227
+ else
228
+ info "Dirty worktree allowed, skipping git branch/tag sync"
229
+ fi
230
+
231
+ ensure_npm_auth
232
+ ensure_version_not_published
233
+ pack_package
234
+ publish_tarball
235
+
236
+ if [[ "$DRY_RUN" == "1" ]]; then
237
+ info "Dry run completed for ${PACKAGE_NAME}@${VERSION}"
238
+ else
239
+ info "Published ${PACKAGE_NAME}@${VERSION}"
240
+ info "Install with: npm install -g ${PACKAGE_NAME}"
241
+ fi