# npmglobalize

Transform `file:` dependencies to npm versions for publishing.

## Overview

`npmglobalize` automates the workflow of publishing npm packages that use local `file:` references during development. It converts those references to proper npm versions, publishes everything in dependency order, and optionally restores the local references afterward.

## Installation

```bash
npm install -g @bobfrankston/npmglobalize
```

## Basic Usage

```bash
cd your-package
npmglobalize              # Transform + publish (patch version)
npmglobalize --minor      # Bump minor version
npmglobalize --major      # Bump major version

# Or run from anywhere with a path
npmglobalize y:\path\to\your-package
```

## Key Features

### 🔗 Automatic Dependency Publishing (Default)

By default, `npmglobalize` ensures all `file:` dependencies are published **before** converting them:

```bash
npmglobalize              # Auto-publishes file: deps in correct order
```

If you have:
```
lxtest
├── file:../lxlan-node
│   └── file:../lxland
└── file:../lxland
```

It automatically:
1. Publishes `lxland` (root dependency)
2. Publishes `lxlan-node` (depends on lxlan)
3. Converts and publishes `lxtest`

**Settings Propagation (Default Behavior):**
When publishing `file:` dependencies, these settings are **automatically inherited**:
- `--update-deps` / `--update-major` (update dependencies)
- `--fix` (run npm audit fix)
- `--conform` (fix .gitignore/.npmignore/.gitattributes and git config)
- `--verbose` / `--quiet`
- `--force` / `--files`

**⚠️ Visibility Settings (Smart Inheritance):**
- `--npmVisibility` is **only inherited by NEW repositories** (never published to npm before)
- **Existing repositories** keep their current npm visibility (public/private) unchanged
- `--gitVisibility` is inherited by all dependencies

This ensures you can safely set `--npmVisibility private` as a default for new packages without accidentally changing the visibility of your existing published packages.

**Why This Matters:**
Once a package is published to npm (public or private), changing its visibility later requires careful consideration. This smart inheritance protects your existing packages while making new ones default to safe settings.

Example - safely publish with new packages private by default:
```bash
npmglobalize --npmVisibility private
# ✓ Existing npm packages: keep their current visibility
# ✓ New packages (never published): default to private (safe!)
# ✓ Regular npm dependencies (express, etc.): unchanged
```

Example with configuration file (recommended):
```bash
# In main package: create .globalize.json5 with "npmVisibility": "private"
npmglobalize
# ✓ Existing repos: publish with their current npm visibility
# ✓ Brand new repos: inherit private setting
```

**Skip auto-publishing** (use with caution):
```bash
npmglobalize -npd         # --no-publish-deps
```

### 🔍 Prescan (Default)

Before any transform/publish, `npmglobalize` walks the full `file:` dep graph and reports all problems up front (unresolvable paths, missing `package.json`, unscoped packages with private intent, etc.). This lets you fix the whole punch list at once rather than being interrupted mid-cascade.

Errors abort (unless `--force`); warnings prompt to continue.

Skip the prescan with `-no-prescan` / `-nps`.

#### Workspace skip prescan

In workspace mode, before processing begins, each package is also checked to decide whether it actually needs work. A package is **skipped** (no rebuild, no version bump, no publish) only if **all** of these are true:

1. **Working tree clean** — `git status --porcelain .` reports no uncommitted changes for that package's directory.
2. **Version already on npm** — the version in its `package.json` is published to the registry, and no non-bookkeeping commit (anything outside "Pre-release commit" / "Restore file: dependencies" / "Pre-version cleanup" / "Untrack node_modules") is newer than that version's publish timestamp.
3. **Build is fresh** — every `.ts` source file (excluding `.d.ts`) has a sibling `.js` whose mtime is ≥ the `.ts` mtime. A missing `.js` or an older `.js` counts as stale.
4. **No sibling workspace `file:` dep flagged for work** — if another workspace package depends on this one via `file:`, and that dep is being updated in this run, this one is also processed (propagates through the graph in topological order).

If any condition fails, the package is processed. The prescan prints one line per package with `⟳` for "will process" (and the reason) or `✓` for "skip". Example:

```
⟳ mlproc     — uncommitted changes
⟳ pzip       — stale build (index.ts newer than index.js)
⟳ stage      — dep pzip is being updated
✓ puller     — skip (clean, published, build fresh)
```

The summary row for a skipped package shows `– name v1.0.0 (skipped — already up to date)`.

Use `--force` or `--force-publish` to bypass the skip prescan and process every package.

**Force republish** all file: dependencies even if versions exist:
```bash
npmglobalize --force-publish
```

### 📦 Dependency Updates

**Safe updates** (minor/patch only, respects semver):
```bash
npmglobalize --update-deps
```
- `express ^4.18.0` → `^4.21.0` ✓
- `lodash ^4.17.0` → `^4.17.21` ✓  
- Won't update to `express ^5.0.0` (breaking change)

**Include major updates** (breaking changes):
```bash
npmglobalize --update-major
```
- Updates to latest including major versions
- Shows "(MAJOR)" indicator for breaking changes

**Note:** The `--update-deps` flag propagates to all file: dependencies, so one command updates your entire dependency tree.

### 🔒 Security Auditing

**Check vulnerabilities**:
```bash
npmglobalize              # Shows audit at end
```

**Auto-fix vulnerabilities**:
```bash
npmglobalize --fix        # Runs npm audit fix
```

**Disable audit**:
```bash
npmglobalize --no-fix
```

### 🧩 Install Scripts (npm `allowScripts`)

npm 11.17+ skips install-time lifecycle scripts (`preinstall`/`install`/
`postinstall`) for packages that aren't on an allowlist. A **global** install
has no project `package.json` to record approvals in, so without help a
postinstall you wrote yourself silently never runs while the install still
exits 0 — e.g. `@bobfrankston/msger`'s postinstall copies its native binary
into the per-user bin dir its launcher reads from, and skipping it leaves a
stale exe behind a green checkmark.

npmglobalize therefore allowlists **your own packages** on the global install
it performs — it just built and published them from your source, so they're
trusted — and leaves third-party packages gated:

```
> npm install -g @bobfrankston/winpos@2.0.51 --allow-scripts @bobfrankston/winpos,@bobfrankston/msger,@bobfrankston/msgcommon
```

Anything npm skips is reported rather than buried in the captured output:

```
  · Skipped koffi@2.16.3 install script — prebuilt-binary fetcher, the package ships binaries; normally harmless.
  ⚠ npm skipped install scripts for third-party packages: sharp@0.33.0
      sharp@0.33.0 (install: node install/check)
    These may be broken at runtime — no prebuilt fallback recognized.
    To run them anyway: npm install -g @bobfrankston/winpos@2.0.51 --allow-scripts @bobfrankston/winpos,@bobfrankston/msger,koffi,sharp
```

Two details worth knowing:

- **Prebuilt fetchers are called out separately.** npm names the skipped
  script, so a runner like `cnoke`, `prebuild-install`, `node-gyp-build`,
  `prebuildify` or `napi-postinstall` is recognized as one that only compiles
  when no prebuild matches. Those packages ship working binaries — koffi
  carries `build/koffi/<platform>/koffi.node` for every platform it supports —
  so the skip gets an informational note, not a warning.
- **The suggested re-run command includes your own packages.** npm takes the
  allowlist from the first source that has one rather than merging sources, so
  a command naming only the third-party package would silently re-gate yours.
  Always run the full list.

An own-scope package showing up as skipped is reported louder — that means the
allowlist missed it, usually a transitive dep not present in the local
`node_modules` tree.

### 🔑 OAuth Credentials Handling

`npmglobalize` automatically detects `credentials.json` files and handles them based on OAuth app type:

- **Desktop/installed apps** (`"installed"` key in JSON): The `client_secret` is just a public app registration ID, not a real secret. Google's own docs state: *"the client_secret is obviously not treated as a secret."* These files are kept in the repo — `!credentials.json` is added to `.gitignore`/`.npmignore` to override any broader ignore patterns.

- **Web apps** (`"web"` key in JSON): The `client_secret` is a real secret. These files are automatically added to `.gitignore`/`.npmignore` to prevent accidental exposure.

This distinction also drives the **push protection auto-bypass**: when GitHub blocks a push because it detects an OAuth client secret, `npmglobalize` checks the credential file type. For installed apps, it auto-bypasses (marking as `false_positive`) since the credential is public by design.

### 🔄 File Reference Management

**Default behavior** (restore file: references after publish):
```bash
npmglobalize              # Converts file: → npm, publishes, then restores file:
```

**Keep npm references** permanently:
```bash
npmglobalize --nofiles    # Don't restore file: references
```

**Just transform** without publishing:
```bash
npmglobalize -np          # --nopublish (formerly --apply)
```

**Restore** from backup:
```bash
npmglobalize --cleanup    # Restore original file: references
```

### 📝 Release Notes via `.commitmsg`

For multi-line or reusable release notes, write them to a `.commitmsg` file in the package root instead of passing them on the command line:

```bash
cat > .commitmsg <<'EOF'
Added foo feature
Fixed bar regression
EOF
npmglobalize
```

Behavior:
- If `-m` / `-message` is **not** given and `.commitmsg` exists, its contents are used as the commit message (and force a release even if the working tree is otherwise clean).
- After a successful `npm publish`, npmglobalize:
  1. Appends the contents to `npmchanges.md` under a `## v<version> — <YYYY-MM-DD>` header (creating the file if needed)
  2. Deletes `.commitmsg`
  3. Commits both changes as `Log v<version> to npmchanges.md` and pushes

Notes:
- **Git/GitHub only.** npm publish does not consume git commit messages; `npmchanges.md` lives in the git repo and on GitHub but is excluded from the published npm tarball (the standard `*.md` rule keeps only `README.md`).
- If both `-m` and `.commitmsg` are present, `-m` wins and `.commitmsg` is left alone (not consumed).
- If publish fails, `.commitmsg` is preserved for the next attempt.
- `.commitmsg` is auto-added to `.npmignore` (security pattern) so it never leaks into the tarball.

### 🔧 Git Integration & Error Recovery

**Automatic tag conflict resolution**:
```bash
npmglobalize --fix-tags   # Auto-fix version/tag mismatches
```

When a previous publish fails, git tags may conflict with package.json version. The `--fix-tags` option (or automatic detection) will clean up these conflicts.

**Automatic rebase** when local is behind remote:
```bash
npmglobalize --rebase     # Auto-rebase if behind remote
```

For single-developer projects, this safely pulls remote changes before publishing.

**Failed publish recovery**: If a previous run bumped the version locally but the publish or push failed (e.g., network error, push protection), `npmglobalize` detects this automatically on the next run. It checks whether the current version actually exists on npm — if not, it republishes without re-bumping the version. Unpushed commits from a failed push are also pushed automatically.

**GitHub Push Protection (GH013)**: When GitHub's secret scanning blocks a push, `npmglobalize` detects the error and checks whether the flagged secrets are actually safe. For Google OAuth desktop/installed app credentials (where the "client_secret" is just a public app identifier, not a real secret), it automatically bypasses push protection via the GitHub API (`gh` CLI required) and retries the push. For other secret types, it displays the unblock URLs with guidance.

**Note:** This tool is designed for single-developer, single-branch workflows where automatic rebase and tag cleanup are safe operations.

### 📂 Git Repository Setup

**Change visibility of an existing repo:**
```bash
npmglobalize -git public     # Makes the GitHub repo public (with confirmation)
npmglobalize -git private    # Makes the GitHub repo private
```

### Missing local `.git` (adopt vs fresh init)

When npmglobalize runs in a directory with no `.git`, it first checks for a
reachable `repository.url` in `package.json`. If found, it offers two paths:

```
How would you like to set up git?
  1) Adopt history from existing remote (recommended)
  2) Initialize fresh git repository
  a) Adopt ALL (don't ask again for remaining deps)
  3) Use local install only (skip git/publish)
  4) Abort
```

**Adopt** runs:

```bash
git init
git remote add origin <package.json repository.url>
git fetch origin
# Point HEAD at origin/<defaultBranch> without touching the working tree
git update-ref refs/heads/<branch> origin/<branch>
git symbolic-ref HEAD refs/heads/<branch>
git reset                       # mixed: refresh index, keep working tree
```

After adoption, your local files appear as **uncommitted changes on top of the
remote's HEAD** — `git status` shows the drift, and npmglobalize's normal flow
will commit them on the next publish. **No force-push is required**, and the
remote's history is preserved.

If the remote is not reachable (no `repository` field, network/auth failure,
deleted repo), the original "Initialize fresh git repository" prompt is shown
instead.

CLI flags:

- `-init` — auto path; adopts if a reachable remote is found, otherwise falls
  back to fresh `git init` + `gh repo create`.
- `-adopt` — **strict**: aborts if no reachable remote in `package.json.repository`.
  Use this when you want to be sure no new GitHub repo is created (e.g. in
  scripts, or when re-attaching a tree of packages to existing repos).

When initializing a new repository with `--init`, npmglobalize automatically sets up:

**File structure** (per programming.md standards):
- `.gitignore` - Node.js best practices (node_modules, secrets, certificates, etc.)
- `.npmignore` - Publishing filters (excludes .git, tests, source files, etc.)
  - **noEmit projects:** `*.ts`, `*.map`, and `tsconfig.json` are kept (not ignored) since TS files are the runtime files
- `.gitattributes` - Forces LF line endings for all text files

**Git configuration** (ensures cross-platform compatibility):
```bash
git config core.autocrlf false  # Disable CRLF conversion
git config core.eol lf          # Force LF line endings
```

This ensures consistent line endings across Windows, macOS, and Linux, preventing "modified file" issues caused by line ending differences.

### 🔍 Understanding "Detached HEAD" Error

**What is Detached HEAD?**
Your git repository is not currently on a branch (like `master` or `main`). This happens when you:
- Check out a specific commit: `git checkout abc123`
- Check out a tag: `git checkout v1.0.0`
- Have some git operations leave you in this state

**Why does it matter?**
Publishing requires being on a branch so commits and tags can be properly tracked in your repository history.

**Common scenarios:**

1. **Just fixing files with `--conform`:**
   ```bash
   npmglobalize --conform   # ✓ Files get fixed, then exits with helpful message
   ```
   The files are already updated! You don't need to run it again.

2. **Want to publish (have commits to keep):**
   ```bash
   git checkout -B master   # Moves master branch to current commit (merges)
   npmglobalize            # Now works normally
   ```
   The `-B` flag moves your branch pointer to include the detached commits.

3. **Want to publish (no commits made, safe to discard):**
   ```bash
   git checkout master      # Just switch back to branch
   npmglobalize            # Now works normally
   ```

4. **Force publish anyway (not recommended):**
   ```bash
   npmglobalize --force    # Proceeds despite detached HEAD
   ```
   Warning: Commits may be hard to track later.

## Command Reference

<!-- NOTE: Keep this in sync with the -help output in cli.ts printHelp().
     The README expands on options with examples and context;
     cli.ts is the concise quick-reference. Update both when adding/changing flags.
     Both -flag and --flag are accepted; single-dash is shown as primary. -->

### Release Options
```
-patch               Bump patch version (default)
-minor               Bump minor version
-major               Bump major version
-nopublish, -np      Just transform, don't publish (persisted to config)
-cleanup             Restore file: dependencies from .dependencies backup
-m, -message <msg>   Custom commit message (forces release even without changes)
                     If -m not given, a `.commitmsg` file (if present) is used instead.
                     See "Release Notes via .commitmsg" below.
```

### Dependency Options
```
-update-deps, -ud        Update package.json to latest versions (safe: minor/patch)
-update-major            Allow major version updates (breaking changes)
-publish-deps            Auto-publish file: dependencies (default)
-pd                      Like -publish-deps, plus auto-yes to dep-cascade prompts (private only)
-no-publish-deps, -npd   Skip auto-publishing file: dependencies
-no-prescan, -nps        Skip upfront dep-graph prescan
-force-publish           Republish dependencies even if version exists
-fix                     Run npm audit fix after transformation
-no-fix                  Don't run npm audit
-no-use-paths, -nup      Declare package standalone; do not resolve file: deps
                         from sibling checkouts (see Configuration File)
```

### Install Options
```
-install, -i    Install globally after publish (from registry)
-link           Install globally via symlink (npm install -g .)
-local          Local install only — skip transform/publish, just npm install -g .
-wsl            Also install in WSL
-once           Don't persist flags to .globalize.json5
```

### Mode Options
```
-files          Keep file: paths after publish (default)
-nofiles        Keep npm versions permanently
```

### Git/npm Visibility
```
-git private    Set GitHub repo to private (default for new repos)
-git public     Set GitHub repo to public (requires confirmation)
                Works on both new and existing repos
-npm <values>   Comma-separated list of npm options:
                  private (default) | public — package visibility
                  ts                         — keep .ts source (and *.map,
                                               tsconfig.json) in npm tarball
                  nts                        — exclude .ts source
                                               (default for non-noEmit projects)
                Example: -npm public,ts
```

`ts` is already the default on git (source is tracked). Pass `-npm ts` to ship
the same files to npm — useful for debugging installed packages or "source on
demand" packages. `noEmit` projects automatically ship `.ts` files (they *are*
the runtime); pass `-npm nts` to override. `allowTs` persists to
`.globalize.json5`.

### Workspace Options
```
-w, -workspace <pkg>   Filter to specific package (repeatable)
-no-workspace          Disable workspace mode at a workspace root
-continue-on-error     Continue if a package fails in workspace mode
```

Workspace mode is auto-detected when run from a root with `"private": true` and a `workspaces` field.

### Other Options
```
-init           Initialize git/npm if needed (creates .gitignore, .npmignore,
                .gitattributes, and configures git for LF line endings).
                If package.json.repository.url is reachable, adopts its
                history instead of creating a fresh repo.
-adopt          Strict adopt: require a reachable git remote in
                package.json.repository. Abort if probe fails. Skips prompt.
-strict-imports, -import-check
                Opt in to scanning .ts/.js source for imports of packages not
                declared in any dependencies bucket. Off by default — the
                always-declare style this enforces doesn't hold in monorepos
                that rely on workspace cross-refs or the -public-deps cascade
                (those produce noisy prompts that get dismissed reflexively,
                which defeats the safety purpose). Use only on packages where
                every import is meant to be a direct package.json declaration.
                When it does fire, it catches silent runtime failures —
                ERR_MODULE_NOT_FOUND on a clean install of a published tarball
                that was resolving via an ambient parent/global node_modules
                at dev time.
-force          Continue despite git errors
-dry-run        Preview what would happen
-quiet          Suppress npm warnings (default)
-verbose        Show detailed output
-conform        Update .gitignore/.npmignore/.gitattributes to best practices
                and configure git for LF line endings (fixes existing repos)
                For noEmit projects: removes *.ts/*.map/tsconfig.json from .npmignore
-asis           Skip ignore file checks (or set "asis": true in .globalize.json5)
-fix-tags       Automatically fix version/tag mismatches
-rebase         Automatically rebase if local is behind remote
-clean-nested-modules, -clean-nested
                Before npm pack, wipe node_modules/ inside each file: dep
                target. Fixes arborist "Cannot read properties of null"
                crashes caused by sibling file: deps with nested
                node_modules. Suggested automatically when the error hits.
-ts7-report, -deprecation-report
                Report-only: scan this package and its file: deps for
                compilerOptions removed in TypeScript 7 and list a migration
                to-do. Writes nothing.
-tsfix, -ts7-fix
                One-off utility, NOT part of the main flow: apply the TS7
                tsconfig migration to a package (and its file: deps) and exit —
                no build, no commit, no push, no publish. Intended as a
                temporary tool for fixing a local/subdirectory tsconfig in
                place. The normal release flow already applies the same
                migration automatically when a build hits a TS7 deprecation
                error, so day-to-day you never need this flag.
                    npmglobalize <path> -tsfix
-show           Show package.json dependency changes
-package, -pkg  Update package.json scripts to use npmglobalize (see below)
-h, -help       Show help
-v, -version    Show version
```

## Using in package.json

You can wire npmglobalize into your package.json `scripts` so that `npm run release` handles publishing:

```json
{
  "scripts": {
    "release": "npmglobalize"
  }
}
```

The `--package` (`-pkg`) flag does this automatically — it adds a `release` script (renaming any existing `release`/`installer` scripts to `old-release`/`old-installer`):

```bash
npmglobalize --package       # Sets up "release": "npmglobalize" in package.json
```

After that, publishing is just:
```bash
npm run release              # Same as running npmglobalize directly
npm run release -- --minor   # Pass flags through
```

You can also combine it with `.globalize.json5` for persistent options so `npm run release` always uses your preferred settings (install, visibility, etc.).

## Configuration File

Settings can be saved in `.globalize.json5`:

```json5
{
  // npmglobalize configuration (JSON5 format)
  "bump": "patch",           // Version bump type
  "install": true,           // Auto-install globally
  "wsl": false,              // Also install in WSL
  "fix": true,               // Auto-run npm audit fix
  "verbose": false,          // Show detailed output
  "gitVisibility": "private",
  "npmVisibility": "public",
  "usePaths": true           // Resolve file: deps from sibling checkouts (see below)
}
```

Configuration persists across runs. CLI flags override config file.

### `usePaths` — Standalone packages

Default: `true`. Set to `false` (or pass `-no-use-paths` / `-nup`) to mark a
package as **standalone** — one that should be publishable/installable on a
machine that does not have sibling `file:` dep checkouts available. Example:
a backup/recovery utility you want to `npm install -g` on any host.

Currently this setting is **declarative** — it is parsed, persisted to
`.globalize.json5`, and surfaced in the settings banner, but the tool does
not yet change its behavior based on it. Behavior wiring (e.g. resolving
`file:` deps to the latest published npm version instead of walking siblings)
is planned.

### `upstream` — who consumes this package

**Experimental.** Bookkeeping, not a setting. When a package with `file:` deps
publishes, npmglobalize appends an entry to each **dependency's**
`.globalize.json5`:

```json5
{
  "install": true,

  // FYI: packages that depend on this one (immediate consumers,
  //      recorded when each of them publishes). Nothing is updated
  //      automatically; follow each path's own .globalize.json5 to
  //      walk further out.
  "upstream": [
    {"path":"Y:\\dev\\utils\\winpos","version":"2.0.51","updated":"2026-08-09"},
  ],
}
```

So publishing `winpos` (which has `"@bobfrankston/msger": "file:../msgx/msger"`)
records winpos in **msger's** config. Each entry carries the consumer's
checkout path, its version at the time, and the date.

Only **immediate** consumers are recorded. A full consumer tree is a walk, not
a copy: follow each entry's path and read that package's own `upstream` list.
That keeps each file small and self-maintaining — no package has to know about
anything beyond its own direct consumers.

The list is **FYI** — it is recorded, preserved across publishes, and printed
in the Release Summary of the package that owns it. Nothing is rebuilt,
republished, or reinstalled on its behalf. An entry appears the first time a
consumer publishes, and is refreshed in place on every publish after that.

#### Scope and limits

**`file:` deps only.** A dependency referenced by npm version (`"^0.1.39"`)
is never recorded, because npmglobalize has no checkout path for it — it
consumes the published tarball, not a sibling directory. To have a consumer
show up in a library's list, that consumer must reference it as
`file:../<lib>`.

**Usually not committed.** The entry is written into the dependency's own
checkout, and npmglobalize then tries to commit it there
(`Record upstream <consumer>@<version>`) and push if that repo has a remote.
In practice that commit is usually skipped: `.globalize.json5` is in the
standard ignore template, so most repos ignore it and the entry stays as
untracked local state. That is a reasonable failure mode for an experimental
mechanism — the list rewrites itself on every publish, so there is nothing to
merge and nothing to reconcile between machines. It also means the list is
**per-machine**, not shared history.

When the commit does happen (a repo that tracks its `.globalize.json5`, as
npmglobalize itself does), only that one file is staged and committed by
pathspec — anything else the dependency had staged or modified is left exactly
as it was. A dependency that isn't a git repo is written and skipped the same
way.

The alternative would be recording this in `package.json`, which is tracked
and published — every consumer of a library would then download that library's
list of local checkout paths in its tarball. Keeping it in `.globalize.json5`
keeps it out of the package entirely.

## Common Workflows

### Standard Release
```bash
npmglobalize --install    # Publish + install globally
```

### Release with Dependency Chain
```bash
cd my-app                  # Has file: deps
npmglobalize              # Publishes all deps automatically
```

### Safe Dependency Updates
```bash
npmglobalize --update-deps  # Update to latest safe versions
```

### Security Fixes
```bash
npmglobalize --fix         # Fix vulnerabilities + release
```

### Force Update Everything
```bash
npmglobalize --force-publish --update-major
```

### Preview Changes
```bash
npmglobalize --dry-run     # See what would happen
```

## How It Works

1. **Validates** package.json and git status
2. **Checks** if current version is on npm (recovers from failed publishes)
3. **Updates dependencies** (if `--update-deps`)
4. **Builds `file:` deps in topological order**, then the target itself, so consumers' `tsc` reads up-to-date `.d.ts` from sibling checkouts whose source has changed (see [Build Cascade](#build-cascade))
5. **Publishes file: dependencies** (if needed)
6. **Backs up** original file: references to `.dependencies`
7. **Converts** `file:` → npm version references
8. **Commits** changes
9. **Bumps** version (using npm version) — skipped if recovering a failed publish
10. **Publishes** to npm
11. **Pushes** to git (with push-protection detection and auto-bypass)
12. **Installs** globally (if `--install`)
13. **Restores** file: references (if `--files`, default)
14. **Runs audit** (shows security status)

## Operational Details

### Build Cascade

Before transforming or publishing anything, `npmglobalize` builds `file:` dependencies in topological order — deps before consumers — and then builds the target itself. This guarantees the target's `tsc` reads up-to-date `.d.ts` and `.js` from sibling checkouts even when a dep's source has changed since its last build.

For each project visited (the target and every transitive `file:` dep):

- If `tsconfig.json` is missing or has `"noEmit": true` → **skip** (not a TypeScript build), unless the `build` script runs `importgen` — a plain-JS browser app still needs its import map regenerated.
- If `tsconfig.json` exists but `package.json` has no `"build"` script → **prompt** to add `"build": "tsc"` (plus a `tsc -p <dir>` per sub-project — see below). Decline and that project is skipped.
- Otherwise → run `npm run build`. A failure halts the cascade unless `-force` is passed.

Cycle-safe via a shared visited set; each project is built at most once per run.

This complements the existing publish cascade (which ensures version refs are correct) by closing the build-freshness gap that `npm install` alone left open.

#### Import maps (`importgen`) as a build step

Browser projects that use [`importgen`](https://www.npmjs.com/package/@bobfrankston/importgen) have historically regenerated their import map from `.vscode/tasks.json`, which only runs when VS Code opens the folder — so a command-line or CI build could publish a stale map. `npmglobalize` treats the import map as a build product and moves the step into the package's own `build` script, where every build path picks it up.

Before building each project, it checks whether the project is an importgen project, in this order:

1. `.vscode/tasks.json` has a task whose `command` is `importgen` (the HTML file, if the task names one, is reused).
2. A root-level `.htm`/`.html` file already contains a `<script type="importmap">` block (`index.html`, `default.html`, `default.htm` are checked first, so a stray `temp.htm` doesn't win).
3. `importgen` is in `dependencies`/`devDependencies` **and** the project has one of those HTML files — the HTML requirement keeps packages that merely *use* importgen as a library from matching.

If a signal matches and the `build` script doesn't already run `importgen`, you're prompted to rewrite it — e.g. `"build": "tsc"` → `"build": "importgen default.htm && tsc"`. The HTML file is named explicitly so importgen doesn't have to guess. Decline and `"importgen": false` is written to `.globalize.json5`, which suppresses the prompt for good; `-noimportgen` does the same from the command line.

Once wired, the freshness check gains a second condition: a project whose build runs importgen is only considered up to date if the generated HTML is at least as new as `package.json`. Adding a dependency changes the import map without touching any `.ts` file, which the source-vs-output comparison alone would miss.

#### Sub-projects (a second `tsconfig.json` in a sub-directory)

A package can hold more than one TypeScript project. The common shape is a service worker in `Sw/` with its own `tsconfig.json` (`lib: ["WebWorker"]`, its own `outDir`) that the root `tsconfig.json` lists under `exclude` — so a bare `"build": "tsc"` compiles everything *except* the service worker, and the stale `sw2.js` ships. As with import maps, these have historically been built only by a second `.vscode/tasks.json` watcher, which runs on folder open and nowhere else.

Before building, `npmglobalize` looks for sub-projects, in this order:

1. `.vscode/tasks.json` has a task that runs `tsc` with `"options": { "cwd": "${workspaceFolder}/Sw" }` or with `-p`/`--project` naming a sub-directory (authoritative — it's how the project is actually built today; the task's `label` is quoted back in the prompt).
2. An immediate sub-directory containing its own `tsconfig.json`. Build output and vendored trees (`node_modules`, `prev`, `dist`, `built`, `out`, `wwwroot`, `coverage`, `temp`, `preflight`, dot-directories) are never scanned.

Nothing is detected when the root `tsconfig.json` uses project `references` — that build graph belongs to `tsc -b` and isn't second-guessed.

When the `build` script is `tsc`-driven and doesn't already compile a detected sub-project, you're prompted to append it — `"build": "tsc"` → `"build": "tsc && tsc -p Sw"`, or for an importgen project `"build": "importgen default.htm && tsc && tsc -p Sw"`. Already-wired scripts are recognized in any of their spellings (`tsc -p Sw`, `tsc --project ./Sw/tsconfig.json`, `cd Sw && tsc`), and the directory is emitted with its real on-disk casing so the script still works on WSL and CI. Decline and `"subProjects": false` is written to `.globalize.json5`, suppressing the prompt for good; a `"subProjects": ["Sw"]` array pins the list instead of detecting it.

Sub-projects the build actually compiles are also folded into the freshness check — the package rebuilds when a sub-project's sources are newer than its output. A sub-project that emits *outside* its own directory (`"outDir": ".."`, the usual service-worker case) always reports stale: comparing its sources against the whole package would prove nothing.

#### TypeScript 6 `types` auto-fix

TypeScript 6 dropped the legacy behavior of auto-including every installed `@types/*` package. A `tsconfig.json` with no explicit `compilerOptions.types` then loses the Node globals (`process`, `Buffer`, …) and the build fails with `TS2591`.

Before each build, when the global `tsc` is version 6 or newer, `npmglobalize` patches the project's `tsconfig.json` to add an explicit `types` list — enumerating the installed `@types/*` packages (e.g. `"types": ["node", …]`) — restoring the old behavior. The edit is:

- **Conservative** — only applied when `compilerOptions.types` is **absent**, `node_modules/@types/node` is actually installed, and there is no `extends` (whose merged `types` can't be seen). An explicit `types` you already set is never overridden.
- **Format-preserving** — the single `types` key is inserted into the existing `compilerOptions` block; comments, ordering, and indentation are left intact.
- **Idempotent** — once the list is present, subsequent runs skip it.

If the patch can't be applied for some reason and the build still fails with `TS2591`, the failure summary prints a hint to add `"types": ["node"]` manually.

### The `.dependencies` Backup (Internal/Transient)

**You should never see `.dependencies` in your `package.json` under normal operation.** It is a temporary internal backup that exists only during the brief publish cycle and is removed automatically when the cycle completes.

During publishing, `npmglobalize` temporarily replaces `file:` references with npm version strings. The original `file:` entries are stashed in `.dependencies` (and `.devDependencies`, etc.) so they can be restored afterward. Once the publish succeeds and `file:` paths are restored, `.dependencies` is deleted. A normal run leaves no trace of it.

**If you see `.dependencies` in your `package.json`, something went wrong** — the tool crashed, was killed, or the publish failed partway through. It is not a feature to rely on or edit manually.

**Recovery:**
- **Re-run `npmglobalize`**: It detects leftover `.dependencies`, restores the originals, and continues normally. Self-healing is automatic.
- **Manual restore**: `npmglobalize -cleanup` restores file: deps and removes the `.dependencies` backup.

**Why a persistent backup?** If the tool crashes hard (killed process, power failure, npm timeout), there's no cleanup code to run. The backup in `package.json` survives because it was written before the risky operations began. The next run self-heals.

### Flag Conventions

Both `-flag` and `--flag` are accepted. Single-dash is the primary convention:
```bash
npmglobalize -patch      # same as --patch
npmglobalize -np         # same as --nopublish
npmglobalize -local      # same as --local
```

### Persistent vs One-Shot Flags

Some flags are **persisted** to `.globalize.json5` when set from the CLI:
- `-install`, `-link`, `-wsl`, `-files`, `-fix` — install/build preferences
- `-np` (noPublish) — once set, prevents accidental publishes
- `-local` — remembers "this project is local-only"
- `-git`/`-npm` visibility

Other flags are **one-shot** (never persisted):
- `-cleanup`, `-init`, `-dry-run`, `-message` — situational actions
- `-update-deps`, `-update-major`, `-force-publish` — explicit per-run choices
- `-conform`, `-asis` — one-time fixes

Use `-once` to prevent any flag from persisting on that run:
```bash
npmglobalize -np -once   # No-publish this run only, don't remember it
```

### Local Install (`-local`)

Skip all transform/publish logic and just run `npm install -g .` with `file:` deps as-is. Use this when you want to install a CLI tool locally for your own use without publishing anything:

```bash
npmglobalize -local          # Install globally from local directory
npmglobalize -local -wsl     # Also install in WSL
```

This is useful for:
- Development tools you don't publish
- Testing a CLI before publishing
- Projects with `file:` deps that should stay as-is

### Private Packages

Packages with `"private": true` in `package.json` skip the npm publish step. Dependencies are still transformed and restored — the publish is the only thing skipped.

## Version Checking

When publishing file: dependencies, checks if each version exists on npm:
- ✅ Exists → Skip, use existing version
- ❌ Missing → Publish it first
- 🔄 Force → Use `--force-publish` to republish

## Examples

```bash
# Basic release
npmglobalize

# Run on a different project
npmglobalize y:\dev\myproject

# Auto-fix tag conflicts and rebase
npmglobalize -fix-tags -rebase

# Release with updates and security fixes
npmglobalize -update-deps -fix

# Just update package.json, don't publish
npmglobalize -np -update-deps

# Force republish all dependencies
npmglobalize -force-publish -update-major

# Release + install on Windows and WSL (from registry)
npmglobalize -install -wsl

# Release + link on Windows and WSL (symlink)
npmglobalize -link -wsl

# Install locally without publishing (file: deps stay as-is)
npmglobalize -local

# Restore original file: references
npmglobalize -cleanup

# Initialize git (adopt existing remote if reachable, else fresh) + release
npmglobalize -init

# Strict adopt: re-attach to remote in package.json.repository (or abort)
npmglobalize -adopt

# Migrate package.json scripts to use npmglobalize
npmglobalize -package

# Preview what would happen
npmglobalize -dry-run -verbose
```

## Authentication

Requires npm authentication:
```bash
npm login
```

Check authentication:
```bash
npm whoami
```

## Development

### Build Check

`npmglobalize` includes automatic build verification to ensure TypeScript files are compiled before execution. This prevents runtime errors from outdated JavaScript files.

**How it works:**
- When you run `npmglobalize`, it automatically checks if `.js` files are newer than their `.ts` sources
- If `.js` files are missing or outdated, execution stops with an error
- The check is skipped if `noEmit: true` is set in `tsconfig.json`

**Building the project:**
```bash
npm run build     # Compile TypeScript files
npm run watch     # Watch mode for development
npm run check     # Manually verify build status
```

**Bypassing the check:**
```bash
npmglobalize --force    # Skip build check (not recommended)
```

**Error example:**
```
❌ Error: TypeScript files not compiled
cli.js is older than cli.ts

Please run: npm run build
Or use --force to skip this check
```

This ensures you never accidentally run outdated code when the TypeScript source has changed.

## License

MIT
