@bobfrankston/npmglobalize 1.0.178 → 1.0.179

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 (5) hide show
  1. package/README.md +52 -2
  2. package/cli.js +9 -2
  3. package/lib.d.ts +8 -0
  4. package/lib.js +203 -20
  5. package/package.json +3 -3
package/README.md CHANGED
@@ -244,6 +244,49 @@ npmglobalize -git public # Makes the GitHub repo public (with confirmation)
244
244
  npmglobalize -git private # Makes the GitHub repo private
245
245
  ```
246
246
 
247
+ ### Missing local `.git` (adopt vs fresh init)
248
+
249
+ When npmglobalize runs in a directory with no `.git`, it first checks for a
250
+ reachable `repository.url` in `package.json`. If found, it offers two paths:
251
+
252
+ ```
253
+ How would you like to set up git?
254
+ 1) Adopt history from existing remote (recommended)
255
+ 2) Initialize fresh git repository
256
+ a) Adopt ALL (don't ask again for remaining deps)
257
+ 3) Use local install only (skip git/publish)
258
+ 4) Abort
259
+ ```
260
+
261
+ **Adopt** runs:
262
+
263
+ ```bash
264
+ git init
265
+ git remote add origin <package.json repository.url>
266
+ git fetch origin
267
+ # Point HEAD at origin/<defaultBranch> without touching the working tree
268
+ git update-ref refs/heads/<branch> origin/<branch>
269
+ git symbolic-ref HEAD refs/heads/<branch>
270
+ git reset # mixed: refresh index, keep working tree
271
+ ```
272
+
273
+ After adoption, your local files appear as **uncommitted changes on top of the
274
+ remote's HEAD** — `git status` shows the drift, and npmglobalize's normal flow
275
+ will commit them on the next publish. **No force-push is required**, and the
276
+ remote's history is preserved.
277
+
278
+ If the remote is not reachable (no `repository` field, network/auth failure,
279
+ deleted repo), the original "Initialize fresh git repository" prompt is shown
280
+ instead.
281
+
282
+ CLI flags:
283
+
284
+ - `-init` — auto path; adopts if a reachable remote is found, otherwise falls
285
+ back to fresh `git init` + `gh repo create`.
286
+ - `-adopt` — **strict**: aborts if no reachable remote in `package.json.repository`.
287
+ Use this when you want to be sure no new GitHub repo is created (e.g. in
288
+ scripts, or when re-attaching a tree of packages to existing repos).
289
+
247
290
  When initializing a new repository with `--init`, npmglobalize automatically sets up:
248
291
 
249
292
  **File structure** (per programming.md standards):
@@ -379,7 +422,11 @@ Workspace mode is auto-detected when run from a root with `"private": true` and
379
422
  ### Other Options
380
423
  ```
381
424
  -init Initialize git/npm if needed (creates .gitignore, .npmignore,
382
- .gitattributes, and configures git for LF line endings)
425
+ .gitattributes, and configures git for LF line endings).
426
+ If package.json.repository.url is reachable, adopts its
427
+ history instead of creating a fresh repo.
428
+ -adopt Strict adopt: require a reachable git remote in
429
+ package.json.repository. Abort if probe fails. Skips prompt.
383
430
  -force Continue despite git errors
384
431
  -dry-run Preview what would happen
385
432
  -quiet Suppress npm warnings (default)
@@ -625,9 +672,12 @@ npmglobalize -local
625
672
  # Restore original file: references
626
673
  npmglobalize -cleanup
627
674
 
628
- # Initialize new git repo + release
675
+ # Initialize git (adopt existing remote if reachable, else fresh) + release
629
676
  npmglobalize -init
630
677
 
678
+ # Strict adopt: re-attach to remote in package.json.repository (or abort)
679
+ npmglobalize -adopt
680
+
631
681
  # Migrate package.json scripts to use npmglobalize
632
682
  npmglobalize -package
633
683
 
package/cli.js CHANGED
@@ -82,7 +82,10 @@ Workspace Options:
82
82
  -continue-on-error Continue if a package fails in workspace mode
83
83
 
84
84
  Other Options:
85
- -init Initialize git/npm if needed
85
+ -init Initialize git/npm if needed (auto-adopts existing remote
86
+ in package.json.repository if reachable, else fresh init)
87
+ -adopt Strict adopt: require a reachable git remote in
88
+ package.json.repository. Abort if probe fails. Skips prompt.
86
89
  -force Continue despite git errors
87
90
  -dry-run Preview what would happen
88
91
  -quiet Suppress npm warnings (default)
@@ -117,7 +120,8 @@ Examples:
117
120
  npmglobalize -local -wsl Local install on Windows and WSL
118
121
  npmglobalize -np Just transform, no publish (remembered in config)
119
122
  npmglobalize -cleanup Restore original dependencies
120
- npmglobalize -init Initialize new git repo + release
123
+ npmglobalize -init Initialize git (adopt existing remote if any) + release
124
+ npmglobalize -adopt Adopt history from package.json.repository (abort if no remote)
121
125
  npmglobalize -dry-run Preview what would happen
122
126
  npmglobalize -package Migrate scripts to use npmglobalize
123
127
 
@@ -222,6 +226,9 @@ function parseArgs(args) {
222
226
  case '-init':
223
227
  options.init = true;
224
228
  break;
229
+ case '-adopt':
230
+ options.adopt = true;
231
+ break;
225
232
  case '-git':
226
233
  i++;
227
234
  if (args[i] === 'private' || args[i] === 'public') {
package/lib.d.ts CHANGED
@@ -115,6 +115,10 @@ export interface GlobalizeOptions {
115
115
  cleanNestedModules?: boolean;
116
116
  /** Internal: auto-initialize git repos without prompting (user chose "all") */
117
117
  autoInit?: boolean;
118
+ /** Strict adopt: require that a reachable git remote exists in
119
+ * package.json.repository. Aborts (rather than creating a fresh repo) if
120
+ * the probe fails. Skips the no-git prompt. */
121
+ adopt?: boolean;
118
122
  /** Internal: signals this call is from workspace orchestrator */
119
123
  /** Skip the upfront dep-graph prescan */
120
124
  noPrescan?: boolean;
@@ -382,6 +386,10 @@ export declare function confirm(message: string, defaultYes?: boolean): Promise<
382
386
  export declare function promptText(message: string, defaultValue?: string): Promise<string>;
383
387
  /** Prompt user for multiple choice */
384
388
  export declare function promptChoice(message: string, choices: string[]): Promise<string | null>;
389
+ /** Initialize a local git repo by adopting history from an existing remote.
390
+ * Preserves the working tree — local files appear as uncommitted changes
391
+ * on top of the remote's HEAD. No force-push is required. */
392
+ export declare function adoptExistingRemote(cwd: string, repoUrl: string, defaultBranch: string, dryRun: boolean): Promise<boolean>;
385
393
  /** Initialize git repository */
386
394
  export declare function initGit(cwd: string, visibility: 'private' | 'public', dryRun: boolean, allowTs?: boolean): Promise<boolean>;
387
395
  /** Main globalize function */
package/lib.js CHANGED
@@ -3045,6 +3045,105 @@ function ensureGitattributes(cwd) {
3045
3045
  console.log(colors.green(' ✓ .gitattributes created (LF line endings)'));
3046
3046
  }
3047
3047
  }
3048
+ /** Extract a git remote URL from package.json's repository field, normalised */
3049
+ function getPackageRepoUrl(pkg) {
3050
+ if (!pkg || !pkg.repository)
3051
+ return null;
3052
+ const raw = typeof pkg.repository === 'string' ? pkg.repository : pkg.repository.url;
3053
+ if (!raw || typeof raw !== 'string')
3054
+ return null;
3055
+ return raw.replace(/^git\+/, '').replace(/\.git$/, '') + (/\.git$/.test(raw) ? '.git' : '');
3056
+ }
3057
+ /** Silently probe a remote git URL. Returns the default branch on success. */
3058
+ function probeRemote(repoUrl) {
3059
+ try {
3060
+ const result = spawnSafe('git', ['ls-remote', '--symref', repoUrl, 'HEAD'], {
3061
+ encoding: 'utf-8',
3062
+ stdio: ['ignore', 'pipe', 'pipe'],
3063
+ timeout: 15000
3064
+ });
3065
+ if (result.status !== 0 || !result.stdout)
3066
+ return null;
3067
+ const match = result.stdout.match(/^ref:\s+refs\/heads\/(\S+)\s+HEAD/m);
3068
+ if (!match)
3069
+ return null;
3070
+ return { defaultBranch: match[1] };
3071
+ }
3072
+ catch {
3073
+ return null;
3074
+ }
3075
+ }
3076
+ /** Initialize a local git repo by adopting history from an existing remote.
3077
+ * Preserves the working tree — local files appear as uncommitted changes
3078
+ * on top of the remote's HEAD. No force-push is required. */
3079
+ export async function adoptExistingRemote(cwd, repoUrl, defaultBranch, dryRun) {
3080
+ console.log(`Adopting history from existing remote: ${repoUrl} (branch: ${defaultBranch})`);
3081
+ if (dryRun) {
3082
+ console.log(' [dry-run] Would run: git init');
3083
+ console.log(` [dry-run] Would run: git remote add origin ${repoUrl}`);
3084
+ console.log(' [dry-run] Would run: git fetch origin');
3085
+ console.log(` [dry-run] Would set HEAD to origin/${defaultBranch} without touching working tree`);
3086
+ return true;
3087
+ }
3088
+ // git init
3089
+ runCommandOrThrow('git', ['init'], { cwd });
3090
+ // Same dubious-ownership self-heal as initGit
3091
+ const ownerCheck = spawnSafe('git', ['rev-parse', '--git-dir'], {
3092
+ encoding: 'utf-8',
3093
+ stdio: 'pipe',
3094
+ cwd
3095
+ });
3096
+ if (ownerCheck.stderr && ownerCheck.stderr.includes('dubious ownership')) {
3097
+ console.log(colors.yellow('Git "dubious ownership" error — directory owner SID differs from current user.'));
3098
+ console.log(colors.yellow('Adding safe.directory \'*\' to global git config...'));
3099
+ const fix = spawnSafe('git', ['config', '--global', '--add', 'safe.directory', '*'], {
3100
+ encoding: 'utf-8',
3101
+ stdio: 'pipe',
3102
+ shell: true
3103
+ });
3104
+ if (fix.status === 0) {
3105
+ console.log(colors.green(' ✓ Fixed git safe.directory'));
3106
+ }
3107
+ else {
3108
+ throw new Error('Failed to fix git safe.directory. Run manually: git config --global --add safe.directory \'*\'');
3109
+ }
3110
+ }
3111
+ // LF line endings (matches initGit / programming.md)
3112
+ runCommandOrThrow('git', ['config', 'core.autocrlf', 'false'], { cwd });
3113
+ runCommandOrThrow('git', ['config', 'core.eol', 'lf'], { cwd });
3114
+ console.log(' ✓ Configured git for LF line endings');
3115
+ // Add origin (set-url if it somehow exists)
3116
+ const addRemote = runCommand('git', ['remote', 'add', 'origin', repoUrl], { cwd, silent: true });
3117
+ if (!addRemote.success) {
3118
+ runCommand('git', ['remote', 'set-url', 'origin', repoUrl], { cwd, silent: true });
3119
+ }
3120
+ // Fetch — this is the slow step
3121
+ console.log(' Fetching from origin...');
3122
+ const fetchRes = await runCommandAsync('git', ['fetch', 'origin'], { cwd, silent: true });
3123
+ if (!fetchRes.success) {
3124
+ const err = (fetchRes.stderr || fetchRes.output || '').trim();
3125
+ console.error(colors.red(` Failed to fetch from origin: ${err}`));
3126
+ return false;
3127
+ }
3128
+ // Point local branch at origin/<branch>, set HEAD, refresh index — all without
3129
+ // touching the working tree. `git reset` (mixed) updates the index from HEAD
3130
+ // so existing local files show as modifications/additions vs the remote.
3131
+ runCommandOrThrow('git', ['update-ref', `refs/heads/${defaultBranch}`, `origin/${defaultBranch}`], { cwd });
3132
+ runCommandOrThrow('git', ['symbolic-ref', 'HEAD', `refs/heads/${defaultBranch}`], { cwd });
3133
+ runCommandOrThrow('git', ['reset'], { cwd });
3134
+ runCommand('git', ['branch', `--set-upstream-to=origin/${defaultBranch}`, defaultBranch], { cwd, silent: true });
3135
+ console.log(colors.green(` ✓ Adopted history from ${repoUrl}`));
3136
+ // Show drift summary
3137
+ const statusRes = runCommand('git', ['status', '--porcelain'], { cwd, silent: true });
3138
+ const changes = (statusRes.output || '').split('\n').filter(l => l.trim().length > 0);
3139
+ if (changes.length === 0) {
3140
+ console.log(colors.dim(' Working tree matches remote HEAD.'));
3141
+ }
3142
+ else {
3143
+ console.log(colors.dim(` ${changes.length} file(s) differ from remote HEAD — npmglobalize will commit them before publishing.`));
3144
+ }
3145
+ return true;
3146
+ }
3048
3147
  /** Initialize git repository */
3049
3148
  export async function initGit(cwd, visibility, dryRun, allowTs) {
3050
3149
  const pkg = readPackageJson(cwd);
@@ -3312,7 +3411,7 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
3312
3411
  const { bump = 'patch', noPublish = false, cleanup = false, install = false, link = false, wsl = false, force = false, files = true, dryRun = false, quiet = true, verbose = false, init = false, gitVisibility = 'private', npmVisibility = 'private', message, conform = false, asis = false, updateDeps = false, updateMajor = false, publishDeps = true, // Default to publishing deps for safety
3313
3412
  publishDepsYes = false, // -pd: auto-yes to dep-cascade prompts (private only)
3314
3413
  publicDeps = false, // -public-deps: cascade public visibility to all deps
3315
- noPrescan = false, forcePublish = false, fix = true, fixTags = false, rebase = false, show = false, local = false, freeze = false, usePaths = true, allowTs } = options;
3414
+ noPrescan = false, forcePublish = false, fix = true, fixTags = false, rebase = false, show = false, local = false, freeze = false, usePaths = true, allowTs, adopt = false } = options;
3316
3415
  // Show tool version only for recursive dep calls (CLI already prints it at startup)
3317
3416
  const toolVersion = getToolVersion();
3318
3417
  if (!options._fromWorkspace && !options._fromCli) {
@@ -3553,33 +3652,116 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
3553
3652
  let justInitialized = false;
3554
3653
  if (!gitStatus.isRepo) {
3555
3654
  console.log('No git repository found.');
3556
- if (dryRun) {
3557
- console.log(' [dry-run] Would initialize git repository');
3655
+ // Probe for an existing remote we could adopt history from.
3656
+ // The package.json may already point at a GitHub repo from a previous
3657
+ // publish — adopting it preserves remote history (no force-push needed).
3658
+ const pkgForProbe = readPackageJson(cwd);
3659
+ const probeUrl = getPackageRepoUrl(pkgForProbe);
3660
+ let adoptable = null;
3661
+ if (probeUrl) {
3662
+ console.log(colors.dim(` Probing existing remote: ${probeUrl} ...`));
3663
+ const probed = probeRemote(probeUrl);
3664
+ if (probed) {
3665
+ adoptable = { url: probeUrl, defaultBranch: probed.defaultBranch };
3666
+ console.log(colors.green(` ✓ Remote reachable (default branch: ${probed.defaultBranch})`));
3667
+ }
3668
+ else {
3669
+ console.log(colors.dim(' Remote not reachable — falling back to fresh init options.'));
3670
+ }
3558
3671
  }
3559
- else if (!init && !options.autoInit && !publishDepsYes) {
3560
- const choice = await promptChoice('No git repository found. What would you like to do?\n 1) Initialize git repository (default)\n a) Initialize ALL (don\'t ask again for remaining deps)\n 2) Use local install only (skip git/publish)\n 3) Abort\nChoice:', ['1', 'a', '2', '3', '']);
3561
- if (choice === '2') {
3562
- console.log(colors.dim('Switching to local-only mode...'));
3563
- writeConfig(cwd, { ...configOptions, local: true }, new Set(['local']));
3564
- return doLocalInstall(cwd, options);
3672
+ // Strict -adopt: require a reachable remote, never create a fresh repo
3673
+ if (adopt && !adoptable) {
3674
+ console.error(colors.red('ERROR: -adopt requires a reachable git remote in package.json.repository'));
3675
+ if (probeUrl) {
3676
+ console.error(colors.red(` Configured: ${probeUrl}`));
3677
+ console.error(colors.red(' Probe failed — check the URL, network, or credentials.'));
3565
3678
  }
3566
- if (choice === '3') {
3567
- console.log('Aborted. Run with --init to initialize.');
3568
- return false;
3679
+ else {
3680
+ console.error(colors.red(' No "repository" field found in package.json.'));
3569
3681
  }
3570
- if (choice === 'a') {
3571
- options.autoInit = true;
3682
+ console.error(colors.red(' Drop -adopt to fall back to fresh init, or fix the remote URL.'));
3683
+ return false;
3684
+ }
3685
+ if (dryRun) {
3686
+ if (adoptable) {
3687
+ console.log(` [dry-run] Would adopt history from ${adoptable.url}`);
3688
+ }
3689
+ else {
3690
+ console.log(' [dry-run] Would initialize git repository');
3572
3691
  }
3573
- // choice is '1', 'a', or '' (default)
3574
- const success = await initGit(cwd, gitVisibility, dryRun, allowTs);
3692
+ }
3693
+ else if (adopt && adoptable) {
3694
+ // -adopt bypasses the prompt
3695
+ const success = await adoptExistingRemote(cwd, adoptable.url, adoptable.defaultBranch, dryRun);
3575
3696
  if (!success)
3576
3697
  return false;
3577
3698
  justInitialized = true;
3578
3699
  }
3700
+ else if (!init && !options.autoInit && !publishDepsYes) {
3701
+ let choice;
3702
+ if (adoptable) {
3703
+ choice = await promptChoice('How would you like to set up git?\n'
3704
+ + ' 1) Adopt history from existing remote (recommended)\n'
3705
+ + ' 2) Initialize fresh git repository\n'
3706
+ + ' a) Adopt ALL (don\'t ask again for remaining deps)\n'
3707
+ + ' 3) Use local install only (skip git/publish)\n'
3708
+ + ' 4) Abort\nChoice:', ['1', '2', 'a', '3', '4', '']);
3709
+ if (choice === '3') {
3710
+ console.log(colors.dim('Switching to local-only mode...'));
3711
+ writeConfig(cwd, { ...configOptions, local: true }, new Set(['local']));
3712
+ return doLocalInstall(cwd, options);
3713
+ }
3714
+ if (choice === '4') {
3715
+ console.log('Aborted. Run with --init to initialize.');
3716
+ return false;
3717
+ }
3718
+ if (choice === 'a')
3719
+ options.autoInit = true;
3720
+ if (choice === '2') {
3721
+ const success = await initGit(cwd, gitVisibility, dryRun, allowTs);
3722
+ if (!success)
3723
+ return false;
3724
+ }
3725
+ else {
3726
+ // '1', 'a', '' default → adopt
3727
+ const success = await adoptExistingRemote(cwd, adoptable.url, adoptable.defaultBranch, dryRun);
3728
+ if (!success)
3729
+ return false;
3730
+ }
3731
+ }
3732
+ else {
3733
+ choice = await promptChoice('No git repository found. What would you like to do?\n 1) Initialize git repository (default)\n a) Initialize ALL (don\'t ask again for remaining deps)\n 2) Use local install only (skip git/publish)\n 3) Abort\nChoice:', ['1', 'a', '2', '3', '']);
3734
+ if (choice === '2') {
3735
+ console.log(colors.dim('Switching to local-only mode...'));
3736
+ writeConfig(cwd, { ...configOptions, local: true }, new Set(['local']));
3737
+ return doLocalInstall(cwd, options);
3738
+ }
3739
+ if (choice === '3') {
3740
+ console.log('Aborted. Run with --init to initialize.');
3741
+ return false;
3742
+ }
3743
+ if (choice === 'a')
3744
+ options.autoInit = true;
3745
+ // choice is '1', 'a', or '' (default)
3746
+ const success = await initGit(cwd, gitVisibility, dryRun, allowTs);
3747
+ if (!success)
3748
+ return false;
3749
+ }
3750
+ justInitialized = true;
3751
+ }
3579
3752
  else {
3580
- const success = await initGit(cwd, gitVisibility, dryRun, allowTs);
3581
- if (!success)
3582
- return false;
3753
+ // Auto path (--init, autoInit, publishDepsYes): prefer adoption if a
3754
+ // reachable remote was found in package.json.
3755
+ if (adoptable) {
3756
+ const success = await adoptExistingRemote(cwd, adoptable.url, adoptable.defaultBranch, dryRun);
3757
+ if (!success)
3758
+ return false;
3759
+ }
3760
+ else {
3761
+ const success = await initGit(cwd, gitVisibility, dryRun, allowTs);
3762
+ if (!success)
3763
+ return false;
3764
+ }
3583
3765
  justInitialized = true;
3584
3766
  }
3585
3767
  }
@@ -4239,7 +4421,8 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
4239
4421
  forcePublish, // Propagate so transitive deps get force-published too
4240
4422
  _fromDep: true, // Suppress nested prescan
4241
4423
  rebase, // Propagate so behind-remote deps get rebased automatically
4242
- autoInit: options.autoInit // Propagate "init all" choice
4424
+ autoInit: options.autoInit, // Propagate "init all" choice
4425
+ adopt // Propagate strict adopt-or-abort flag through cascade
4243
4426
  });
4244
4427
  if (!depSuccess) {
4245
4428
  console.error(colors.red(`Failed to publish ${name}`));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/npmglobalize",
3
- "version": "1.0.178",
3
+ "version": "1.0.179",
4
4
  "description": "Transform file: dependencies to npm versions for publishing",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -32,7 +32,7 @@
32
32
  },
33
33
  "dependencies": {
34
34
  "@bobfrankston/freezepak": "^0.1.8",
35
- "@bobfrankston/importgen": "^0.1.35",
35
+ "@bobfrankston/importgen": "^0.1.36",
36
36
  "@bobfrankston/mailx": "^1.0.466",
37
37
  "@bobfrankston/npmglobalize": "^1.0.153",
38
38
  "@bobfrankston/themecolors": "^0.1.6",
@@ -64,7 +64,7 @@
64
64
  ".transformedSnapshot": {
65
65
  "dependencies": {
66
66
  "@bobfrankston/freezepak": "^0.1.8",
67
- "@bobfrankston/importgen": "^0.1.35",
67
+ "@bobfrankston/importgen": "^0.1.36",
68
68
  "@bobfrankston/mailx": "^1.0.466",
69
69
  "@bobfrankston/npmglobalize": "^1.0.153",
70
70
  "@bobfrankston/themecolors": "^0.1.6",