x7-code-line 0.1.0 → 0.3.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
@@ -18,6 +18,20 @@
18
18
 
19
19
  作为 npm 依赖安装时,`postinstall` 脚本使用 `INIT_CWD` 作为当前安装目录,也就是执行 `npm install` 时所在的项目目录。
20
20
 
21
+ 如果只是全局安装:
22
+
23
+ ```sh
24
+ npm install -g x7-code-line
25
+ ```
26
+
27
+ 此时只安装 CLI,不会自动为任何项目写入 hooks。全局安装完成后,需要手动执行:
28
+
29
+ ```sh
30
+ x7-code-line install --dir <absolute-path>
31
+ ```
32
+
33
+ 只有在 `X7_CODE_LINE_DIRS` 或 `X7_CODE_LINE_CONFIG` 明确提供目标目录时,`postinstall` 才会继续执行项目安装逻辑。
34
+
21
35
  当前安装目录会创建或覆盖:
22
36
 
23
37
  ```text
@@ -100,6 +114,7 @@ git diff --no-ext-diff --unified=0 --no-color
100
114
  - 变更类型:`add` 或 `delete`
101
115
  - 行号
102
116
  - 行内容
117
+ - 该文件当前的修改时间
103
118
 
104
119
  如果该 ID 不在 AI diff 缓存中,就写入普通 diff 缓存:
105
120
 
@@ -146,6 +161,15 @@ git diff --cached --no-ext-diff --unified=0 --no-color
146
161
  .x7-code-line/pending-commit.json
147
162
  ```
148
163
 
164
+ 计算完成后,会清除当前仓库在以下两份中心缓存中的条目:
165
+
166
+ ```text
167
+ .x7-code-line/ai-diff-line-ids.json
168
+ .x7-code-line/normal-diff-line-ids.json
169
+ ```
170
+
171
+ 其他项目的缓存不会被修改。
172
+
149
173
  `commit-msg` 会读取该结果,并追加或替换 trailer:
150
174
 
151
175
  ```text
@@ -160,25 +184,45 @@ x7-ai-lines: 0
160
184
 
161
185
  ## 多项目安装
162
186
 
163
- 通过 CLI 参数指定多个目标目录:
187
+ `install` 至少需要传入一个目录;0 目录场景会直接失败。
188
+
189
+ 默认要求传入绝对路径:
190
+
191
+ ```sh
192
+ npm install -g x7-code-line --dir /abs/path/repo-a --dir /abs/path/repo-b
193
+ ```
194
+
195
+ 例如,将 `x7-code-line` 安装在 C 盘目录,但指定 D 盘的 Git 项目:
196
+
197
+ ```powershell
198
+ cd C:\x7-tools\x7-code-line-host
199
+ npm install -g x7-code-line --dir D:\work\repo-a --dir D:\work\repo-b
200
+ ```
201
+
202
+ 如果确实需要解析相对路径,必须显式传 `--relative`:
164
203
 
165
204
  ```sh
166
- npx --no-install x7-code-line install --dir ../repo-a --dir ../repo-b
205
+ npm install -g x7-code-line --relative --dir ../repo-a --dir ../repo-b
167
206
  ```
168
207
 
169
- 也可以在安装时通过环境变量指定。
208
+ 此时相对路径基于当前执行 `install` 命令的目录解析。
209
+
210
+ 也可以在安装时通过环境变量指定多个目标目录。
211
+
212
+ 如果通过环境变量或配置文件传相对路径,需要同时设置 `X7_CODE_LINE_RELATIVE_PATHS=1`。
170
213
 
171
- macOS / Linux 使用 `:` 作为目录分隔符:
214
+ macOS / Linux 示例:
172
215
 
173
216
  ```sh
174
- X7_CODE_LINE_DIRS="../repo-a:../repo-b" npm install x7-code-line
217
+ X7_CODE_LINE_RELATIVE_PATHS=1 X7_CODE_LINE_DIRS="../repo-a:../repo-b" npm install -g x7-code-line
175
218
  ```
176
219
 
177
220
  Windows 使用 `;` 作为目录分隔符:
178
221
 
179
222
  ```powershell
223
+ $env:X7_CODE_LINE_RELATIVE_PATHS = "1"
180
224
  $env:X7_CODE_LINE_DIRS = "../repo-a;../repo-b"
181
- npm install x7-code-line
225
+ npm install -g x7-code-line
182
226
  ```
183
227
 
184
228
  ## 配置文件
@@ -231,10 +275,40 @@ which npx
231
275
  $env:X7_CODE_LINE_DIRS = "D:\repo-a;D:\repo-b"
232
276
  ```
233
277
 
278
+ ## 增量添加目录
279
+
280
+ 在已经完成安装的当前安装目录下,可以使用 `addDir` 增量添加新的项目目录:
281
+
282
+ ```sh
283
+ x7-code-line addDir --dir /abs/path/repo-c
284
+ ```
285
+
286
+ 这个命令只做两件事:
287
+
288
+ - 把新目录追加到当前安装目录 `.x7-code-line/projects.json`
289
+ - 为新目录安装 `.git/hooks/pre-commit` 和 `.git/hooks/commit-msg`
290
+
291
+ 它不会重写当前安装目录的 `.cursor/hooks.json`、`.codex/hooks.json`,也不会重写已有项目集合。
292
+
293
+ `addDir` 会沿用与 `install` 相同的严格校验:
294
+
295
+ - 目录必须真实存在
296
+ - 目录必须可访问
297
+ - 目录必须包含可用的 `.git` 目录
298
+
299
+ 任一新增目录不满足条件时,命令直接失败,不会继续追加。
300
+
301
+ 默认只接受绝对路径;如果需要解析相对路径,需要显式传入:
302
+
303
+ ```sh
304
+ x7-code-line addDir --relative --dir ../repo-c
305
+ ```
306
+
234
307
  ## CLI 命令
235
308
 
236
309
  ```sh
237
- x7-code-line install [--config path] [--dir path ...]
310
+ x7-code-line install --dir <absolute-path> [--dir <absolute-path> ...] [--config path] [--relative]
311
+ x7-code-line addDir --dir <absolute-path> [--dir <absolute-path> ...] [--config path] [--relative]
238
312
  x7-code-line prompt-submit
239
313
  x7-code-line stop
240
314
  x7-code-line git-pre-commit
@@ -274,7 +348,7 @@ npm login
274
348
  确认 `package.json` 中的包名和版本号正确后,执行:
275
349
 
276
350
  ```sh
277
- npm publish
351
+ npm publish --access public
278
352
  ```
279
353
 
280
354
  如果 npm 账号开启了 2FA,通常需要附带 OTP:
@@ -291,7 +365,7 @@ npm publish --access public --otp <6位验证码>
291
365
 
292
366
  ### 更新已发布版本
293
367
 
294
- 每次发布新版本前,先提升版本号:
368
+ 每次发布新版本前,手动修改package.json版本号或命令提升版本号:
295
369
 
296
370
  ```sh
297
371
  npm version patch
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "x7-code-line",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "Install Cursor, Codex, and git hooks for x7 code line workflows.",
5
5
  "main": "src/index.js",
6
6
  "bin": {
package/src/cache.js CHANGED
@@ -67,6 +67,19 @@ function addProjectId(cache, projectDir, id) {
67
67
  return false;
68
68
  }
69
69
 
70
+ function clearProjectIds(cache, projectDir) {
71
+ const key = path.resolve(projectDir);
72
+ if (!cache.projects || typeof cache.projects !== "object" || Array.isArray(cache.projects)) {
73
+ cache.projects = {};
74
+ return false;
75
+ }
76
+ if (Object.prototype.hasOwnProperty.call(cache.projects, key)) {
77
+ delete cache.projects[key];
78
+ return true;
79
+ }
80
+ return false;
81
+ }
82
+
70
83
  function ensureJson(filePath, defaultValue) {
71
84
  if (!fs.existsSync(filePath)) {
72
85
  writeJson(filePath, defaultValue);
@@ -92,6 +105,7 @@ module.exports = {
92
105
  PENDING_COMMIT_FILE,
93
106
  PROJECTS_CACHE_FILE,
94
107
  addProjectId,
108
+ clearProjectIds,
95
109
  ensureCacheFiles,
96
110
  getCacheDir,
97
111
  getProjectIds,
package/src/cli.js CHANGED
@@ -3,11 +3,13 @@
3
3
  const fs = require("node:fs");
4
4
  const path = require("node:path");
5
5
  const { handlePromptSubmit, handleStop } = require("./agent-hooks");
6
- const { install } = require("./install");
6
+ const { addDirs, install } = require("./install");
7
7
  const { appendCommitTrailer } = require("./git-trailer");
8
8
  const {
9
9
  AI_DIFF_CACHE_FILE,
10
+ NORMAL_DIFF_CACHE_FILE,
10
11
  PENDING_COMMIT_FILE,
12
+ clearProjectIds,
11
13
  getCacheDir,
12
14
  getProjectIds,
13
15
  readLineIdCache
@@ -20,6 +22,8 @@ async function runCli(argv) {
20
22
  switch (command) {
21
23
  case "install":
22
24
  return install(parseInstallOptions(rest));
25
+ case "addDir":
26
+ return addDirs(parseInstallOptions(rest));
23
27
  case "prompt-submit":
24
28
  case "cursor-before-submit-prompt":
25
29
  case "codex-user-prompt-submit":
@@ -44,13 +48,16 @@ async function runCli(argv) {
44
48
  function parseInstallOptions(args) {
45
49
  const options = {
46
50
  targetDirs: [],
47
- configPath: undefined
51
+ configPath: undefined,
52
+ relativePaths: false
48
53
  };
49
54
 
50
55
  for (let index = 0; index < args.length; index += 1) {
51
56
  const arg = args[index];
52
57
  if (arg === "--config" || arg === "-c") {
53
58
  options.configPath = requireValue(args, (index += 1), arg);
59
+ } else if (arg === "--relative") {
60
+ options.relativePaths = true;
54
61
  } else if (arg === "--dir" || arg === "-d") {
55
62
  options.targetDirs.push(requireValue(args, (index += 1), arg));
56
63
  } else if (arg === "--dirs") {
@@ -75,6 +82,7 @@ async function runGitPreCommit() {
75
82
  const baseDir = getBaseDir();
76
83
  const projectDir = process.cwd();
77
84
  const aiCache = readLineIdCache(baseDir, AI_DIFF_CACHE_FILE);
85
+ const normalCache = readLineIdCache(baseDir, NORMAL_DIFF_CACHE_FILE);
78
86
  const aiIds = getProjectIds(aiCache, projectDir);
79
87
  const stagedIds = getGitDiffLineIds(projectDir, { staged: true });
80
88
  const aiLineCount = stagedIds.filter((id) => aiIds.has(id)).length;
@@ -83,6 +91,10 @@ async function runGitPreCommit() {
83
91
  pendingPath,
84
92
  `${JSON.stringify({ projects: { [path.resolve(projectDir)]: { aiLineCount } } }, null, 2)}\n`
85
93
  );
94
+ clearProjectIds(aiCache, projectDir);
95
+ clearProjectIds(normalCache, projectDir);
96
+ writeCacheFile(baseDir, AI_DIFF_CACHE_FILE, aiCache);
97
+ writeCacheFile(baseDir, NORMAL_DIFF_CACHE_FILE, normalCache);
86
98
 
87
99
  if (process.env.X7_CODE_LINE_DEBUG === "1") {
88
100
  console.error(`[x7-code-line] git pre-commit ai lines: ${aiLineCount}`);
@@ -113,10 +125,16 @@ function readPendingAiLineCount(baseDir, projectDir) {
113
125
  }
114
126
  }
115
127
 
128
+ function writeCacheFile(baseDir, fileName, cache) {
129
+ const cacheDir = getCacheDir(baseDir);
130
+ fs.writeFileSync(path.join(cacheDir, fileName), `${JSON.stringify(cache, null, 2)}\n`);
131
+ }
132
+
116
133
  function printHelp() {
117
134
  const text = [
118
135
  "Usage:",
119
- " x7-code-line install [--config path] [--dir path ...]",
136
+ " x7-code-line install --dir <absolute-path> [--dir <absolute-path> ...] [--config path] [--relative]",
137
+ " x7-code-line addDir --dir <absolute-path> [--dir <absolute-path> ...] [--config path] [--relative]",
120
138
  " x7-code-line prompt-submit",
121
139
  " x7-code-line stop",
122
140
  " x7-code-line git-pre-commit",
package/src/diff-lines.js CHANGED
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
 
3
3
  const crypto = require("node:crypto");
4
+ const fs = require("node:fs");
4
5
  const path = require("node:path");
5
6
  const { execFileSync } = require("node:child_process");
6
7
 
@@ -16,14 +17,15 @@ function getGitDiffLineIds(projectDir, options = {}) {
16
17
  stdio: ["ignore", "pipe", "ignore"]
17
18
  });
18
19
 
19
- return parseDiffLineIds(projectDir, diff);
20
+ return parseDiffLineIds(projectDir, diff, options);
20
21
  }
21
22
 
22
- function parseDiffLineIds(projectDir, diff) {
23
+ function parseDiffLineIds(projectDir, diff, options = {}) {
23
24
  const ids = [];
24
25
  let currentFile = "";
25
26
  let oldLine = 0;
26
27
  let newLine = 0;
28
+ const fileMtimes = new Map();
27
29
 
28
30
  for (const line of diff.split(/\r?\n/u)) {
29
31
  if (line.startsWith("+++ b/")) {
@@ -47,10 +49,21 @@ function parseDiffLineIds(projectDir, diff) {
47
49
  }
48
50
 
49
51
  if (line.startsWith("+") && !line.startsWith("+++")) {
50
- ids.push(makeLineId(projectDir, currentFile, "add", newLine, line.slice(1)));
52
+ ids.push(
53
+ makeLineId(projectDir, currentFile, "add", newLine, line.slice(1), getFileModifiedTime(projectDir, currentFile, fileMtimes))
54
+ );
51
55
  newLine += 1;
52
56
  } else if (line.startsWith("-") && !line.startsWith("---")) {
53
- ids.push(makeLineId(projectDir, currentFile, "delete", oldLine, line.slice(1)));
57
+ ids.push(
58
+ makeLineId(
59
+ projectDir,
60
+ currentFile,
61
+ "delete",
62
+ oldLine,
63
+ line.slice(1),
64
+ getFileModifiedTime(projectDir, currentFile, fileMtimes)
65
+ )
66
+ );
54
67
  oldLine += 1;
55
68
  } else if (line.startsWith(" ")) {
56
69
  oldLine += 1;
@@ -61,7 +74,7 @@ function parseDiffLineIds(projectDir, diff) {
61
74
  return ids;
62
75
  }
63
76
 
64
- function makeLineId(projectDir, filePath, changeType, lineNumber, content) {
77
+ function makeLineId(projectDir, filePath, changeType, lineNumber, content, modifiedTimeMs = 0) {
65
78
  return crypto
66
79
  .createHash("sha256")
67
80
  .update(
@@ -70,12 +83,31 @@ function makeLineId(projectDir, filePath, changeType, lineNumber, content) {
70
83
  file: filePath,
71
84
  changeType,
72
85
  lineNumber,
73
- content
86
+ content,
87
+ modifiedTimeMs
74
88
  })
75
89
  )
76
90
  .digest("hex");
77
91
  }
78
92
 
93
+ function getFileModifiedTime(projectDir, filePath, fileMtimes) {
94
+ const key = `${path.resolve(projectDir)}\0${filePath}`;
95
+ if (fileMtimes.has(key)) {
96
+ return fileMtimes.get(key);
97
+ }
98
+
99
+ const absolutePath = path.resolve(projectDir, filePath);
100
+ let modifiedTimeMs = 0;
101
+ try {
102
+ modifiedTimeMs = fs.statSync(absolutePath).mtimeMs;
103
+ } catch {
104
+ modifiedTimeMs = 0;
105
+ }
106
+
107
+ fileMtimes.set(key, modifiedTimeMs);
108
+ return modifiedTimeMs;
109
+ }
110
+
79
111
  module.exports = {
80
112
  getGitDiffLineIds,
81
113
  makeLineId,
package/src/install.js CHANGED
@@ -2,32 +2,70 @@
2
2
 
3
3
  const fs = require("node:fs");
4
4
  const path = require("node:path");
5
- const { CACHE_DIR_NAME, ensureCacheFiles, writeProjects } = require("./cache");
5
+ const { CACHE_DIR_NAME, ensureCacheFiles, readProjects, writeProjects } = require("./cache");
6
6
 
7
7
  const PACKAGE_ROOT = path.resolve(__dirname, "..");
8
8
 
9
9
  function install(options = {}) {
10
+ const pathBaseDir = path.resolve(options.pathBaseDir || process.env.INIT_CWD || process.cwd());
10
11
  const config = readConfig(options.configPath || process.env.X7_CODE_LINE_CONFIG);
11
- const targetDirs = normalizeTargetDirs(options.targetDirs, config);
12
- const cacheBaseDir = path.resolve(options.cacheBaseDir || process.env.INIT_CWD || process.cwd());
12
+ const targetDirs = normalizeTargetDirs(options.targetDirs, config, {
13
+ relativePaths: Boolean(options.relativePaths || process.env.X7_CODE_LINE_RELATIVE_PATHS === "1"),
14
+ pathBaseDir
15
+ });
16
+ const cacheBaseDir = path.resolve(options.cacheBaseDir || pathBaseDir);
17
+ const validatedTargets = validateTargets(targetDirs, config);
13
18
 
14
19
  installCurrentProjectFiles(cacheBaseDir);
15
- for (const targetDir of targetDirs) {
16
- installGitHooksInto(targetDir, config, cacheBaseDir);
20
+ for (const target of validatedTargets) {
21
+ installGitHooksInto(target, cacheBaseDir);
17
22
  }
18
- writeProjects(cacheBaseDir, targetDirs);
23
+ writeProjects(cacheBaseDir, validatedTargets.map((target) => target.targetDir));
19
24
 
20
- return targetDirs;
25
+ return validatedTargets.map((target) => target.targetDir);
21
26
  }
22
27
 
23
28
  function installFromPostinstall() {
24
29
  const envDirs = splitList(process.env.X7_CODE_LINE_DIRS);
25
- const targetDirs = envDirs.length > 0 ? envDirs : [process.env.INIT_CWD || process.cwd()];
30
+ const configPath = process.env.X7_CODE_LINE_CONFIG;
31
+ const config = readConfig(configPath);
32
+ const configuredDirs = Array.isArray(config.dirs) ? config.dirs.map((dir) => String(dir || "").trim()).filter(Boolean) : [];
33
+
34
+ if (envDirs.length === 0 && configuredDirs.length === 0) {
35
+ return [];
36
+ }
26
37
 
27
38
  return install({
28
- configPath: process.env.X7_CODE_LINE_CONFIG,
29
- targetDirs
39
+ configPath,
40
+ targetDirs: envDirs,
41
+ relativePaths: process.env.X7_CODE_LINE_RELATIVE_PATHS === "1"
42
+ });
43
+ }
44
+
45
+ function addDirs(options = {}) {
46
+ const explicitDirs = Array.isArray(options.targetDirs) ? options.targetDirs.map((dir) => String(dir || "").trim()).filter(Boolean) : [];
47
+ if (explicitDirs.length === 0) {
48
+ throw new Error("addDir requires at least one --dir value");
49
+ }
50
+
51
+ const pathBaseDir = path.resolve(options.pathBaseDir || process.env.INIT_CWD || process.cwd());
52
+ const config = readConfig(options.configPath || process.env.X7_CODE_LINE_CONFIG);
53
+ const targetDirs = normalizeTargetDirs(explicitDirs, config, {
54
+ relativePaths: Boolean(options.relativePaths || process.env.X7_CODE_LINE_RELATIVE_PATHS === "1"),
55
+ pathBaseDir
30
56
  });
57
+ const cacheBaseDir = path.resolve(options.cacheBaseDir || pathBaseDir);
58
+ assertInstalledBase(cacheBaseDir);
59
+
60
+ const validatedTargets = validateTargets(targetDirs, config);
61
+ for (const target of validatedTargets) {
62
+ installGitHooksInto(target, cacheBaseDir);
63
+ }
64
+
65
+ const mergedProjects = [...new Set([...readProjects(cacheBaseDir), ...validatedTargets.map((target) => target.targetDir)])];
66
+ writeProjects(cacheBaseDir, mergedProjects);
67
+
68
+ return validatedTargets.map((target) => target.targetDir);
31
69
  }
32
70
 
33
71
  function installCurrentProjectFiles(cacheBaseDir) {
@@ -52,11 +90,8 @@ function installCurrentProjectFiles(cacheBaseDir) {
52
90
  );
53
91
  }
54
92
 
55
- function installGitHooksInto(targetDir, config = {}, cacheBaseDir = process.cwd()) {
56
- const resolvedTarget = path.resolve(targetDir);
57
- ensureDirectory(resolvedTarget);
58
- const gitTargets = resolveGitTargets(resolvedTarget, config);
59
- for (const gitDir of gitTargets) {
93
+ function installGitHooksInto(target, cacheBaseDir = process.cwd()) {
94
+ for (const gitDir of target.gitDirs) {
60
95
  installGitHooks(gitDir, cacheBaseDir);
61
96
  }
62
97
  }
@@ -87,6 +122,63 @@ function resolveGitTargets(targetDir, config = {}) {
87
122
  });
88
123
  }
89
124
 
125
+ function validateTargets(targetDirs, config = {}) {
126
+ return targetDirs.map((targetDir) => validateTarget(targetDir, config));
127
+ }
128
+
129
+ function validateTarget(targetDir, config = {}) {
130
+ const resolvedTarget = path.resolve(targetDir);
131
+ assertReachableDirectory(resolvedTarget);
132
+ const gitDirs = resolveGitTargets(resolvedTarget, config);
133
+
134
+ if (gitDirs.length === 0) {
135
+ throw new Error(`Target directory must contain a reachable .git directory: ${resolvedTarget}`);
136
+ }
137
+
138
+ const configured = Array.isArray(config.gitDirs) ? config.gitDirs : [];
139
+ if (configured.length > 0) {
140
+ const missing = configured
141
+ .map((gitDir) => path.resolve(resolvedTarget, gitDir))
142
+ .filter((gitDir) => !gitDirs.includes(gitDir));
143
+ if (missing.length > 0) {
144
+ throw new Error(`Configured gitDirs are not reachable in target directory: ${missing.join(", ")}`);
145
+ }
146
+ }
147
+
148
+ return { targetDir: resolvedTarget, gitDirs };
149
+ }
150
+
151
+ function assertReachableDirectory(directory) {
152
+ let stat;
153
+ try {
154
+ stat = fs.statSync(directory);
155
+ } catch {
156
+ throw new Error(`Target directory does not exist or is not reachable: ${directory}`);
157
+ }
158
+
159
+ if (!stat.isDirectory()) {
160
+ throw new Error(`Target path is not a directory: ${directory}`);
161
+ }
162
+
163
+ try {
164
+ fs.accessSync(directory, fs.constants.R_OK);
165
+ } catch {
166
+ throw new Error(`Target directory is not readable: ${directory}`);
167
+ }
168
+ }
169
+
170
+ function assertInstalledBase(cacheBaseDir) {
171
+ const installFile = path.join(cacheBaseDir, CACHE_DIR_NAME, "install.json");
172
+ try {
173
+ const stat = fs.statSync(installFile);
174
+ if (!stat.isFile()) {
175
+ throw new Error("");
176
+ }
177
+ } catch {
178
+ throw new Error(`Current directory is not an installed x7-code-line base: ${cacheBaseDir}`);
179
+ }
180
+ }
181
+
90
182
  function readConfig(configPath) {
91
183
  if (!configPath) {
92
184
  return {};
@@ -103,15 +195,26 @@ function readConfig(configPath) {
103
195
  return config;
104
196
  }
105
197
 
106
- function normalizeTargetDirs(targetDirs = [], config = {}) {
198
+ function normalizeTargetDirs(targetDirs = [], config = {}, options = {}) {
107
199
  const configuredDirs = Array.isArray(config.dirs) ? config.dirs : [];
108
200
  const dirs = [...targetDirs, ...configuredDirs].map((dir) => String(dir || "").trim()).filter(Boolean);
109
201
 
110
202
  if (dirs.length === 0) {
111
- return [process.env.INIT_CWD || process.cwd()];
203
+ throw new Error("install requires at least one target directory");
112
204
  }
113
205
 
114
- return [...new Set(dirs)].map((dir) => path.resolve(dir));
206
+ const baseDir = path.resolve(options.pathBaseDir || process.env.INIT_CWD || process.cwd());
207
+ return [...new Set(dirs)].map((dir) => normalizeTargetDir(dir, options.relativePaths, baseDir));
208
+ }
209
+
210
+ function normalizeTargetDir(dir, relativePaths, baseDir) {
211
+ if (path.isAbsolute(dir)) {
212
+ return path.resolve(dir);
213
+ }
214
+ if (!relativePaths) {
215
+ throw new Error(`Target directory must be an absolute path unless --relative is used: ${dir}`);
216
+ }
217
+ return path.resolve(baseDir, dir);
115
218
  }
116
219
 
117
220
  function splitList(value) {
@@ -165,6 +268,7 @@ function ensureDirectory(directory) {
165
268
  }
166
269
 
167
270
  module.exports = {
271
+ addDirs,
168
272
  CACHE_DIR_NAME,
169
273
  install,
170
274
  installFromPostinstall