@s2j/global-npm 2.0.2 → 2.1.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  # 📦 Global npm Package Setup
2
2
 
3
3
  Node.js の **グローバル npm パッケージ管理** を `package.json` で一元化する CLI です。
4
- macOS / Windows 11で同じ `global-npm check|update|install` フローを使えます。
4
+ macOS / Windows 11で同じ `global-npm` フローを使えます。
5
5
 
6
6
  GitHub: [stein2nd/global-npm-setup](https://github.com/stein2nd/global-npm-setup)
7
7
 
@@ -11,9 +11,11 @@ GitHub: [stein2nd/global-npm-setup](https://github.com/stein2nd/global-npm-setup
11
11
  global-npm check # グローバルパッケージの更新確認 (ncu)
12
12
  global-npm update # package.json のバージョン範囲を更新 (ncu -u)
13
13
  global-npm install # dependencies を列挙して npm install -g を実行
14
+ global-npm sync # upstream + user-deps → 物理的なアーカイブを再生成
15
+ global-npm add # user-deps.json にパッケージを追記
14
16
  ```
15
17
 
16
- 定番フロー:
18
+ 定番フローは、下記の順番になるかと思います。なお、`install` 単体では ncu は実行しません。
17
19
 
18
20
  ```sh
19
21
  global-npm check
@@ -21,7 +23,22 @@ global-npm update
21
23
  global-npm install
22
24
  ```
23
25
 
24
- `install` 単体では ncu は実行しません。
26
+ ## しくみ (v2.1)
27
+
28
+ | レイヤ | 場所 | 役割 |
29
+ |--------|------|------|
30
+ | Upstream 正本 | `@s2j/global-npm` 同梱 `package.json` | 公式 `dependencies` 一覧 |
31
+ | ユーザー overlay | `$SETUP_DIR/user-deps.json` | 追加分、ピン留め |
32
+ | 物理的なアーカイブ | `$SETUP_DIR/package.json` | ncu / install が読む実効マニフェスト |
33
+
34
+ **setup ディレクトリ (`$SETUP_DIR`) のデフォルト**
35
+
36
+ | OS | パス |
37
+ |----|------|
38
+ | macOS / Linux | `~/.config/global-npm` |
39
+ | Windows 11 | `%APPDATA%\global-npm` |
40
+
41
+ `GLOBAL_NPM_SETUP_DIR` 環境変数で上書きできます。詳細は [docs/layout.md](./docs/layout.md)。
25
42
 
26
43
  ## セットアップ
27
44
 
@@ -36,8 +53,9 @@ Homebrew、Node.js v18以降が未導入の場合は、先にインストール
36
53
  2. `node --version`、`npm --version` でバージョンを確認する。
37
54
  3. 移行する場合、`~/bin/global-npm` (Zsh ラッパー) が PATH に残っていないか確認する。残っている場合は削除する (npm グローバル bin の `global-npm` と競合する場合がある)。
38
55
 
39
- 推奨配置: `~/dotfiles/global-npm-setup/`
40
- npm グローバル bin: 通常 `$(npm prefix -g)/bin` (nvm 利用時はその Node に紐づく prefix)
56
+ * 推奨配置 (開発): `~/dotfiles/global-npm-setup/`
57
+ * setup ディレクトリ: `~/.config/global-npm`
58
+ * npm グローバル bin: 通常 `$(npm prefix -g)/bin` (nvm 利用時はその Node に紐づく prefix)
41
59
 
42
60
  ### Windows での下準備
43
61
 
@@ -52,62 +70,93 @@ Node.js v18以上 (LTS 推奨) が未導入の場合は、先にインストー
52
70
  3. `node --version`、`npm --version` でバージョンを確認する。
53
71
  4. PowerShell を再起動後、`global-npm check` (導入後) で PATH を確認する。
54
72
 
55
- 推奨配置: `%USERPROFILE%\dotfiles\global-npm-setup\`
56
- npm グローバル bin: `%AppData%\npm` (通常 PATH に含まれる)
73
+ * 推奨配置 (開発): `%USERPROFILE%\dotfiles\global-npm-setup\`
74
+ * setup ディレクトリ: `%APPDATA%\global-npm`
75
+ * npm グローバル bin: `%AppData%\npm` (通常 PATH に含まれる)
57
76
 
58
77
  ### npm パッケージの導入
59
78
 
60
- 初回 publish 後:
61
-
62
79
  ```sh
63
- npm install -g @s2j/global-npm
64
- global-npm install
80
+ npm install -g @s2j/global-npm # CLI 本体の導入 (これは一度だけ)
81
+ global-npm add @s2j/docs-linter@^1.0.16 # 任意: ユーザー追加分
82
+ global-npm install # 物理的なアーカイブ化 dependencies を global install
65
83
  ```
66
84
 
85
+ 初回の `global-npm install` で `~/.config/global-npm/` (Windows 11では `%APPDATA%\global-npm\`) に物理的なアーカイブ化 `package.json` が生成されます。
86
+ `global-npm install` は、`@s2j/global-npm` 自身 (自己参照) も含め、`dependencies` のキーを列挙して `npm install -g` します。
87
+
67
88
  ### 開発 — リポジトリ clone
68
89
 
69
90
  ```sh
70
91
  git clone https://github.com/stein2nd/global-npm-setup.git
71
92
  cd global-npm-setup
72
93
  npm link
94
+ GLOBAL_NPM_SETUP_DIR=.sandbox/setup global-npm sync
73
95
  global-npm install
74
96
  ```
75
97
 
76
- ## package.json の役割
98
+ ## ユーザー追加分の管理
77
99
 
78
- 同梱の `package.json` の `dependencies` が、グローバルインストール対象パッケージ一覧です。
79
- `check` / `update` / `install` はいずれもこの一覧を参照します。
100
+ ```sh
101
+ # 追加分を登録 (range 省略時は npm view で ^x.y.z、失敗時は *)
102
+ global-npm add typescript
103
+ global-npm add eslint --dev
80
104
 
81
- 一覧の変更はリポジトリ更新 → npm publish → `npm update -g @s2j/global-npm` で各環境に反映します。
105
+ # マージ結果を確認 (書き込みなし)
106
+ global-npm sync --dry-run
107
+ ```
82
108
 
83
- ## 開発用 scripts
109
+ * upstream (`npm update -g @s2j/global-npm`) 更新後も、ユーザー追加分は消えない。
110
+ * upstream 管理パッケージのうち未 update 分は、次回 `check` 時の sync で新 range に追従する。
111
+ * upstream パッケージをピン留めする場合は、`user-deps.json` の `dependencies` に同名で range を書く。
84
112
 
85
- CLI 実装のデバッグ用に、リポジトリ root から次の npm scripts も使えます。
113
+ ## 日常の更新サイクル
86
114
 
87
- ```sh
88
- npm run ncu:check
89
- npm run ncu:update
115
+ ```mermaid
116
+ flowchart LR
117
+ A["global-npm check"] --> B{"更新あり?"}
118
+ B -->|Yes| C["global-npm update"]
119
+ C --> D["global-npm install"]
120
+ B -->|No| E["終了"]
90
121
  ```
91
122
 
92
- ## グローバルパッケージ一覧の初期取り込み (任意)
123
+ * **check:** sync 後に ncu で更新候補を表示。ncu 自体は range を書き換えない。
124
+ * **update:** 物理的なアーカイブ化 `dependencies` / `devDependencies` の range を更新。
125
+ * **install:** 物理的なアーカイブ化 **dependencies** のみ global install。
126
+
127
+ ## 開発用 scripts
93
128
 
94
- 手元のグローバル npm パッケージを `dependencies` に反映する作業です。CLI 実行には jq は不要ですが、この作業で jq を使う場合は任意です。
129
+ CLI 実装のデバッグ用に、リポジトリ root から下記の npm scripts も使えます。
95
130
 
96
131
  ```sh
97
- npm ls -g --depth=0 --json > tmp.json
98
- jq '.dependencies | to_entries | map({ key: .key, value: ("^" + (.value.version | tostring)) }) | from_entries' tmp.json > deps.json
99
- jq '.dependencies = input' package.json deps.json > package.tmp.json && mv package.tmp.json package.json
100
- rm tmp.json deps.json
132
+ npm run ncu:check
133
+ npm run ncu:update
101
134
  ```
102
135
 
103
- ## 移行 (v1→ v2)
136
+ ## 移行
137
+
138
+ ### v1→ v2
104
139
 
105
140
  | v1 | v2 |
106
141
  |----|-----|
107
142
  | `~/bin/global-npm` (Zsh) | 廃止 — `npm install -g @s2j/global-npm` |
108
143
  | `install-global.zsh` | 廃止 — `global-npm install` |
109
144
  | `ncu:install` (jq 列挙) | `global-npm install` (Node 列挙) |
110
- | SETUP_DIR = ラッパー配置 dir (`~/bin`) | package root (npm global install 先) を参照 |
145
+
146
+ ### v2.0.x → v2.1
147
+
148
+ | v2.0.x | v2.1 |
149
+ |--------|------|
150
+ | setup = package root | setup = `~/.config/global-npm`。Windows は `%APPDATA%\global-npm` |
151
+ | 同梱 `package.json` を直接 ncu | 物理的なアーカイブを ncu |
152
+ | 3サブコマンド | 5サブコマンド (`sync` / `add` 追加) |
153
+
154
+ 移行手順は、下記のとおりです。
155
+
156
+ 1. `npm update -g @s2j/global-npm` で v2.1に上げる。
157
+ 2. 追加分を `global-npm add …` で登録する。
158
+ 3. `global-npm sync` で 物理的なアーカイブを生成する。
159
+ 4. `global-npm install` で global 環境を同期する。
111
160
 
112
161
  ## ライセンス
113
162
 
@@ -1,19 +1,27 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  const { spawnSync } = require('child_process');
4
- const fs = require('fs');
5
4
  const path = require('path');
6
5
 
7
- const setupDir = path.resolve(__dirname, '..');
8
- const pkgPath = path.join(setupDir, 'package.json');
6
+ const { toGlobalInstallSpec } = require('../lib/install-spec.cjs');
7
+ const { ensureSetupDir, readDependencies, readUserDeps, writeJson } = require('../lib/pkg-io.cjs');
8
+ const { resolveSetupContext } = require('../lib/paths.cjs');
9
+ const { parseAddSpec, resolveDefaultRange } = require('../lib/resolve-range.cjs');
10
+ const { formatReport, syncManifest } = require('../lib/sync-manifest.cjs');
11
+
12
+ const packageRoot = path.resolve(__dirname, '..');
13
+ const ctx = resolveSetupContext(packageRoot);
14
+ const { pkgPath } = ctx;
9
15
  const shell = process.platform === 'win32';
10
16
 
11
17
  function usage() {
12
- console.error(`Usage: global-npm <check|update|install>
18
+ console.error(`Usage: global-npm <check|update|install|sync|add>
13
19
 
14
20
  check Check for available updates (ncu -g)
15
21
  update Update version ranges in package.json (ncu -g -u)
16
- install Install dependencies globally (npm install -g <each>)`);
22
+ install Install dependencies globally (npm install -g <name>@<range>…)
23
+ sync Merge upstream + user-deps into materialized package.json
24
+ add Add a package to user-deps.json (optional: --dev)`);
17
25
  process.exit(1);
18
26
  }
19
27
 
@@ -31,39 +39,101 @@ function run(command, args) {
31
39
  process.exit(result.status ?? 1);
32
40
  }
33
41
 
42
+ function prepare() {
43
+ ensureSetupDir(ctx);
44
+ return syncManifest(ctx);
45
+ }
46
+
47
+ function handleSync(dryRun) {
48
+ ensureSetupDir(ctx);
49
+ const { changed, report } = syncManifest(ctx, { dryRun });
50
+
51
+ if (dryRun) {
52
+ const lines = formatReport(report);
53
+ if (lines.length === 0) {
54
+ console.error('No changes.');
55
+ } else {
56
+ for (const line of lines) {
57
+ console.error(line);
58
+ }
59
+ }
60
+ process.exit(0);
61
+ }
62
+
63
+ if (changed) {
64
+ console.error(`Synced materialized manifest: ${pkgPath}`);
65
+ }
66
+
67
+ process.exit(0);
68
+ }
69
+
70
+ function handleAdd(args) {
71
+ const isDev = args.includes('--dev');
72
+ const positional = args.filter((arg) => arg !== '--dev');
73
+
74
+ if (positional.length !== 1) {
75
+ console.error('Usage: global-npm add <pkg>[@range] [--dev]');
76
+ process.exit(1);
77
+ }
78
+
79
+ const { name, range: explicitRange } = parseAddSpec(positional[0]);
80
+ if (!name) {
81
+ console.error('Package name is required.');
82
+ process.exit(1);
83
+ }
84
+
85
+ const range =
86
+ explicitRange !== undefined ? explicitRange : resolveDefaultRange(name);
87
+
88
+ ensureSetupDir(ctx);
89
+ const userDeps = readUserDeps(ctx);
90
+ const section = isDev ? 'devDependencies' : 'dependencies';
91
+ userDeps[section][name] = range;
92
+ writeJson(ctx.userDepsPath, userDeps);
93
+
94
+ syncManifest(ctx);
95
+ console.error(`Added ${name}@${range} to user-deps.json (${section}).`);
96
+ process.exit(0);
97
+ }
98
+
34
99
  const subcommand = process.argv[2];
100
+ const restArgs = process.argv.slice(3);
35
101
 
36
102
  switch (subcommand) {
37
103
  case 'check':
104
+ prepare();
38
105
  run('ncu', ['-g', '--format', 'time', '--packageFile', pkgPath]);
39
106
  break;
40
107
 
41
108
  case 'update':
109
+ prepare();
42
110
  run('ncu', ['-g', '--format', 'time', '-u', '--packageFile', pkgPath]);
43
111
  break;
44
112
 
45
113
  case 'install': {
46
- let dependencies;
47
-
48
- try {
49
- const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
50
- dependencies = pkg.dependencies ?? {};
51
- } catch (err) {
52
- console.error(`Failed to read ${pkgPath}: ${err.message}`);
53
- process.exit(1);
54
- }
114
+ prepare();
115
+ const dependencies = readDependencies(pkgPath);
116
+ const specs = Object.entries(dependencies).map(([name, versionRange]) =>
117
+ toGlobalInstallSpec(name, versionRange),
118
+ );
55
119
 
56
- const names = Object.keys(dependencies);
57
-
58
- if (names.length === 0) {
120
+ if (specs.length === 0) {
59
121
  console.error('No dependencies to install.');
60
122
  process.exit(1);
61
123
  }
62
124
 
63
- run('npm', ['install', '-g', ...names]);
125
+ run('npm', ['install', '-g', ...specs]);
64
126
  break;
65
127
  }
66
128
 
129
+ case 'sync':
130
+ handleSync(restArgs.includes('--dry-run'));
131
+ break;
132
+
133
+ case 'add':
134
+ handleAdd(restArgs);
135
+ break;
136
+
67
137
  default:
68
138
  usage();
69
139
  }
@@ -0,0 +1,13 @@
1
+ 'use strict';
2
+
3
+ function toGlobalInstallSpec(name, versionRange) {
4
+ if (typeof versionRange === 'string' && versionRange.trim() !== '') {
5
+ return `${name}@${versionRange}`;
6
+ }
7
+
8
+ return name;
9
+ }
10
+
11
+ module.exports = {
12
+ toGlobalInstallSpec,
13
+ };
package/lib/paths.cjs ADDED
@@ -0,0 +1,34 @@
1
+ 'use strict';
2
+
3
+ const os = require('os');
4
+ const path = require('path');
5
+
6
+ function defaultSetupDir() {
7
+ if (process.platform === 'win32') {
8
+ const appData =
9
+ process.env.APPDATA ?? path.join(os.homedir(), 'AppData', 'Roaming');
10
+ return path.join(appData, 'global-npm');
11
+ }
12
+
13
+ return path.join(os.homedir(), '.config', 'global-npm');
14
+ }
15
+
16
+ function resolveSetupContext(packageRoot) {
17
+ const setupDir = path.resolve(
18
+ process.env.GLOBAL_NPM_SETUP_DIR?.trim() || defaultSetupDir(),
19
+ );
20
+
21
+ return {
22
+ packageRoot,
23
+ setupDir,
24
+ upstreamPkgPath: path.join(packageRoot, 'package.json'),
25
+ pkgPath: path.join(setupDir, 'package.json'),
26
+ userDepsPath: path.join(setupDir, 'user-deps.json'),
27
+ metaPath: path.join(setupDir, '.upstream-meta.json'),
28
+ };
29
+ }
30
+
31
+ module.exports = {
32
+ defaultSetupDir,
33
+ resolveSetupContext,
34
+ };
package/lib/pkg-io.cjs ADDED
@@ -0,0 +1,57 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ const EMPTY_USER_DEPS = {
7
+ dependencies: {},
8
+ devDependencies: {},
9
+ };
10
+
11
+ function readJson(filePath) {
12
+ if (!fs.existsSync(filePath)) {
13
+ return null;
14
+ }
15
+
16
+ const raw = fs.readFileSync(filePath, 'utf8');
17
+ return JSON.parse(raw);
18
+ }
19
+
20
+ function writeJson(filePath, data) {
21
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
22
+ fs.writeFileSync(filePath, `${JSON.stringify(data, null, 2)}\n`, 'utf8');
23
+ }
24
+
25
+ function ensureSetupDir(ctx) {
26
+ fs.mkdirSync(ctx.setupDir, { recursive: true });
27
+
28
+ if (!fs.existsSync(ctx.userDepsPath)) {
29
+ writeJson(ctx.userDepsPath, { ...EMPTY_USER_DEPS });
30
+ }
31
+ }
32
+
33
+ function readUserDeps(ctx) {
34
+ const data = readJson(ctx.userDepsPath);
35
+ return {
36
+ dependencies: { ...(data?.dependencies ?? {}) },
37
+ devDependencies: { ...(data?.devDependencies ?? {}) },
38
+ };
39
+ }
40
+
41
+ function readDependencies(pkgPath) {
42
+ const pkg = readJson(pkgPath);
43
+ if (!pkg) {
44
+ return {};
45
+ }
46
+
47
+ return pkg.dependencies ?? {};
48
+ }
49
+
50
+ module.exports = {
51
+ EMPTY_USER_DEPS,
52
+ readJson,
53
+ writeJson,
54
+ ensureSetupDir,
55
+ readUserDeps,
56
+ readDependencies,
57
+ };
@@ -0,0 +1,53 @@
1
+ 'use strict';
2
+
3
+ const { spawnSync } = require('child_process');
4
+
5
+ const shell = process.platform === 'win32';
6
+
7
+ function resolveDefaultRange(pkgName, { spawn = spawnSync, log = console.error } = {}) {
8
+ const result = spawn('npm', ['view', pkgName, 'version', '--json'], {
9
+ encoding: 'utf8',
10
+ shell,
11
+ });
12
+
13
+ if (result.status === 0) {
14
+ try {
15
+ const version = JSON.parse(result.stdout.trim());
16
+ if (typeof version === 'string' && version) {
17
+ return `^${version}`;
18
+ }
19
+ } catch {
20
+ // fall through to wildcard fallback
21
+ }
22
+ }
23
+
24
+ log(`Warning: could not resolve latest version for ${pkgName}; using "*".`);
25
+ return '*';
26
+ }
27
+
28
+ function parseAddSpec(arg) {
29
+ if (!arg || typeof arg !== 'string') {
30
+ return { name: '', range: undefined };
31
+ }
32
+
33
+ if (!arg.startsWith('@')) {
34
+ const at = arg.lastIndexOf('@');
35
+ if (at > 0) {
36
+ return { name: arg.slice(0, at), range: arg.slice(at + 1) };
37
+ }
38
+ return { name: arg, range: undefined };
39
+ }
40
+
41
+ const slash = arg.indexOf('/');
42
+ const at = arg.lastIndexOf('@');
43
+ if (at > 0 && at > slash) {
44
+ return { name: arg.slice(0, at), range: arg.slice(at + 1) };
45
+ }
46
+
47
+ return { name: arg, range: undefined };
48
+ }
49
+
50
+ module.exports = {
51
+ resolveDefaultRange,
52
+ parseAddSpec,
53
+ };
@@ -0,0 +1,221 @@
1
+ 'use strict';
2
+
3
+ const { readJson, writeJson } = require('./pkg-io.cjs');
4
+
5
+ const MATERIALIZED_NAME = 'global-npm-user-manifest';
6
+
7
+ function mergeDependencies({ upstream, current, meta, userDeps }) {
8
+ const merged = {};
9
+ const upstreamDeps = upstream.dependencies ?? {};
10
+ const userDepOverrides = userDeps.dependencies ?? {};
11
+ const prevUpstream = meta?.dependencies ?? {};
12
+ const currentDeps = current?.dependencies ?? {};
13
+
14
+ for (const [name, upstreamRange] of Object.entries(upstreamDeps)) {
15
+ if (Object.hasOwn(userDepOverrides, name)) {
16
+ merged[name] = userDepOverrides[name];
17
+ continue;
18
+ }
19
+
20
+ const currentRange = currentDeps[name];
21
+ const prevRange = prevUpstream[name];
22
+
23
+ if (
24
+ currentRange !== undefined &&
25
+ prevRange !== undefined &&
26
+ currentRange !== prevRange
27
+ ) {
28
+ merged[name] = currentRange;
29
+ } else {
30
+ merged[name] = upstreamRange;
31
+ }
32
+ }
33
+
34
+ for (const [name, range] of Object.entries(userDepOverrides)) {
35
+ if (!Object.hasOwn(upstreamDeps, name)) {
36
+ merged[name] = range;
37
+ }
38
+ }
39
+
40
+ for (const [name, range] of Object.entries(currentDeps)) {
41
+ if (Object.hasOwn(merged, name)) {
42
+ continue;
43
+ }
44
+
45
+ const wasUpstream = Object.hasOwn(prevUpstream, name);
46
+ const stillUpstream = Object.hasOwn(upstreamDeps, name);
47
+
48
+ if (wasUpstream && !stillUpstream && !Object.hasOwn(userDepOverrides, name)) {
49
+ continue;
50
+ }
51
+
52
+ merged[name] = range;
53
+ }
54
+
55
+ return merged;
56
+ }
57
+
58
+ function mergeDevDependencies({ current, meta, userDeps }) {
59
+ const merged = {};
60
+ const userDevDeps = userDeps.devDependencies ?? {};
61
+ const prevUserDevDeps = meta?.userDeps?.devDependencies ?? {};
62
+ const currentDevDeps = current?.devDependencies ?? {};
63
+
64
+ for (const [name, userRange] of Object.entries(userDevDeps)) {
65
+ const currentRange = currentDevDeps[name];
66
+ const prevRange = prevUserDevDeps[name];
67
+
68
+ if (
69
+ currentRange !== undefined &&
70
+ prevRange !== undefined &&
71
+ currentRange !== prevRange
72
+ ) {
73
+ merged[name] = currentRange;
74
+ } else {
75
+ merged[name] = userRange;
76
+ }
77
+ }
78
+
79
+ for (const [name, range] of Object.entries(currentDevDeps)) {
80
+ if (Object.hasOwn(merged, name) || Object.hasOwn(userDevDeps, name)) {
81
+ continue;
82
+ }
83
+
84
+ if (Object.hasOwn(prevUserDevDeps, name)) {
85
+ continue;
86
+ }
87
+
88
+ merged[name] = range;
89
+ }
90
+
91
+ return merged;
92
+ }
93
+
94
+ function diffSections(before = {}, after = {}) {
95
+ const added = [];
96
+ const updated = [];
97
+ const removed = [];
98
+
99
+ for (const [name, range] of Object.entries(after)) {
100
+ if (!Object.hasOwn(before, name)) {
101
+ added.push({ name, range });
102
+ } else if (before[name] !== range) {
103
+ updated.push({ name, from: before[name], to: range });
104
+ }
105
+ }
106
+
107
+ for (const name of Object.keys(before)) {
108
+ if (!Object.hasOwn(after, name)) {
109
+ removed.push({ name, range: before[name] });
110
+ }
111
+ }
112
+
113
+ return { added, updated, removed };
114
+ }
115
+
116
+ function buildReport(beforePkg, afterPkg) {
117
+ return {
118
+ dependencies: diffSections(beforePkg?.dependencies, afterPkg.dependencies),
119
+ devDependencies: diffSections(
120
+ beforePkg?.devDependencies,
121
+ afterPkg.devDependencies,
122
+ ),
123
+ };
124
+ }
125
+
126
+ function hasReportChanges(report) {
127
+ for (const section of Object.values(report)) {
128
+ if (section.added.length > 0 || section.updated.length > 0 || section.removed.length > 0) {
129
+ return true;
130
+ }
131
+ }
132
+
133
+ return false;
134
+ }
135
+
136
+ function formatReport(report) {
137
+ const lines = [];
138
+
139
+ for (const [sectionName, section] of Object.entries(report)) {
140
+ for (const { name, range } of section.added) {
141
+ lines.push(`+ ${sectionName} ${name}@${range}`);
142
+ }
143
+ for (const { name, from, to } of section.updated) {
144
+ lines.push(`~ ${sectionName} ${name}: ${from} -> ${to}`);
145
+ }
146
+ for (const { name, range } of section.removed) {
147
+ lines.push(`- ${sectionName} ${name}@${range}`);
148
+ }
149
+ }
150
+
151
+ return lines;
152
+ }
153
+
154
+ function buildMaterializedPkg(merged) {
155
+ return {
156
+ name: MATERIALIZED_NAME,
157
+ private: true,
158
+ dependencies: merged.dependencies,
159
+ devDependencies: merged.devDependencies,
160
+ };
161
+ }
162
+
163
+ function syncManifest(ctx, { dryRun = false } = {}) {
164
+ const upstream = readJson(ctx.upstreamPkgPath);
165
+ if (!upstream) {
166
+ throw new Error(`Failed to read upstream package.json: ${ctx.upstreamPkgPath}`);
167
+ }
168
+
169
+ const userDeps = readJson(ctx.userDepsPath) ?? {
170
+ dependencies: {},
171
+ devDependencies: {},
172
+ };
173
+ const normalizedUserDeps = {
174
+ dependencies: { ...(userDeps.dependencies ?? {}) },
175
+ devDependencies: { ...(userDeps.devDependencies ?? {}) },
176
+ };
177
+
178
+ const current = readJson(ctx.pkgPath);
179
+ const meta = readJson(ctx.metaPath);
180
+
181
+ const merged = {
182
+ dependencies: mergeDependencies({
183
+ upstream,
184
+ current,
185
+ meta,
186
+ userDeps: normalizedUserDeps,
187
+ }),
188
+ devDependencies: mergeDevDependencies({
189
+ current,
190
+ meta,
191
+ userDeps: normalizedUserDeps,
192
+ }),
193
+ };
194
+
195
+ const nextPkg = buildMaterializedPkg(merged);
196
+ const report = buildReport(current, nextPkg);
197
+ const changed = hasReportChanges(report);
198
+
199
+ if (!dryRun) {
200
+ writeJson(ctx.pkgPath, nextPkg);
201
+ writeJson(ctx.metaPath, {
202
+ upstreamVersion: upstream.version,
203
+ dependencies: { ...(upstream.dependencies ?? {}) },
204
+ userDeps: {
205
+ dependencies: { ...normalizedUserDeps.dependencies },
206
+ devDependencies: { ...normalizedUserDeps.devDependencies },
207
+ },
208
+ });
209
+ }
210
+
211
+ return { changed, report, pkgPath: ctx.pkgPath, nextPkg };
212
+ }
213
+
214
+ module.exports = {
215
+ MATERIALIZED_NAME,
216
+ mergeDependencies,
217
+ mergeDevDependencies,
218
+ buildMaterializedPkg,
219
+ syncManifest,
220
+ formatReport,
221
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@s2j/global-npm",
3
- "version": "2.0.2",
3
+ "version": "2.1.0",
4
4
  "description": "Manage globally installed npm packages via package.json with ncu.",
5
5
  "author": "Koutarou ISHIKAWA <stein2nd@gmail.com>",
6
6
  "type": "commonjs",
@@ -10,6 +10,7 @@
10
10
  },
11
11
  "files": [
12
12
  "bin/",
13
+ "lib/",
13
14
  "package.json",
14
15
  "LICENSE",
15
16
  "README.md"
@@ -34,14 +35,14 @@
34
35
  "node": ">=18"
35
36
  },
36
37
  "scripts": {
37
- "test": "node --test test/spec-compliance.test.cjs",
38
+ "test": "node --test test/*.test.cjs",
38
39
  "lint": "eslint .",
39
40
  "lint:fix": "eslint . --fix",
40
- "lint:docs": "s2j-docs-linter --profile base ./README.md ./docsMod/**/*.md ./.vscode/**/*.md",
41
+ "lint:docs": "s2j-docs-linter --profile base ./README.md ./CHANGELOG.md ./docs/**/*.md ./docsMod/status.md ./docsMod/modification.md ./docsMod/mod-overlay-manifest.md ./.vscode/**/*.md",
41
42
  "format": "prettier --write \"**/*.{cjs,js,mjs,json}\"",
42
43
  "format:check": "prettier --check \"**/*.{cjs,js,mjs,json}\"",
43
- "pack": "npm pack --pack-destination artifacts",
44
- "pack:dry-run": "npm pack --dry-run --pack-destination artifacts",
44
+ "pack": "mkdir -p artifacts && npm pack --pack-destination artifacts",
45
+ "pack:dry-run": "mkdir -p artifacts && npm pack --dry-run --pack-destination artifacts",
45
46
  "ncu:check": "ncu -g --format time --packageFile package.json",
46
47
  "ncu:update": "ncu -g --format time -u --packageFile package.json"
47
48
  },
@@ -55,7 +56,7 @@
55
56
  "typescript": "^5.9.3"
56
57
  },
57
58
  "dependencies": {
58
- "@s2j/global-npm": "^2.0.1",
59
+ "@s2j/global-npm": "^2.0.2",
59
60
  "corepack": "^0.35.0",
60
61
  "editorconfig-cli": "^1.0.0",
61
62
  "envinfo": "^7.21.0",