Skip to content

feat(instance): db upgrade-major — guarded Postgres major upgrade + real detection adapters + matrix reconcile (#129) - #137

Merged
groganz merged 2 commits into
mainfrom
lane/pg-upgrade-major
Jul 13, 2026
Merged

feat(instance): db upgrade-major — guarded Postgres major upgrade + real detection adapters + matrix reconcile (#129)#137
groganz merged 2 commits into
mainfrom
lane/pg-upgrade-major

Conversation

@groganz

@groganz groganz commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

cinatra-cli#129 — cinatra instance db upgrade-major (guarded Postgres major upgrade)

Guarded logical dump → fresh target-major volume → restore for the four Postgres instances (platform / nango / twenty / plane), driven by the supported matrix. This is the command the fail-closed preflight (cli#128) STOP messages point to.

What's here

  • cinatra instance db upgrade-major — matrix eligibility gate (fail-closed on downgrade / unsupported / unknown, before any mutation); the guarded-frame contract model + exit-code contract (0 ok / 2 usage / 3 refusal / 4 interrupted / 5 rolled-back); a dry-run plan by default, --yes executes. The destructive dump/restore/cutover MECHANICS are delegated to the product's guarded shell frame (scripts/upgrade/postgres-upgrade-major.sh, proven with real docker by the works-after upgrade-from arm in the paired PR) with the deployed-version ledger wired in as that frame's UPGRADE_LEDGER_HOOK.
  • Real live-probe + raw PG_VERSION marker adapters (folds in cli#128 residual 1) — the marker reads from the deployment's ACTUAL data path, handling both pg layouts (the pg18 parent-mount move, Change PGDATA in 18+ to /var/lib/postgresql/MAJOR/docker docker-library/postgres#1259).
  • Matrix reconcile to authoritative revision 2 (#1420 / PR #1438 4e89bf4a) — removed a bogus nango general 16→17 hop the shipped copy carried; pinned the reconciled revision (cli#128 residual 3).
  • src/upgrade-ledger-hook.mjs — the ledger transaction wiring: begin/commit/rollback under the per-slug lock, volume-identity-enforced, fail-closed.

Coordinated PAIR with cinatra-ai/cinatra#1422 (the guarded shell mechanism + the works-after upgrade-from arm proving Case A platform 17→18 and Case B nango 15→17). Both pin matrix revision 2; neither is complete without its counterpart. Part of the upgrade-paths epic (cinatra-ai/cinatra#1417 / #1419).

Codex-converged (3 rounds; found + fixed 6 correctness/data-safety issues → MERGE-SAFE).

Not user-visible: a CLI command + detection adapters; no UI surface.

Closes #129

Summary by CodeRabbit

  • New Features

    • Added a guarded instance db upgrade-major command for supported PostgreSQL major-version upgrades.
    • Added dry-run previews and confirmation options, including service, instance, target version, backup location, and JSON output.
    • Added transactional safeguards, verification steps, recovery handling, and volume identity checks during upgrades.
    • Updated upgrade eligibility to fail closed for unsupported or downgrade transitions, including narrowed database migration paths.
  • Tests

    • Added comprehensive coverage for upgrade planning, execution, recovery, detection, ledger behavior, and command discovery.

@coderabbitai

coderabbitai Bot commented Jul 13, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The CLI adds instance db upgrade-major, with matrix-gated version detection, guarded logical dump/restore execution, ledger-backed rollback and interruption handling, Docker volume identity checks, dry-run support, and updated command help.

Changes

PostgreSQL major upgrade workflow

Layer / File(s) Summary
Eligibility contract and upgrade matrix
src/upgrade-major.mjs, src/upgrade-matrix.mjs, tests/upgrade-major.test.mjs, tests/upgrade-matrix*
Supported transitions, mount layouts, matrix revision 2, and fail-closed planning are defined and tested.
Version detection and preflight wiring
src/pg-adapters.mjs, src/index.mjs, tests/pg-adapters.test.mjs
Live server_version probing and layout-aware PG_VERSION reads are wired into service discovery and preflight.
Ledger hook and identity checks
src/upgrade-ledger-hook.mjs, tests/upgrade-ledger-hook.test.mjs
Ledger operations run under locking and verify Docker volume identity across migration boundaries.
Guarded upgrade state machine
src/upgrade-major.mjs, tests/upgrade-major.test.mjs
The upgrade executes ordered quiesce, backup, restore, verification, cutover, commit, rollback, cleanup, and interruption handling, with argument parsing and dry-run previews.
CLI routing, discovery, and help
src/command-table.mjs, src/index.mjs, tests/command-table.test.mjs
The command is surfaced in help, routed by the dispatcher, and connected to instance discovery and the guarded upgrade frame script.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related issues

  • cinatra-ai/cinatra-cli issue 129: Directly covers the new guarded db upgrade-major command and its matrix, ledger, rollback, and failure-handling workflow.
  • cinatra-ai/cinatra issue 1419: Covers the corresponding Postgres upgrade epic, including version ledgers and fail-closed migrations.
  • cinatra-ai/cinatra issue 1417: Covers the existing-deployment Postgres upgrade path and supported migration transitions.
  • cinatra-ai/ops issue 489: Describes the guarded dump, restore, rollback, cutover, and version safeguards implemented here.

Possibly related PRs

Sequence Diagram(s)

sequenceDiagram
  participant CLI
  participant runDbUpgradeMajor
  participant pg-adapters
  participant upgrade-major
  participant upgrade-ledger-hook
  participant DockerVolumes

  CLI->>runDbUpgradeMajor: dispatch db.upgrade-major
  runDbUpgradeMajor->>pg-adapters: probeVersion and readMarker
  pg-adapters->>DockerVolumes: inspect container and PG_VERSION
  runDbUpgradeMajor->>upgrade-major: planTransition
  upgrade-major->>upgrade-ledger-hook: begin migration
  upgrade-major->>DockerVolumes: dump, restore, verify, and cut over
  upgrade-major->>upgrade-ledger-hook: commit or rollback
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: a guarded Postgres major-upgrade command with detection adapters and matrix reconciliation.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch lane/pg-upgrade-major

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (2)
tests/upgrade-ledger-hook.test.mjs (1)

25-31: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Dead cleanup: temp dirs are collected but never removed.

dirs is populated on every ledgerDir() call, but afterEach(() => {}) is a no-op, so the created temp directories accumulate across runs. Either wire up removal or drop the unused bookkeeping.

🧹 Proposed cleanup
-import { mkdtempSync, readFileSync } from "node:fs";
+import { mkdtempSync, readFileSync, rmSync } from "node:fs";
 const dirs = [];
 function ledgerDir() {
   const d = freshDir();
   dirs.push(d);
   return d;
 }
-afterEach(() => {});
+afterEach(() => {
+  while (dirs.length) rmSync(dirs.pop(), { recursive: true, force: true });
+});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/upgrade-ledger-hook.test.mjs` around lines 25 - 31, Fix the dead
temp-directory cleanup in ledgerDir by either removing the unused dirs
collection or implementing afterEach cleanup that removes every directory
recorded during the test. Keep ledgerDir’s freshDir creation behavior unchanged
and ensure cleanup runs after each test.
tests/pg-adapters.test.mjs (1)

65-95: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Coverage looks solid. Note that Line 77 pins the probe argv to -U postgres; if the superuser fix suggested in src/pg-adapters.mjs is adopted, update this assertion and add a case covering a non-postgres superuser (e.g. nango-db).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/pg-adapters.test.mjs` around lines 65 - 95, Update the makeProbeVersion
test assertion to match the superuser argument used by the implementation in
src/pg-adapters.mjs, and add a test case proving probing works when the
configured superuser is a non-postgres value such as nango-db. Keep the existing
running-container, non-running, and exec-failure coverage unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/index.mjs`:
- Line 5727: Update the targetTag assignment to fall back to
`${ctx.plan.to}-alpine` when imageRefTagWithDigest(imageParts(ctx.targetImage))
returns null or another empty value, while preserving the existing fallback when
ctx.targetImage is absent.

In `@src/pg-adapters.mjs`:
- Around line 113-122: Update makeProbeVersion to accept the service-specific
superuser configuration and use it when constructing the dockerExec psql
arguments, defaulting to "postgres" when none is provided. Thread
PG_SERVICE_SUPERUSER from the existing caller in src/index.mjs into the probe
while preserving the current SHOW server_version and parseServerVersionMajor
flow.

In `@src/upgrade-ledger-hook.mjs`:
- Around line 181-184: Update the entrypoint guard around main() in
src/upgrade-ledger-hook.mjs to use a URL-safe comparison, such as
pathToFileURL(process.argv[1]).href against import.meta.url, or the supported
import.meta.main check. Preserve the existing behavior of invoking main() and
exiting with its returned code when executed directly, including paths
containing spaces or escaped characters.

---

Nitpick comments:
In `@tests/pg-adapters.test.mjs`:
- Around line 65-95: Update the makeProbeVersion test assertion to match the
superuser argument used by the implementation in src/pg-adapters.mjs, and add a
test case proving probing works when the configured superuser is a non-postgres
value such as nango-db. Keep the existing running-container, non-running, and
exec-failure coverage unchanged.

In `@tests/upgrade-ledger-hook.test.mjs`:
- Around line 25-31: Fix the dead temp-directory cleanup in ledgerDir by either
removing the unused dirs collection or implementing afterEach cleanup that
removes every directory recorded during the test. Keep ledgerDir’s freshDir
creation behavior unchanged and ensure cleanup runs after each test.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4b2881cb-b27e-44c0-a843-8331f5c8c4f5

📥 Commits

Reviewing files that changed from the base of the PR and between eaca7f6 and 8fe20c0.

📒 Files selected for processing (12)
  • src/command-table.mjs
  • src/index.mjs
  • src/pg-adapters.mjs
  • src/upgrade-ledger-hook.mjs
  • src/upgrade-major.mjs
  • src/upgrade-matrix.mjs
  • tests/command-table.test.mjs
  • tests/pg-adapters.test.mjs
  • tests/upgrade-ledger-hook.test.mjs
  • tests/upgrade-major.test.mjs
  • tests/upgrade-matrix-reconcile.test.mjs
  • tests/upgrade-matrix.test.mjs

Comment thread src/index.mjs
const backupDir = ctx.backupDir ?? mkdtempSync(path.join(os.tmpdir(), `cinatra-pgupgrade-${ctx.service}-`));
const ledgerDir = process.env.CINATRA_VERSION_LEDGER_DIR || defaultLedgerDir();
const hookPath = fileURLToPath(new URL("./upgrade-ledger-hook.mjs", import.meta.url));
const targetTag = ctx.targetImage ? imageRefTagWithDigest(imageParts(ctx.targetImage)) : `${ctx.plan.to}-alpine`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the helper and the target usage, then print the relevant ranges with line numbers.
rg -n "imageRefTagWithDigest|targetTag|--to-tag|targetImage" src/index.mjs

echo '---'
# Print a focused window around the target assignment and nearby usage.
sed -n '5700,5785p' src/index.mjs

echo '---'
# Print the helper definition region.
start=$(rg -n "function imageRefTagWithDigest|const imageRefTagWithDigest" src/index.mjs | head -n1 | cut -d: -f1 || true)
if [ -n "${start}" ]; then
  sed -n "${start},$((start+80))p" src/index.mjs
fi

Repository: cinatra-ai/cinatra-cli

Length of output: 8740


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- serviceImage and imageParts references ---'
rg -n "serviceImage|imageParts\(" src/index.mjs

echo '--- relevant surrounding ranges ---'
# Print around the imageParts helper if present
imgparts_line=$(rg -n "function imageParts|const imageParts" src/index.mjs | head -n1 | cut -d: -f1 || true)
if [ -n "${imgparts_line}" ]; then
  sed -n "${imgparts_line},$((imgparts_line+80))p" src/index.mjs
fi

echo '--- Node spawnSync null arg probe ---'
node - <<'JS'
const { spawnSync } = require('node:child_process');

for (const arg of [null, undefined, 0, false, '']) {
  try {
    const r = spawnSync(process.execPath, ['-e', 'console.log(JSON.stringify(process.argv.slice(2)))', arg], { encoding: 'utf8' });
    console.log('ARG:', arg);
    console.log('status:', r.status);
    console.log('error:', r.error && r.error.message);
    console.log('stdout:', JSON.stringify(r.stdout.trim()));
    console.log('stderr:', JSON.stringify(r.stderr.trim()));
  } catch (e) {
    console.log('ARG:', arg, 'THREW:', e && e.message);
  }
  console.log('---');
}
JS

Repository: cinatra-ai/cinatra-cli

Length of output: 1556


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the ranges where serviceImage is populated and where imageParts is defined.
sed -n '5388,5455p' src/index.mjs
echo '---'
sed -n '5600,5665p' src/index.mjs
echo '---'
sed -n '5750,5795p' src/index.mjs

Repository: cinatra-ai/cinatra-cli

Length of output: 9032


🏁 Script executed:

#!/bin/bash
set -euo pipefail

node - <<'JS'
const { spawnSync } = require('node:child_process');

const args = ['-c', 'for i in "$@"; do printf "<%s>\n" "$i"; done', 'bash', '--to-tag', null, '--backup-dir', 'B'];
const r = spawnSync('bash', args, { encoding: 'utf8' });
console.log('status:', r.status);
console.log('stdout:\n' + r.stdout);
console.log('stderr:\n' + r.stderr);
JS

Repository: cinatra-ai/cinatra-cli

Length of output: 225


🏁 Script executed:

#!/bin/bash
set -euo pipefail

node - <<'JS'
const { spawnSync } = require('node:child_process');

const args = [
  '-c',
  'printf "argc=%s\n" "$#"; i=1; for a in "$@"; do printf "%s=<%s>\n" "$i" "$a"; i=$((i+1)); done',
  'bash',
  '--service',
  'svc',
  '--from',
  '1',
  '--to',
  '2',
  '--from-tag',
  '1-alpine',
  '--to-tag',
  null,
  '--backup-dir',
  'B',
  '--superuser',
  'postgres',
];

const r = spawnSync('bash', args, { encoding: 'utf8' });
console.log('status:', r.status);
console.log(r.stdout);
console.log(r.stderr);
JS

Repository: cinatra-ai/cinatra-cli

Length of output: 341


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- imageParts helper ---'
rg -n "function imageParts|const imageParts|imageParts\s*=" src/index.mjs

echo '--- digest-style image refs in repo ---'
rg -n "`@sha256`:|sha256:" -g '!**/node_modules/**' .

echo '--- compose/image handling around serviceImage ---'
rg -n "serviceImage\.set\(|s\.image|image:" src/index.mjs

Repository: cinatra-ai/cinatra-cli

Length of output: 186


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- all imageParts references in repo ---'
rg -n "imageParts" .

echo '--- imports near top of src/index.mjs ---'
sed -n '1,220p' src/index.mjs

Repository: cinatra-ai/cinatra-cli

Length of output: 10714


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '180,235p' src/upgrade-matrix.mjs
echo '---'
sed -n '1,120p' tests/version-ledger-capture.test.mjs

Repository: cinatra-ai/cinatra-cli

Length of output: 7014


Handle digest-only --to-tag values
At src/index.mjs:5727, imageRefTagWithDigest() can return null for a digest-only image ref, and spawnSync() forwards that as the literal string null. Fall back to ${ctx.plan.to}-alpine when the helper returns nothing.

Proposed change
-    const targetTag = ctx.targetImage ? imageRefTagWithDigest(imageParts(ctx.targetImage)) : `${ctx.plan.to}-alpine`;
+    const targetTag = imageRefTagWithDigest(imageParts(ctx.targetImage)) ?? `${ctx.plan.to}-alpine`;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/index.mjs` at line 5727, Update the targetTag assignment to fall back to
`${ctx.plan.to}-alpine` when imageRefTagWithDigest(imageParts(ctx.targetImage))
returns null or another empty value, while preserving the existing fallback when
ctx.targetImage is absent.

Comment thread src/pg-adapters.mjs
Comment on lines +113 to +122
export function makeProbeVersion({ runningContainerFor, dockerExec }) {
return (service) => {
const container = runningContainerFor ? runningContainerFor(service) : null;
if (!container) return null;
// -tA: tuples-only, unaligned — bare "17.2". `SHOW server_version` is
// cheaper + more robust than parsing verbose version().
const out = dockerExec(container, ["psql", "-U", "postgres", "-tArc", "SHOW server_version"]);
return parseServerVersionMajor(out);
};
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Does the official postgres Docker image create a "postgres" superuser role when POSTGRES_USER is set to a different name?

💡 Result:

Yes, when you set the POSTGRES_USER environment variable to a different name, the official PostgreSQL Docker image creates that user as a superuser [1][2]. According to the official documentation, the POSTGRES_USER variable is used to define the database superuser [1][2]. If this variable is specified, the image creates that user with superuser privileges and automatically creates a database with the same name [1][2]. If POSTGRES_USER is not defined, the image defaults to creating the 'postgres' user as the superuser [1][2]. It is important to note that this behavior only applies when the container is initialized for the first time with an empty data directory [3]. If a pre-existing data directory is mounted to the container, the environment variables will not modify the existing database's users or roles [3]. Additionally, this configuration is distinct from the Linux system user (also named 'postgres') that runs the PostgreSQL daemon process inside the container [1][2].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the relevant files and nearby context.
git ls-files src/pg-adapters.mjs src/index.mjs
echo "---- src/pg-adapters.mjs ----"
sed -n '1,220p' src/pg-adapters.mjs
echo "---- src/index.mjs ----"
sed -n '1,260p' src/index.mjs

# Search for probe/version wiring and any superuser plumbing.
echo "---- search ----"
rg -n "makeProbeVersion|PG_SERVICE_SUPERUSER|superuserFor|server_version|psql -U" src

Repository: cinatra-ai/cinatra-cli

Length of output: 19621


Thread the service superuser into the live probe

makeProbeVersion() hardcodes psql -U postgres, but src/index.mjs already tracks per-service superusers via PG_SERVICE_SUPERUSER (nango-db → nango, plane-db → plane). For those services, the live probe can’t authenticate, so it falls back to the raw marker every time.

Pass the service-specific superuser into the probe (defaulting to postgres) so the SHOW server_version path works for all supported services.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pg-adapters.mjs` around lines 113 - 122, Update makeProbeVersion to
accept the service-specific superuser configuration and use it when constructing
the dockerExec psql arguments, defaulting to "postgres" when none is provided.
Thread PG_SERVICE_SUPERUSER from the existing caller in src/index.mjs into the
probe while preserving the current SHOW server_version and
parseServerVersionMajor flow.

Comment on lines +181 to +184
// Executed directly by the shell frame (never imported there): run main + exit.
if (import.meta.url === `file://${process.argv[1]}`) {
main().then((code) => process.exit(code));
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

node import.meta.url equals file protocol process.argv[1] pathToFileURL idiom

💡 Result:

In Node.js, the pattern of comparing process.argv[1] to the file path of the current module is the historical idiom used to determine if a module is being executed directly as the entry point (the ESM equivalent of the CommonJS require.main === module check) [1][2][3]. Recent versions of Node.js have introduced more direct, built-in ways to achieve this, reducing the need for the fileURLToPath idiom [4]. Current methods for detecting the main module: 1. import.meta.main (Recommended): Available since Node.js v24.2.0 and v22.18.0. This is the modern, idiomatic way to check if a module is the main entry point [4]. if (import.meta.main) { // Module is being run directly } 2. import.meta.filename: Available since Node.js v20.11.0. You can compare this directly to process.argv[1] [4]. if (process.argv[5] === import.meta.filename) { // Module is being run directly } 3. The legacy idiom: For older versions of Node.js, developers use fileURLToPath from the node:url module to convert import.meta.url into a file system path, which is then compared to process.argv[1] [6][2][4]. import { fileURLToPath } from 'node:url'; if (process.argv[5] === fileURLToPath(import.meta.url)) { // Module is being run directly } Note that the legacy idiom has known limitations, such as potential issues with symbolic links or cases where the script is executed without its file extension [7][8][6]. For robust production code, using the newer built-in properties (import.meta.main) is preferred [4].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## locate references\n'
rg -n --hidden --glob '!node_modules' --glob '!dist' --glob '!build' --glob '!coverage' 'upgrade-ledger-hook\.mjs|upgrade ledger hook|ledger-hook' .

printf '\n## file outline\n'
ast-grep outline src/upgrade-ledger-hook.mjs --view expanded || true

printf '\n## relevant lines around guard\n'
nl -ba src/upgrade-ledger-hook.mjs | sed -n '150,210p'

Repository: cinatra-ai/cinatra-cli

Length of output: 1323


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## guard in src/upgrade-ledger-hook.mjs\n'
sed -n '175,190p' src/upgrade-ledger-hook.mjs | cat -n

printf '\n## spawn site in src/index.mjs\n'
sed -n '5716,5745p' src/index.mjs | cat -n

printf '\n## tests referencing hook execution\n'
sed -n '1,220p' tests/upgrade-ledger-hook.test.mjs | cat -n

Repository: cinatra-ai/cinatra-cli

Length of output: 12122


🌐 Web query:

Node ESM import.meta.url symlink realpath process.argv[1] direct execution guard

💡 Result:

To determine if an ES module is being executed directly in Node.js, the most authoritative and modern approach is using the import.meta.main property [1][2][3]. ### Preferred Approach: import.meta.main Starting in recent Node.js versions (e.g., v24.2.0+), import.meta.main provides a boolean indicating whether the module is the entry point of the current process [3]. if (import.meta.main) { // Executed directly } ### Robust Legacy / Compatibility Approach If you need to support older environments, or if you encounter issues with symlinks where process.argv[1] might point to a symlink path while import.meta.url resolves to the real path, use import.meta.filename or a manual comparison with realpathSync [4][5][1]. 1. Using import.meta.filename (Node.js v20.11.0+): import.meta.filename is the absolute path to the current module with symlinks resolved [4][5]. if (process.argv[4] === import.meta.filename) { // Executed directly } 2. Manual symlink-safe comparison: If import.meta.filename is unavailable, compare the file URL of the resolved process.argv[1] against import.meta.url [1][3]. import { fileURLToPath, pathToFileURL } from 'node:url'; import { realpathSync } from 'node:fs'; const isDirect = import.meta.url === pathToFileURL(realpathSync(process.argv[4])).href; if (isDirect) { // Executed directly } ### Key Considerations * Symlinks: import.meta.filename automatically resolves symlinks, making it highly reliable for identifying the file path [4][5]. Using realpathSync(process.argv[1]) ensures that you are comparing the actual target file path against the entry point, preventing mismatches caused by symlinks (e.g., in node_modules/.bin) [1][3]. * Command Line Arguments: process.argv[1] is the standard way to identify the entry module path, but relying on it directly can be fragile without resolving the path to its real canonical form [1][6]. * Comparison: Always convert path strings to file URLs before comparing them to import.meta.url to ensure the protocols and formatting match correctly [1][7][8].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## UPGRADE_LEDGER_HOOK references\n'
rg -n --hidden --glob '!node_modules' --glob '!dist' --glob '!build' --glob '!coverage' 'UPGRADE_LEDGER_HOOK|upgrade-ledger-hook' src scripts tests .

printf '\n## shell frame candidates\n'
fd -t f 'upgrade' .
fd -t f 'postgres-upgrade-major.sh' .

Repository: cinatra-ai/cinatra-cli

Length of output: 2341


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## search for hook execution\n'
rg -n --hidden --glob '!node_modules' --glob '!dist' --glob '!build' --glob '!coverage' 'UPGRADE_LEDGER_HOOK|execFileSync|spawnSync|spawn\(' src/upgrade-major.mjs src/index.mjs scripts tests

printf '\n## relevant section of src/upgrade-major.mjs\n'
sed -n '430,540p' src/upgrade-major.mjs | cat -n

Repository: cinatra-ai/cinatra-cli

Length of output: 21150


Use a URL-safe entrypoint check in src/upgrade-ledger-hook.mjs
file://${process.argv[1]} is brittle here: a checkout path with spaces or other escaped characters makes the comparison false, so the hook skips main() and exits 0. Use pathToFileURL(process.argv[1]).href or import.meta.main so the ledger hook still runs.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/upgrade-ledger-hook.mjs` around lines 181 - 184, Update the entrypoint
guard around main() in src/upgrade-ledger-hook.mjs to use a URL-safe comparison,
such as pathToFileURL(process.argv[1]).href against import.meta.url, or the
supported import.meta.main check. Preserve the existing behavior of invoking
main() and exiting with its returned code when executed directly, including
paths containing spaces or escaped characters.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant