@bobfrankston/npmglobalize 1.0.178 → 1.0.180
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.
- package/README.md +59 -2
- package/cli.js +17 -2
- package/lib.d.ts +16 -0
- package/lib.js +363 -20
- 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,18 @@ 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.
|
|
430
|
+
-no-import-check, -no-imports
|
|
431
|
+
Skip the undeclared-imports scan. By default, npmglobalize
|
|
432
|
+
scans .ts/.js source for imports of packages not declared in
|
|
433
|
+
any dependencies bucket and warns before publishing. The scan
|
|
434
|
+
catches silent runtime failures (ERR_MODULE_NOT_FOUND on a
|
|
435
|
+
clean install) where the import was resolving via a global or
|
|
436
|
+
parent node_modules at dev time but won't on the target host.
|
|
383
437
|
-force Continue despite git errors
|
|
384
438
|
-dry-run Preview what would happen
|
|
385
439
|
-quiet Suppress npm warnings (default)
|
|
@@ -625,9 +679,12 @@ npmglobalize -local
|
|
|
625
679
|
# Restore original file: references
|
|
626
680
|
npmglobalize -cleanup
|
|
627
681
|
|
|
628
|
-
# Initialize
|
|
682
|
+
# Initialize git (adopt existing remote if reachable, else fresh) + release
|
|
629
683
|
npmglobalize -init
|
|
630
684
|
|
|
685
|
+
# Strict adopt: re-attach to remote in package.json.repository (or abort)
|
|
686
|
+
npmglobalize -adopt
|
|
687
|
+
|
|
631
688
|
# Migrate package.json scripts to use npmglobalize
|
|
632
689
|
npmglobalize -package
|
|
633
690
|
|
package/cli.js
CHANGED
|
@@ -82,7 +82,14 @@ 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.
|
|
89
|
+
-no-import-check
|
|
90
|
+
Skip the undeclared-imports scan. Default: scan runs and
|
|
91
|
+
warns about packages imported in .ts/.js but missing from
|
|
92
|
+
any dependencies bucket (catches silent runtime failures).
|
|
86
93
|
-force Continue despite git errors
|
|
87
94
|
-dry-run Preview what would happen
|
|
88
95
|
-quiet Suppress npm warnings (default)
|
|
@@ -117,7 +124,8 @@ Examples:
|
|
|
117
124
|
npmglobalize -local -wsl Local install on Windows and WSL
|
|
118
125
|
npmglobalize -np Just transform, no publish (remembered in config)
|
|
119
126
|
npmglobalize -cleanup Restore original dependencies
|
|
120
|
-
npmglobalize -init Initialize
|
|
127
|
+
npmglobalize -init Initialize git (adopt existing remote if any) + release
|
|
128
|
+
npmglobalize -adopt Adopt history from package.json.repository (abort if no remote)
|
|
121
129
|
npmglobalize -dry-run Preview what would happen
|
|
122
130
|
npmglobalize -package Migrate scripts to use npmglobalize
|
|
123
131
|
|
|
@@ -222,6 +230,13 @@ function parseArgs(args) {
|
|
|
222
230
|
case '-init':
|
|
223
231
|
options.init = true;
|
|
224
232
|
break;
|
|
233
|
+
case '-adopt':
|
|
234
|
+
options.adopt = true;
|
|
235
|
+
break;
|
|
236
|
+
case '-no-import-check':
|
|
237
|
+
case '-no-imports':
|
|
238
|
+
options.noImportCheck = true;
|
|
239
|
+
break;
|
|
225
240
|
case '-git':
|
|
226
241
|
i++;
|
|
227
242
|
if (args[i] === 'private' || args[i] === 'public') {
|
package/lib.d.ts
CHANGED
|
@@ -115,6 +115,12 @@ 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;
|
|
122
|
+
/** Skip the undeclared-imports scan. Default false (scan runs). */
|
|
123
|
+
noImportCheck?: boolean;
|
|
118
124
|
/** Internal: signals this call is from workspace orchestrator */
|
|
119
125
|
/** Skip the upfront dep-graph prescan */
|
|
120
126
|
noPrescan?: boolean;
|
|
@@ -256,6 +262,12 @@ export interface PrescanIssue {
|
|
|
256
262
|
message: string;
|
|
257
263
|
suggestion?: string;
|
|
258
264
|
}
|
|
265
|
+
export interface UndeclaredImport {
|
|
266
|
+
name: string;
|
|
267
|
+
file: string;
|
|
268
|
+
line: number;
|
|
269
|
+
}
|
|
270
|
+
export declare function findUndeclaredImports(cwd: string, pkg: any): UndeclaredImport[];
|
|
259
271
|
/** Walk the full file: dep graph and collect issues up front — missing scopes,
|
|
260
272
|
* unresolvable paths, missing package.json, unpublished transitives. Lets the
|
|
261
273
|
* user resolve all problems before starting the publish cascade instead of
|
|
@@ -382,6 +394,10 @@ export declare function confirm(message: string, defaultYes?: boolean): Promise<
|
|
|
382
394
|
export declare function promptText(message: string, defaultValue?: string): Promise<string>;
|
|
383
395
|
/** Prompt user for multiple choice */
|
|
384
396
|
export declare function promptChoice(message: string, choices: string[]): Promise<string | null>;
|
|
397
|
+
/** Initialize a local git repo by adopting history from an existing remote.
|
|
398
|
+
* Preserves the working tree — local files appear as uncommitted changes
|
|
399
|
+
* on top of the remote's HEAD. No force-push is required. */
|
|
400
|
+
export declare function adoptExistingRemote(cwd: string, repoUrl: string, defaultBranch: string, dryRun: boolean): Promise<boolean>;
|
|
385
401
|
/** Initialize git repository */
|
|
386
402
|
export declare function initGit(cwd: string, visibility: 'private' | 'public', dryRun: boolean, allowTs?: boolean): Promise<boolean>;
|
|
387
403
|
/** Main globalize function */
|
package/lib.js
CHANGED
|
@@ -13,6 +13,7 @@ import fs from 'fs';
|
|
|
13
13
|
import os from 'os';
|
|
14
14
|
import path from 'path';
|
|
15
15
|
import { execSync, spawn, spawnSync } from 'child_process';
|
|
16
|
+
import { builtinModules } from 'module';
|
|
16
17
|
import { readConfig as readUserConfig, writeConfig as writeUserConfig, configDir } from '@bobfrankston/userconfig';
|
|
17
18
|
import { freezeDependencies } from '@bobfrankston/freezepak';
|
|
18
19
|
import { importgen as runImportgen } from '@bobfrankston/importgen';
|
|
@@ -1304,6 +1305,130 @@ function printDepTree(baseDir, indent = 0, visited = new Set()) {
|
|
|
1304
1305
|
}
|
|
1305
1306
|
}
|
|
1306
1307
|
}
|
|
1308
|
+
/** Source-import scan: finds package imports in .ts/.js that aren't declared
|
|
1309
|
+
* in any of the package.json dep buckets. Catches the failure mode where a
|
|
1310
|
+
* file imports `@scope/thing` but no one ever added it to dependencies — the
|
|
1311
|
+
* tarball publishes "fine" and crashes with ERR_MODULE_NOT_FOUND on install.
|
|
1312
|
+
* The previously-working case usually relies on a global/parent copy that
|
|
1313
|
+
* Node ESM resolution happened to find. */
|
|
1314
|
+
const SOURCE_SKIP_DIRS = new Set([
|
|
1315
|
+
'node_modules', 'prev', '.git', 'dist', 'build', 'out',
|
|
1316
|
+
'coverage', '.next', '.nuxt', '.turbo', '.cache'
|
|
1317
|
+
]);
|
|
1318
|
+
function collectSourceFiles(dir, acc = [], depth = 0) {
|
|
1319
|
+
if (depth > 12)
|
|
1320
|
+
return acc;
|
|
1321
|
+
let entries;
|
|
1322
|
+
try {
|
|
1323
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
1324
|
+
}
|
|
1325
|
+
catch {
|
|
1326
|
+
return acc;
|
|
1327
|
+
}
|
|
1328
|
+
for (const ent of entries) {
|
|
1329
|
+
const name = ent.name;
|
|
1330
|
+
if (ent.isDirectory()) {
|
|
1331
|
+
if (SOURCE_SKIP_DIRS.has(name))
|
|
1332
|
+
continue;
|
|
1333
|
+
if (name.startsWith('.'))
|
|
1334
|
+
continue; // .git, .vscode, .idea, etc.
|
|
1335
|
+
collectSourceFiles(path.join(dir, name), acc, depth + 1);
|
|
1336
|
+
}
|
|
1337
|
+
else if (ent.isFile()) {
|
|
1338
|
+
if (name.endsWith('.d.ts') || name.endsWith('.map'))
|
|
1339
|
+
continue;
|
|
1340
|
+
if (name.endsWith('.ts') || name.endsWith('.tsx')
|
|
1341
|
+
|| name.endsWith('.js') || name.endsWith('.jsx')
|
|
1342
|
+
|| name.endsWith('.mjs') || name.endsWith('.cjs')) {
|
|
1343
|
+
acc.push(path.join(dir, name));
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
return acc;
|
|
1348
|
+
}
|
|
1349
|
+
/** Extract the package name from an ESM/CJS module specifier.
|
|
1350
|
+
* Returns null for relative paths, absolute paths, URLs, node: builtins. */
|
|
1351
|
+
function specifierToPackageName(spec) {
|
|
1352
|
+
if (!spec)
|
|
1353
|
+
return null;
|
|
1354
|
+
if (spec.startsWith('.'))
|
|
1355
|
+
return null; // ./ or ../
|
|
1356
|
+
if (spec.startsWith('/'))
|
|
1357
|
+
return null; // /abs
|
|
1358
|
+
if (spec.startsWith('node:'))
|
|
1359
|
+
return null; // node:fs
|
|
1360
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(spec))
|
|
1361
|
+
return null; // url/data
|
|
1362
|
+
if (spec.startsWith('#'))
|
|
1363
|
+
return null; // subpath import
|
|
1364
|
+
if (spec.startsWith('@')) {
|
|
1365
|
+
const parts = spec.split('/');
|
|
1366
|
+
return parts.length >= 2 ? `${parts[0]}/${parts[1]}` : null;
|
|
1367
|
+
}
|
|
1368
|
+
return spec.split('/')[0];
|
|
1369
|
+
}
|
|
1370
|
+
const NODE_BUILTIN_NAMES = new Set(builtinModules);
|
|
1371
|
+
export function findUndeclaredImports(cwd, pkg) {
|
|
1372
|
+
const declared = new Set();
|
|
1373
|
+
for (const k of DEP_KEYS) {
|
|
1374
|
+
const bucket = pkg[k] || pkg['.' + k]; // also consult the backup bucket if a transform is pending
|
|
1375
|
+
if (bucket)
|
|
1376
|
+
for (const n of Object.keys(bucket))
|
|
1377
|
+
declared.add(n);
|
|
1378
|
+
}
|
|
1379
|
+
const selfName = pkg.name;
|
|
1380
|
+
// One specifier per line is the common case; a few patterns cover the rest.
|
|
1381
|
+
// We process line-by-line so we can report file:line and avoid pathological
|
|
1382
|
+
// backtracking on huge files.
|
|
1383
|
+
const FROM_RE = /\bfrom\s+['"]([^'"]+)['"]/g;
|
|
1384
|
+
const BARE_IMPORT_RE = /(?:^|;|\s)import\s+['"]([^'"]+)['"]/g;
|
|
1385
|
+
const DYNAMIC_RE = /(?:^|[^.\w$])import\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
|
|
1386
|
+
const REQUIRE_RE = /(?:^|[^.\w$])require\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
|
|
1387
|
+
const PATTERNS = [FROM_RE, BARE_IMPORT_RE, DYNAMIC_RE, REQUIRE_RE];
|
|
1388
|
+
const findings = new Map();
|
|
1389
|
+
const files = collectSourceFiles(cwd);
|
|
1390
|
+
for (const f of files) {
|
|
1391
|
+
let content;
|
|
1392
|
+
try {
|
|
1393
|
+
content = fs.readFileSync(f, 'utf-8');
|
|
1394
|
+
}
|
|
1395
|
+
catch {
|
|
1396
|
+
continue;
|
|
1397
|
+
}
|
|
1398
|
+
// Quick reject: no import/require keyword anywhere → skip
|
|
1399
|
+
if (!/\bimport\b|\brequire\s*\(/.test(content))
|
|
1400
|
+
continue;
|
|
1401
|
+
const lines = content.split('\n');
|
|
1402
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1403
|
+
const line = lines[i];
|
|
1404
|
+
for (const re of PATTERNS) {
|
|
1405
|
+
re.lastIndex = 0;
|
|
1406
|
+
let m;
|
|
1407
|
+
while ((m = re.exec(line))) {
|
|
1408
|
+
const spec = m[1];
|
|
1409
|
+
const pkgName = specifierToPackageName(spec);
|
|
1410
|
+
if (!pkgName)
|
|
1411
|
+
continue;
|
|
1412
|
+
if (NODE_BUILTIN_NAMES.has(pkgName))
|
|
1413
|
+
continue;
|
|
1414
|
+
if (pkgName === selfName)
|
|
1415
|
+
continue;
|
|
1416
|
+
if (declared.has(pkgName))
|
|
1417
|
+
continue;
|
|
1418
|
+
if (!findings.has(pkgName)) {
|
|
1419
|
+
findings.set(pkgName, {
|
|
1420
|
+
file: path.relative(cwd, f).replace(/\\/g, '/'),
|
|
1421
|
+
line: i + 1
|
|
1422
|
+
});
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
return [...findings.entries()]
|
|
1429
|
+
.map(([name, loc]) => ({ name, ...loc }))
|
|
1430
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
1431
|
+
}
|
|
1307
1432
|
/** Walk the full file: dep graph and collect issues up front — missing scopes,
|
|
1308
1433
|
* unresolvable paths, missing package.json, unpublished transitives. Lets the
|
|
1309
1434
|
* user resolve all problems before starting the publish cascade instead of
|
|
@@ -3045,6 +3170,105 @@ function ensureGitattributes(cwd) {
|
|
|
3045
3170
|
console.log(colors.green(' ✓ .gitattributes created (LF line endings)'));
|
|
3046
3171
|
}
|
|
3047
3172
|
}
|
|
3173
|
+
/** Extract a git remote URL from package.json's repository field, normalised */
|
|
3174
|
+
function getPackageRepoUrl(pkg) {
|
|
3175
|
+
if (!pkg || !pkg.repository)
|
|
3176
|
+
return null;
|
|
3177
|
+
const raw = typeof pkg.repository === 'string' ? pkg.repository : pkg.repository.url;
|
|
3178
|
+
if (!raw || typeof raw !== 'string')
|
|
3179
|
+
return null;
|
|
3180
|
+
return raw.replace(/^git\+/, '').replace(/\.git$/, '') + (/\.git$/.test(raw) ? '.git' : '');
|
|
3181
|
+
}
|
|
3182
|
+
/** Silently probe a remote git URL. Returns the default branch on success. */
|
|
3183
|
+
function probeRemote(repoUrl) {
|
|
3184
|
+
try {
|
|
3185
|
+
const result = spawnSafe('git', ['ls-remote', '--symref', repoUrl, 'HEAD'], {
|
|
3186
|
+
encoding: 'utf-8',
|
|
3187
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
3188
|
+
timeout: 15000
|
|
3189
|
+
});
|
|
3190
|
+
if (result.status !== 0 || !result.stdout)
|
|
3191
|
+
return null;
|
|
3192
|
+
const match = result.stdout.match(/^ref:\s+refs\/heads\/(\S+)\s+HEAD/m);
|
|
3193
|
+
if (!match)
|
|
3194
|
+
return null;
|
|
3195
|
+
return { defaultBranch: match[1] };
|
|
3196
|
+
}
|
|
3197
|
+
catch {
|
|
3198
|
+
return null;
|
|
3199
|
+
}
|
|
3200
|
+
}
|
|
3201
|
+
/** Initialize a local git repo by adopting history from an existing remote.
|
|
3202
|
+
* Preserves the working tree — local files appear as uncommitted changes
|
|
3203
|
+
* on top of the remote's HEAD. No force-push is required. */
|
|
3204
|
+
export async function adoptExistingRemote(cwd, repoUrl, defaultBranch, dryRun) {
|
|
3205
|
+
console.log(`Adopting history from existing remote: ${repoUrl} (branch: ${defaultBranch})`);
|
|
3206
|
+
if (dryRun) {
|
|
3207
|
+
console.log(' [dry-run] Would run: git init');
|
|
3208
|
+
console.log(` [dry-run] Would run: git remote add origin ${repoUrl}`);
|
|
3209
|
+
console.log(' [dry-run] Would run: git fetch origin');
|
|
3210
|
+
console.log(` [dry-run] Would set HEAD to origin/${defaultBranch} without touching working tree`);
|
|
3211
|
+
return true;
|
|
3212
|
+
}
|
|
3213
|
+
// git init
|
|
3214
|
+
runCommandOrThrow('git', ['init'], { cwd });
|
|
3215
|
+
// Same dubious-ownership self-heal as initGit
|
|
3216
|
+
const ownerCheck = spawnSafe('git', ['rev-parse', '--git-dir'], {
|
|
3217
|
+
encoding: 'utf-8',
|
|
3218
|
+
stdio: 'pipe',
|
|
3219
|
+
cwd
|
|
3220
|
+
});
|
|
3221
|
+
if (ownerCheck.stderr && ownerCheck.stderr.includes('dubious ownership')) {
|
|
3222
|
+
console.log(colors.yellow('Git "dubious ownership" error — directory owner SID differs from current user.'));
|
|
3223
|
+
console.log(colors.yellow('Adding safe.directory \'*\' to global git config...'));
|
|
3224
|
+
const fix = spawnSafe('git', ['config', '--global', '--add', 'safe.directory', '*'], {
|
|
3225
|
+
encoding: 'utf-8',
|
|
3226
|
+
stdio: 'pipe',
|
|
3227
|
+
shell: true
|
|
3228
|
+
});
|
|
3229
|
+
if (fix.status === 0) {
|
|
3230
|
+
console.log(colors.green(' ✓ Fixed git safe.directory'));
|
|
3231
|
+
}
|
|
3232
|
+
else {
|
|
3233
|
+
throw new Error('Failed to fix git safe.directory. Run manually: git config --global --add safe.directory \'*\'');
|
|
3234
|
+
}
|
|
3235
|
+
}
|
|
3236
|
+
// LF line endings (matches initGit / programming.md)
|
|
3237
|
+
runCommandOrThrow('git', ['config', 'core.autocrlf', 'false'], { cwd });
|
|
3238
|
+
runCommandOrThrow('git', ['config', 'core.eol', 'lf'], { cwd });
|
|
3239
|
+
console.log(' ✓ Configured git for LF line endings');
|
|
3240
|
+
// Add origin (set-url if it somehow exists)
|
|
3241
|
+
const addRemote = runCommand('git', ['remote', 'add', 'origin', repoUrl], { cwd, silent: true });
|
|
3242
|
+
if (!addRemote.success) {
|
|
3243
|
+
runCommand('git', ['remote', 'set-url', 'origin', repoUrl], { cwd, silent: true });
|
|
3244
|
+
}
|
|
3245
|
+
// Fetch — this is the slow step
|
|
3246
|
+
console.log(' Fetching from origin...');
|
|
3247
|
+
const fetchRes = await runCommandAsync('git', ['fetch', 'origin'], { cwd, silent: true });
|
|
3248
|
+
if (!fetchRes.success) {
|
|
3249
|
+
const err = (fetchRes.stderr || fetchRes.output || '').trim();
|
|
3250
|
+
console.error(colors.red(` Failed to fetch from origin: ${err}`));
|
|
3251
|
+
return false;
|
|
3252
|
+
}
|
|
3253
|
+
// Point local branch at origin/<branch>, set HEAD, refresh index — all without
|
|
3254
|
+
// touching the working tree. `git reset` (mixed) updates the index from HEAD
|
|
3255
|
+
// so existing local files show as modifications/additions vs the remote.
|
|
3256
|
+
runCommandOrThrow('git', ['update-ref', `refs/heads/${defaultBranch}`, `origin/${defaultBranch}`], { cwd });
|
|
3257
|
+
runCommandOrThrow('git', ['symbolic-ref', 'HEAD', `refs/heads/${defaultBranch}`], { cwd });
|
|
3258
|
+
runCommandOrThrow('git', ['reset'], { cwd });
|
|
3259
|
+
runCommand('git', ['branch', `--set-upstream-to=origin/${defaultBranch}`, defaultBranch], { cwd, silent: true });
|
|
3260
|
+
console.log(colors.green(` ✓ Adopted history from ${repoUrl}`));
|
|
3261
|
+
// Show drift summary
|
|
3262
|
+
const statusRes = runCommand('git', ['status', '--porcelain'], { cwd, silent: true });
|
|
3263
|
+
const changes = (statusRes.output || '').split('\n').filter(l => l.trim().length > 0);
|
|
3264
|
+
if (changes.length === 0) {
|
|
3265
|
+
console.log(colors.dim(' Working tree matches remote HEAD.'));
|
|
3266
|
+
}
|
|
3267
|
+
else {
|
|
3268
|
+
console.log(colors.dim(` ${changes.length} file(s) differ from remote HEAD — npmglobalize will commit them before publishing.`));
|
|
3269
|
+
}
|
|
3270
|
+
return true;
|
|
3271
|
+
}
|
|
3048
3272
|
/** Initialize git repository */
|
|
3049
3273
|
export async function initGit(cwd, visibility, dryRun, allowTs) {
|
|
3050
3274
|
const pkg = readPackageJson(cwd);
|
|
@@ -3312,7 +3536,7 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
|
|
|
3312
3536
|
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
3537
|
publishDepsYes = false, // -pd: auto-yes to dep-cascade prompts (private only)
|
|
3314
3538
|
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;
|
|
3539
|
+
noPrescan = false, forcePublish = false, fix = true, fixTags = false, rebase = false, show = false, local = false, freeze = false, usePaths = true, allowTs, adopt = false, noImportCheck = false } = options;
|
|
3316
3540
|
// Show tool version only for recursive dep calls (CLI already prints it at startup)
|
|
3317
3541
|
const toolVersion = getToolVersion();
|
|
3318
3542
|
if (!options._fromWorkspace && !options._fromCli) {
|
|
@@ -3455,6 +3679,41 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
|
|
|
3455
3679
|
console.log(colors.dim('Prescan: no issues found in dep graph.'));
|
|
3456
3680
|
}
|
|
3457
3681
|
}
|
|
3682
|
+
// Source-import scan — catches imports of packages not declared in
|
|
3683
|
+
// dependencies/devDependencies/peer/optional. Runs by default; skip with
|
|
3684
|
+
// -no-import-check. Only at the top level (deps in the cascade will be
|
|
3685
|
+
// scanned when their own globalize() runs).
|
|
3686
|
+
if (!noImportCheck && !cleanup && !options._fromDep && !options._fromWorkspace) {
|
|
3687
|
+
const scanPkg = readPackageJson(cwd);
|
|
3688
|
+
const undeclared = findUndeclaredImports(cwd, scanPkg);
|
|
3689
|
+
if (undeclared.length > 0) {
|
|
3690
|
+
console.log('');
|
|
3691
|
+
console.log(colors.yellow(`Undeclared imports found (${undeclared.length}):`));
|
|
3692
|
+
for (const u of undeclared) {
|
|
3693
|
+
console.log(colors.yellow(` ${u.name}`)
|
|
3694
|
+
+ colors.dim(` (${u.file}:${u.line})`));
|
|
3695
|
+
}
|
|
3696
|
+
console.log(colors.dim(' These packages are imported but not in any package.json dep bucket.'));
|
|
3697
|
+
console.log(colors.dim(' After publish, `npm install` won\'t pull them and the package will'));
|
|
3698
|
+
console.log(colors.dim(' crash with ERR_MODULE_NOT_FOUND on a clean install.'));
|
|
3699
|
+
const interactive = !init && !options.autoInit && !publishDepsYes && !dryRun;
|
|
3700
|
+
if (interactive) {
|
|
3701
|
+
const ok = await confirm('Continue anyway?', false);
|
|
3702
|
+
if (!ok) {
|
|
3703
|
+
console.log('Aborted. Add the missing packages to dependencies and re-run.');
|
|
3704
|
+
console.log(colors.dim(' Tip: for siblings, use "file:../<dir>"; otherwise add the latest published version.'));
|
|
3705
|
+
return false;
|
|
3706
|
+
}
|
|
3707
|
+
}
|
|
3708
|
+
else {
|
|
3709
|
+
console.log(colors.yellow(' (non-interactive mode — continuing, but the install will likely fail at runtime)'));
|
|
3710
|
+
}
|
|
3711
|
+
console.log('');
|
|
3712
|
+
}
|
|
3713
|
+
else if (verbose) {
|
|
3714
|
+
console.log(colors.dim('Import scan: all imports are declared.'));
|
|
3715
|
+
}
|
|
3716
|
+
}
|
|
3458
3717
|
// Check ignore files first (unless cleanup mode)
|
|
3459
3718
|
if (!cleanup && !asis) {
|
|
3460
3719
|
const checkResult = checkIgnoreFiles(cwd, { conform, asis, verbose, allowTs });
|
|
@@ -3553,33 +3812,116 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
|
|
|
3553
3812
|
let justInitialized = false;
|
|
3554
3813
|
if (!gitStatus.isRepo) {
|
|
3555
3814
|
console.log('No git repository found.');
|
|
3556
|
-
|
|
3557
|
-
|
|
3815
|
+
// Probe for an existing remote we could adopt history from.
|
|
3816
|
+
// The package.json may already point at a GitHub repo from a previous
|
|
3817
|
+
// publish — adopting it preserves remote history (no force-push needed).
|
|
3818
|
+
const pkgForProbe = readPackageJson(cwd);
|
|
3819
|
+
const probeUrl = getPackageRepoUrl(pkgForProbe);
|
|
3820
|
+
let adoptable = null;
|
|
3821
|
+
if (probeUrl) {
|
|
3822
|
+
console.log(colors.dim(` Probing existing remote: ${probeUrl} ...`));
|
|
3823
|
+
const probed = probeRemote(probeUrl);
|
|
3824
|
+
if (probed) {
|
|
3825
|
+
adoptable = { url: probeUrl, defaultBranch: probed.defaultBranch };
|
|
3826
|
+
console.log(colors.green(` ✓ Remote reachable (default branch: ${probed.defaultBranch})`));
|
|
3827
|
+
}
|
|
3828
|
+
else {
|
|
3829
|
+
console.log(colors.dim(' Remote not reachable — falling back to fresh init options.'));
|
|
3830
|
+
}
|
|
3558
3831
|
}
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
|
|
3563
|
-
|
|
3564
|
-
|
|
3832
|
+
// Strict -adopt: require a reachable remote, never create a fresh repo
|
|
3833
|
+
if (adopt && !adoptable) {
|
|
3834
|
+
console.error(colors.red('ERROR: -adopt requires a reachable git remote in package.json.repository'));
|
|
3835
|
+
if (probeUrl) {
|
|
3836
|
+
console.error(colors.red(` Configured: ${probeUrl}`));
|
|
3837
|
+
console.error(colors.red(' Probe failed — check the URL, network, or credentials.'));
|
|
3565
3838
|
}
|
|
3566
|
-
|
|
3567
|
-
console.
|
|
3568
|
-
return false;
|
|
3839
|
+
else {
|
|
3840
|
+
console.error(colors.red(' No "repository" field found in package.json.'));
|
|
3569
3841
|
}
|
|
3570
|
-
|
|
3571
|
-
|
|
3842
|
+
console.error(colors.red(' Drop -adopt to fall back to fresh init, or fix the remote URL.'));
|
|
3843
|
+
return false;
|
|
3844
|
+
}
|
|
3845
|
+
if (dryRun) {
|
|
3846
|
+
if (adoptable) {
|
|
3847
|
+
console.log(` [dry-run] Would adopt history from ${adoptable.url}`);
|
|
3848
|
+
}
|
|
3849
|
+
else {
|
|
3850
|
+
console.log(' [dry-run] Would initialize git repository');
|
|
3572
3851
|
}
|
|
3573
|
-
|
|
3574
|
-
|
|
3852
|
+
}
|
|
3853
|
+
else if (adopt && adoptable) {
|
|
3854
|
+
// -adopt bypasses the prompt
|
|
3855
|
+
const success = await adoptExistingRemote(cwd, adoptable.url, adoptable.defaultBranch, dryRun);
|
|
3575
3856
|
if (!success)
|
|
3576
3857
|
return false;
|
|
3577
3858
|
justInitialized = true;
|
|
3578
3859
|
}
|
|
3860
|
+
else if (!init && !options.autoInit && !publishDepsYes) {
|
|
3861
|
+
let choice;
|
|
3862
|
+
if (adoptable) {
|
|
3863
|
+
choice = await promptChoice('How would you like to set up git?\n'
|
|
3864
|
+
+ ' 1) Adopt history from existing remote (recommended)\n'
|
|
3865
|
+
+ ' 2) Initialize fresh git repository\n'
|
|
3866
|
+
+ ' a) Adopt ALL (don\'t ask again for remaining deps)\n'
|
|
3867
|
+
+ ' 3) Use local install only (skip git/publish)\n'
|
|
3868
|
+
+ ' 4) Abort\nChoice:', ['1', '2', 'a', '3', '4', '']);
|
|
3869
|
+
if (choice === '3') {
|
|
3870
|
+
console.log(colors.dim('Switching to local-only mode...'));
|
|
3871
|
+
writeConfig(cwd, { ...configOptions, local: true }, new Set(['local']));
|
|
3872
|
+
return doLocalInstall(cwd, options);
|
|
3873
|
+
}
|
|
3874
|
+
if (choice === '4') {
|
|
3875
|
+
console.log('Aborted. Run with --init to initialize.');
|
|
3876
|
+
return false;
|
|
3877
|
+
}
|
|
3878
|
+
if (choice === 'a')
|
|
3879
|
+
options.autoInit = true;
|
|
3880
|
+
if (choice === '2') {
|
|
3881
|
+
const success = await initGit(cwd, gitVisibility, dryRun, allowTs);
|
|
3882
|
+
if (!success)
|
|
3883
|
+
return false;
|
|
3884
|
+
}
|
|
3885
|
+
else {
|
|
3886
|
+
// '1', 'a', '' default → adopt
|
|
3887
|
+
const success = await adoptExistingRemote(cwd, adoptable.url, adoptable.defaultBranch, dryRun);
|
|
3888
|
+
if (!success)
|
|
3889
|
+
return false;
|
|
3890
|
+
}
|
|
3891
|
+
}
|
|
3892
|
+
else {
|
|
3893
|
+
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', '']);
|
|
3894
|
+
if (choice === '2') {
|
|
3895
|
+
console.log(colors.dim('Switching to local-only mode...'));
|
|
3896
|
+
writeConfig(cwd, { ...configOptions, local: true }, new Set(['local']));
|
|
3897
|
+
return doLocalInstall(cwd, options);
|
|
3898
|
+
}
|
|
3899
|
+
if (choice === '3') {
|
|
3900
|
+
console.log('Aborted. Run with --init to initialize.');
|
|
3901
|
+
return false;
|
|
3902
|
+
}
|
|
3903
|
+
if (choice === 'a')
|
|
3904
|
+
options.autoInit = true;
|
|
3905
|
+
// choice is '1', 'a', or '' (default)
|
|
3906
|
+
const success = await initGit(cwd, gitVisibility, dryRun, allowTs);
|
|
3907
|
+
if (!success)
|
|
3908
|
+
return false;
|
|
3909
|
+
}
|
|
3910
|
+
justInitialized = true;
|
|
3911
|
+
}
|
|
3579
3912
|
else {
|
|
3580
|
-
|
|
3581
|
-
|
|
3582
|
-
|
|
3913
|
+
// Auto path (--init, autoInit, publishDepsYes): prefer adoption if a
|
|
3914
|
+
// reachable remote was found in package.json.
|
|
3915
|
+
if (adoptable) {
|
|
3916
|
+
const success = await adoptExistingRemote(cwd, adoptable.url, adoptable.defaultBranch, dryRun);
|
|
3917
|
+
if (!success)
|
|
3918
|
+
return false;
|
|
3919
|
+
}
|
|
3920
|
+
else {
|
|
3921
|
+
const success = await initGit(cwd, gitVisibility, dryRun, allowTs);
|
|
3922
|
+
if (!success)
|
|
3923
|
+
return false;
|
|
3924
|
+
}
|
|
3583
3925
|
justInitialized = true;
|
|
3584
3926
|
}
|
|
3585
3927
|
}
|
|
@@ -4239,7 +4581,8 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
|
|
|
4239
4581
|
forcePublish, // Propagate so transitive deps get force-published too
|
|
4240
4582
|
_fromDep: true, // Suppress nested prescan
|
|
4241
4583
|
rebase, // Propagate so behind-remote deps get rebased automatically
|
|
4242
|
-
autoInit: options.autoInit // Propagate "init all" choice
|
|
4584
|
+
autoInit: options.autoInit, // Propagate "init all" choice
|
|
4585
|
+
adopt // Propagate strict adopt-or-abort flag through cascade
|
|
4243
4586
|
});
|
|
4244
4587
|
if (!depSuccess) {
|
|
4245
4588
|
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.
|
|
3
|
+
"version": "1.0.180",
|
|
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
|
+
"@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.
|
|
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",
|