rootless-config 1.2.0 → 1.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rootless-config",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "Store project config files outside the project root, auto-deploy them where tools expect them.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -5,22 +5,29 @@ import { readdir, unlink, readFile } from 'node:fs/promises'
5
5
  import { createLogger } from '../../utils/logger.js'
6
6
  import { fileExists, ensureDir, atomicWrite, readJsonFile } from '../../utils/fsUtils.js'
7
7
  import { confirm } from '../../utils/prompt.js'
8
- import { patchPackageScripts } from '../../core/scriptPatcher.js'
8
+ import { patchPackageScripts, getAllKnownFiles, NEVER_MIGRATE } from '../../core/scriptPatcher.js'
9
9
 
10
- const CONFIG_PATTERN = /\.(config|rc)\.(js|ts|mjs|cjs|json)$|\.eslintrc$|\.babelrc$|tsconfig.*\.json$/
11
- const ENV_PATTERN = /^\.env/
12
- // Dot-files and special files that must be in root — auto-detected
13
- const DOTFILE_PATTERN = /^\.(nvmrc|node-version|npmrc|yarnrc|yarnrc\.yml|pnpmfile\.cjs|editorconfig|gitignore|gitattributes|dockerignore|browserslistrc|prettierignore|eslintignore|stylelintignore|czrc|cz\.json|commitlintrc.*|lintstagedrc.*|releaserc.*)$|^(Dockerfile.*|Procfile|browserslist)$/
10
+ // Pattern-based detection for file families not covered by the explicit known-files set
11
+ const ENV_PATTERN = /^\.env/ // .env, .env.local, .env.production, etc.
12
+ const TSCONFIG_PATTERN = /^tsconfig.*\.json$/ // tsconfig.custom.json etc.
13
+ const DOCKERFILE_PATTERN = /^Dockerfile/ // Dockerfile.dev, Dockerfile.prod etc.
14
+ const DOCKER_COMPOSE_PATTERN = /^docker-compose/ // docker-compose.override.yml etc.
14
15
 
15
16
  async function findMigratableFiles(projectRoot) {
17
+ const knownFiles = getAllKnownFiles()
16
18
  const entries = await readdir(projectRoot, { withFileTypes: true })
17
19
  return entries
18
- .filter(e => e.isFile() && (
19
- CONFIG_PATTERN.test(e.name) ||
20
- ENV_PATTERN.test(e.name) ||
21
- DOTFILE_PATTERN.test(e.name)
22
- ))
23
- .filter(e => e.name !== 'package.json')
20
+ .filter(e => {
21
+ if (!e.isFile()) return false
22
+ if (NEVER_MIGRATE.has(e.name)) return false
23
+ return (
24
+ knownFiles.has(e.name) ||
25
+ ENV_PATTERN.test(e.name) ||
26
+ TSCONFIG_PATTERN.test(e.name) ||
27
+ DOCKERFILE_PATTERN.test(e.name) ||
28
+ DOCKER_COMPOSE_PATTERN.test(e.name)
29
+ )
30
+ })
24
31
  .map(e => e.name)
25
32
  .sort()
26
33
  }
@@ -68,18 +75,19 @@ export default {
68
75
  for (const name of candidates) {
69
76
  const src = path.join(projectRoot, name)
70
77
  const isEnv = ENV_PATTERN.test(name)
71
- const destDir = path.join(containerPath, isEnv ? 'env' : 'configs')
78
+ const isAsset = /\.(ico|png|jpg|jpeg|svg|txt|xml|webmanifest|json)$/.test(name) &&
79
+ ['favicon.ico', 'robots.txt', 'sitemap.xml', 'manifest.json', 'site.webmanifest'].includes(name)
80
+ const destSubdir = isEnv ? 'env' : (isAsset ? 'assets' : 'configs')
81
+ const destDir = path.join(containerPath, destSubdir)
72
82
 
73
83
  await ensureDir(destDir)
74
84
  const content = await readFile(src, 'utf8')
75
85
  await atomicWrite(path.join(destDir, name), content)
76
86
 
77
87
  if (isCleanMode) {
78
- // Clean mode: delete original — no proxy file in root
79
88
  await unlink(src)
80
- logger.success(`Moved to .root/${isEnv ? 'env' : 'configs'}/${name} (deleted from root)`)
89
+ logger.success(`Moved to .root/${destSubdir}/${name} (deleted from root)`)
81
90
  } else {
82
- // Proxy mode: replace original with re-export stub
83
91
  const rel = path.relative(path.dirname(src), path.join(destDir, name)).replace(/\\/g, '/')
84
92
  const relPath = rel.startsWith('.') ? rel : `./${rel}`
85
93
  await atomicWrite(src, `export { default } from "${relPath}"\n`)
@@ -1,92 +1,190 @@
1
- /*-------- Patches package.json scripts to use --config flags pointing to .root/ --------*/
1
+ /*-------- Patches package.json scripts to use --config flags pointing to .root/ --------*/
2
2
 
3
3
  import path from 'node:path'
4
4
  import { readdir } from 'node:fs/promises'
5
5
  import { readJsonFile, writeJsonFile, fileExists } from '../utils/fsUtils.js'
6
6
 
7
+ // ─── Tool definitions ────────────────────────────────────────────────────────
8
+
7
9
  /**
8
10
  * Tools that support a --config (or similar) flag.
9
- * These files can be REMOVED from root — scripts get patched instead.
10
- * Ordered by specificity (longer command first to avoid partial matches).
11
+ * Matching files will NOT be copied to root — scripts get patched instead.
12
+ * Ordered by command specificity (longer commands first to avoid partial matches).
11
13
  */
12
14
  const TOOL_CONFIGS = [
13
- { cmd: 'vite build', flag: '--config', files: ['vite.config.js', 'vite.config.ts', 'vite.config.mjs'] },
14
- { cmd: 'vite preview', flag: '--config', files: ['vite.config.js', 'vite.config.ts', 'vite.config.mjs'] },
15
- { cmd: 'vite', flag: '--config', files: ['vite.config.js', 'vite.config.ts', 'vite.config.mjs'] },
16
- { cmd: 'vitest run', flag: '--config', files: ['vitest.config.js', 'vitest.config.ts', 'vitest.config.mjs'] },
17
- { cmd: 'vitest', flag: '--config', files: ['vitest.config.js', 'vitest.config.ts', 'vitest.config.mjs'] },
18
- { cmd: 'eslint', flag: '--config', files: ['eslint.config.js', 'eslint.config.mjs', 'eslint.config.cjs'] },
19
- { cmd: 'prettier', flag: '--config', files: ['prettier.config.js', 'prettier.config.cjs', '.prettierrc.js', 'prettier.config.mjs'] },
20
- { cmd: 'jest', flag: '--config', files: ['jest.config.js', 'jest.config.ts', 'jest.config.mjs', 'jest.config.cjs'] },
21
- { cmd: 'webpack serve', flag: '--config', files: ['webpack.config.js', 'webpack.config.ts', 'webpack.config.mjs'] },
22
- { cmd: 'webpack', flag: '--config', files: ['webpack.config.js', 'webpack.config.ts', 'webpack.config.mjs'] },
23
- { cmd: 'rollup', flag: '--config', files: ['rollup.config.js', 'rollup.config.ts', 'rollup.config.mjs'] },
24
- { cmd: 'tsup', flag: '--config', files: ['tsup.config.js', 'tsup.config.ts', 'tsup.config.mjs'] },
25
- { cmd: 'tsc', flag: '--project', files: ['tsconfig.json', 'tsconfig.build.json', 'tsconfig.prod.json'] },
26
- { cmd: 'nodemon', flag: '--config', files: ['nodemon.json', '.nodemonrc', 'nodemon.config.js'] },
27
- { cmd: 'stylelint', flag: '--config', files: ['stylelint.config.js', 'stylelint.config.mjs', 'stylelint.config.cjs', '.stylelintrc.js', '.stylelintrc.json'] },
28
- { cmd: 'next build', flag: '--', files: [] }, // next doesn't support --config; handled separately
29
- { cmd: 'tailwindcss', flag: '--config', files: ['tailwind.config.js', 'tailwind.config.ts', 'tailwind.config.mjs'] },
30
- { cmd: 'postcss', flag: '--config', files: ['postcss.config.js', 'postcss.config.mjs', 'postcss.config.cjs'] },
31
- { cmd: 'babel', flag: '--config-file', files: ['babel.config.js', 'babel.config.mjs', 'babel.config.cjs', 'babel.config.json'] },
32
- { cmd: 'mocha', flag: '--config', files: ['.mocharc.js', '.mocharc.cjs', '.mocharc.yml', '.mocharc.json', 'mocha.config.js'] },
33
- { cmd: 'nyc', flag: '--config', files: ['.nycrc', '.nycrc.json', 'nyc.config.js', 'nyc.config.cjs'] },
34
- { cmd: 'cypress run', flag: '--config-file', files: ['cypress.config.js', 'cypress.config.ts', 'cypress.config.mjs'] },
35
- { cmd: 'cypress open', flag: '--config-file', files: ['cypress.config.js', 'cypress.config.ts', 'cypress.config.mjs'] },
36
- { cmd: 'playwright test', flag: '--config', files: ['playwright.config.js', 'playwright.config.ts', 'playwright.config.mjs'] },
15
+ // Vite
16
+ { cmd: 'vite build', flag: '--config', files: ['vite.config.js', 'vite.config.ts', 'vite.config.mjs', 'vite.config.cjs'] },
17
+ { cmd: 'vite preview', flag: '--config', files: ['vite.config.js', 'vite.config.ts', 'vite.config.mjs', 'vite.config.cjs'] },
18
+ { cmd: 'vite', flag: '--config', files: ['vite.config.js', 'vite.config.ts', 'vite.config.mjs', 'vite.config.cjs'] },
19
+ // Vitest
20
+ { cmd: 'vitest run', flag: '--config', files: ['vitest.config.js', 'vitest.config.ts', 'vitest.config.mjs'] },
21
+ { cmd: 'vitest', flag: '--config', files: ['vitest.config.js', 'vitest.config.ts', 'vitest.config.mjs'] },
22
+ // ESLint (v8 flat config + legacy .eslintrc variants)
23
+ { cmd: 'eslint', flag: '--config', files: ['eslint.config.js', 'eslint.config.mjs', 'eslint.config.cjs', '.eslintrc', '.eslintrc.js', '.eslintrc.cjs', '.eslintrc.mjs', '.eslintrc.json', '.eslintrc.yaml', '.eslintrc.yml'] },
24
+ // Prettier
25
+ { cmd: 'prettier', flag: '--config', files: ['prettier.config.js', 'prettier.config.cjs', 'prettier.config.mjs', '.prettierrc', '.prettierrc.js', '.prettierrc.cjs', '.prettierrc.json', '.prettierrc.yaml', '.prettierrc.yml'] },
26
+ // Stylelint
27
+ { cmd: 'stylelint', flag: '--config', files: ['stylelint.config.js', 'stylelint.config.cjs', 'stylelint.config.mjs', '.stylelintrc', '.stylelintrc.js', '.stylelintrc.json', '.stylelintrc.yaml', '.stylelintrc.yml'] },
28
+ // Jest
29
+ { cmd: 'jest', flag: '--config', files: ['jest.config.js', 'jest.config.cjs', 'jest.config.mjs', 'jest.config.ts', 'jest.config.json'] },
30
+ // Webpack
31
+ { cmd: 'webpack serve', flag: '--config', files: ['webpack.config.js', 'webpack.config.cjs', 'webpack.config.mjs', 'webpack.config.ts', 'webpack.common.js', 'webpack.dev.js', 'webpack.prod.js', 'webpack.base.js', 'webpack.config.babel.js'] },
32
+ { cmd: 'webpack', flag: '--config', files: ['webpack.config.js', 'webpack.config.cjs', 'webpack.config.mjs', 'webpack.config.ts', 'webpack.common.js', 'webpack.dev.js', 'webpack.prod.js', 'webpack.base.js', 'webpack.config.babel.js'] },
33
+ // Rollup
34
+ { cmd: 'rollup', flag: '--config', files: ['rollup.config.js', 'rollup.config.mjs', 'rollup.config.cjs', 'rollup.config.ts'] },
35
+ // tsup
36
+ { cmd: 'tsup', flag: '--config', files: ['tsup.config.js', 'tsup.config.ts', 'tsup.config.mjs'] },
37
+ // TypeScript compiler
38
+ { cmd: 'tsc', flag: '--project', files: ['tsconfig.json', 'tsconfig.base.json', 'tsconfig.app.json', 'tsconfig.build.json', 'tsconfig.node.json', 'tsconfig.spec.json', 'tsconfig.test.json', 'tsconfig.worker.json'] },
39
+ // Nodemon
40
+ { cmd: 'nodemon', flag: '--config', files: ['nodemon.json', '.nodemonrc', 'nodemon.config.js'] },
41
+ // Babel
42
+ { cmd: 'babel', flag: '--config-file', files: ['babel.config.js', 'babel.config.cjs', 'babel.config.mjs', 'babel.config.json', '.babelrc', '.babelrc.js', '.babelrc.cjs', '.babelrc.json'] },
43
+ // PostCSS
44
+ { cmd: 'postcss', flag: '--config', files: ['postcss.config.js', 'postcss.config.cjs', 'postcss.config.mjs', 'postcss.config.ts'] },
45
+ // Tailwind CSS
46
+ { cmd: 'tailwindcss', flag: '--config', files: ['tailwind.config.js', 'tailwind.config.cjs', 'tailwind.config.mjs', 'tailwind.config.ts'] },
47
+ // Mocha
48
+ { cmd: 'mocha', flag: '--config', files: ['.mocharc.js', '.mocharc.cjs', '.mocharc.json', '.mocharc.yaml', '.mocharc.yml'] },
49
+ // nyc (Istanbul coverage)
50
+ { cmd: 'nyc', flag: '--nycrc', files: ['.nycrc', '.nycrc.json', 'nyc.config.js', 'nyc.config.cjs'] },
51
+ // Cypress
52
+ { cmd: 'cypress run', flag: '--config-file', files: ['cypress.config.js', 'cypress.config.ts'] },
53
+ { cmd: 'cypress open', flag: '--config-file', files: ['cypress.config.js', 'cypress.config.ts'] },
54
+ // Playwright
55
+ { cmd: 'playwright test', flag: '--config', files: ['playwright.config.js', 'playwright.config.ts'] },
56
+ // Astro
57
+ { cmd: 'astro dev', flag: '--config', files: ['astro.config.js', 'astro.config.mjs', 'astro.config.ts'] },
58
+ { cmd: 'astro build', flag: '--config', files: ['astro.config.js', 'astro.config.mjs', 'astro.config.ts'] },
59
+ { cmd: 'astro preview', flag: '--config', files: ['astro.config.js', 'astro.config.mjs', 'astro.config.ts'] },
60
+ { cmd: 'astro', flag: '--config', files: ['astro.config.js', 'astro.config.mjs', 'astro.config.ts'] },
61
+ // Biome
62
+ { cmd: 'biome check', flag: '--config-path', files: ['biome.json'] },
63
+ { cmd: 'biome lint', flag: '--config-path', files: ['biome.json'] },
64
+ { cmd: 'biome format', flag: '--config-path', files: ['biome.json'] },
65
+ { cmd: 'biome ci', flag: '--config-path', files: ['biome.json'] },
66
+ { cmd: 'biome', flag: '--config-path', files: ['biome.json'] },
37
67
  ]
38
68
 
39
69
  /**
40
70
  * Tools that use the config file as a POSITIONAL argument (not a --flag).
41
- * These files can also be removed from root — the argument in the script is patched.
42
71
  */
43
72
  const POSITIONAL_TOOLS = [
44
- { cmd: 'pm2 start', files: ['ecosystem.config.js', 'ecosystem.config.cjs', 'ecosystem.config.mjs'] },
45
- { cmd: 'pm2 restart', files: ['ecosystem.config.js', 'ecosystem.config.cjs', 'ecosystem.config.mjs'] },
46
- { cmd: 'pm2 reload', files: ['ecosystem.config.js', 'ecosystem.config.cjs', 'ecosystem.config.mjs'] },
73
+ { cmd: 'pm2 start', files: ['ecosystem.config.js', 'ecosystem.config.cjs', 'ecosystem.config.mjs', 'pm2.config.js'] },
74
+ { cmd: 'pm2 restart', files: ['ecosystem.config.js', 'ecosystem.config.cjs', 'ecosystem.config.mjs', 'pm2.config.js'] },
75
+ { cmd: 'pm2 reload', files: ['ecosystem.config.js', 'ecosystem.config.cjs', 'ecosystem.config.mjs', 'pm2.config.js'] },
76
+ { cmd: 'karma start', files: ['karma.conf.js', 'karma.conf.ts'] },
77
+ { cmd: 'karma', files: ['karma.conf.js', 'karma.conf.ts'] },
47
78
  ]
48
79
 
49
80
  /**
50
81
  * Files that MUST be physically present in the project root.
51
- * These cannot be redirected via CLI flags tools discover them by convention.
52
- * Everything in .root/configs/ NOT matching TOOL_CONFIGS or POSITIONAL_TOOLS falls into this category,
53
- * but we keep an explicit list for clarity and migrate detection.
82
+ * Tools discover them by conventionno CLI redirect is possible.
83
+ * These are COPIED to root by `rootless prepare`.
54
84
  */
55
85
  const ROOT_REQUIRED_FILES = new Set([
56
- // Node.js version managers
86
+ // Node / npm
87
+ '.npmrc',
88
+ // Node version managers
57
89
  '.nvmrc', '.node-version',
58
90
  // Package managers
59
- '.npmrc', '.yarnrc', '.yarnrc.yml', '.pnpmfile.cjs',
91
+ '.yarnrc', '.yarnrc.yml', '.pnpmfile.cjs', 'pnpm-workspace.yaml',
92
+ // TypeScript (auto-discovered by ts-node, ts-jest, esbuild, IDEs, etc.)
93
+ 'tsconfig.json', 'tsconfig.base.json', 'tsconfig.app.json', 'tsconfig.build.json',
94
+ 'tsconfig.node.json', 'tsconfig.spec.json', 'tsconfig.test.json', 'tsconfig.worker.json',
95
+ 'jsconfig.json',
60
96
  // Editor
61
97
  '.editorconfig',
62
- // Git (root discovery via git, these are auto-found)
63
- '.gitignore', '.gitattributes', '.gitmodules',
64
- // Docker
65
- 'Dockerfile', '.dockerignore',
66
- // Process / deployment
67
- 'Procfile',
98
+ // Git
99
+ '.gitignore', '.gitattributes', '.gitmodules', '.mailmap',
100
+ // Linting ignore files (auto-discovered, no --flag support)
101
+ '.eslintignore', '.prettierignore', '.stylelintignore',
102
+ // Parcel (auto-discovers .parcelrc from root)
103
+ '.parcelrc', 'parcel.config.js',
104
+ // Next.js (no --config flag support)
105
+ 'next.config.js', 'next.config.mjs', 'next.config.ts',
106
+ 'middleware.ts', 'middleware.js', 'instrumentation.ts', 'instrumentation.js',
107
+ // Nuxt (auto-discovered)
108
+ 'nuxt.config.js', 'nuxt.config.ts',
109
+ // Vue CLI (auto-discovered)
110
+ 'vue.config.js',
111
+ // Angular
112
+ 'angular.json', 'proxy.conf.json',
113
+ // SvelteKit / Svelte (auto-discovered by Vite plugin from root)
114
+ 'svelte.config.js', 'svelte.config.cjs', 'svelte.config.mjs',
115
+ // Remix (auto-discovered)
116
+ 'remix.config.js', 'remix.config.mjs', 'remix.config.ts',
117
+ // Monorepo tools
118
+ 'turbo.json', 'nx.json', 'workspace.json', 'project.json', 'lerna.json', 'rush.json',
119
+ // Build tools (auto-discovered from root)
120
+ '.swcrc', 'swc.config.js', 'rome.json',
121
+ // CSS tooling (auto-discovered by framework plugins)
122
+ 'windicss.config.js', 'windicss.config.ts', 'unocss.config.js', 'unocss.config.ts',
68
123
  // Browser targets
69
124
  '.browserslistrc', 'browserslist',
70
- // Lint ignore files (eslint/prettier look for these automatically)
71
- '.eslintignore', '.prettierignore', '.stylelintignore',
72
- // Commitlint / lint-staged / release (auto-discovery only)
125
+ // Commit / git hooks
126
+ '.huskyrc', '.huskyrc.json', '.huskyrc.js', '.huskyrc.yaml', '.huskyrc.yml',
73
127
  'commitlint.config.js', 'commitlint.config.cjs', 'commitlint.config.mjs',
74
- '.commitlintrc', '.commitlintrc.js', '.commitlintrc.json',
128
+ '.commitlintrc', '.commitlintrc.js', '.commitlintrc.json', '.commitlintrc.yaml', '.commitlintrc.yml',
129
+ 'lint-staged.config.js', 'lint-staged.config.cjs', 'lint-staged.config.mjs',
75
130
  '.lintstagedrc', '.lintstagedrc.js', '.lintstagedrc.json', '.lintstagedrc.cjs',
76
- 'lint-staged.config.js', 'lint-staged.config.mjs', 'lint-staged.config.cjs',
77
- '.releaserc', '.releaserc.js', '.releaserc.json', '.releaserc.yml',
78
- 'release.config.js', 'release.config.cjs',
79
- // Semantic tooling
131
+ // Release automation
132
+ 'release.config.js', 'release.config.cjs', 'semantic-release.config.js', '.semantic-release.json',
133
+ '.releaserc', '.releaserc.js', '.releaserc.json', '.releaserc.yml', '.releaserc.yaml',
134
+ // Semantic commit
80
135
  '.czrc', '.cz.json',
81
- // HTTPS / certs
82
- '.htaccess',
83
- // Web assets
136
+ // Docker
137
+ 'Dockerfile', '.dockerignore', 'docker-compose.yml', 'docker-compose.yaml',
138
+ // CI
139
+ '.gitlab-ci.yml', 'azure-pipelines.yml',
140
+ // Web assets (served directly from root by web servers)
84
141
  'robots.txt', 'sitemap.xml', 'favicon.ico', 'manifest.json', 'site.webmanifest',
142
+ // Documentation
143
+ 'README.md', 'LICENSE', 'CHANGELOG.md', 'CONTRIBUTING.md', 'CODE_OF_CONDUCT.md',
144
+ 'SECURITY.md', 'SUPPORT.md',
145
+ // Dependency management / update bots
146
+ 'renovate.json', '.renovaterc', '.renovaterc.json',
147
+ // Misc
148
+ '.htaccess', 'Procfile',
85
149
  ])
86
150
 
87
151
  /**
88
- * Returns true if the file can be redirected away from root via CLI flags.
89
- * Files where this returns false MUST be copied to root.
152
+ * Lock files and core manifests that should NEVER be migrated.
153
+ * They must always live in the project root and are managed by package managers.
154
+ */
155
+ const NEVER_MIGRATE = new Set([
156
+ 'package.json',
157
+ 'package-lock.json',
158
+ 'yarn.lock',
159
+ 'pnpm-lock.yaml',
160
+ 'npm-shrinkwrap.json',
161
+ ])
162
+
163
+ // ─── Helper functions ────────────────────────────────────────────────────────
164
+
165
+ /**
166
+ * Returns all config file names known to rootless across all categories.
167
+ * Excludes NEVER_MIGRATE entries. Used by the migrate command.
168
+ */
169
+ function getAllKnownFiles() {
170
+ const all = new Set()
171
+ for (const tool of TOOL_CONFIGS) {
172
+ for (const f of tool.files) all.add(f)
173
+ }
174
+ for (const tool of POSITIONAL_TOOLS) {
175
+ for (const f of tool.files) all.add(f)
176
+ }
177
+ for (const f of ROOT_REQUIRED_FILES) {
178
+ all.add(f)
179
+ }
180
+ for (const f of NEVER_MIGRATE) {
181
+ all.delete(f)
182
+ }
183
+ return all
184
+ }
185
+
186
+ /**
187
+ * Returns true if the file can be redirected via CLI flags (not copied to root).
90
188
  */
91
189
  function isPatchable(filename) {
92
190
  return (
@@ -96,23 +194,23 @@ function isPatchable(filename) {
96
194
  }
97
195
 
98
196
  /**
99
- * Returns true if the file must always be physically present in root.
197
+ * Returns true if the file must be physically present in the project root.
100
198
  */
101
199
  function isRootRequired(filename) {
200
+ if (NEVER_MIGRATE.has(filename)) return false
102
201
  if (ROOT_REQUIRED_FILES.has(filename)) return true
103
- // .env* patterns
104
- if (/^\.env/.test(filename)) return true
105
- // Dockerfile variants: Dockerfile.dev, Dockerfile.prod etc.
106
- if (/^Dockerfile/.test(filename)) return true
107
- // Not patchable and not in ROOT_REQUIRED_FILES still copy to be safe
108
- if (!isPatchable(filename)) return true
202
+ if (/^\.env/.test(filename)) return true // .env.local, .env.production, etc.
203
+ if (/^Dockerfile/.test(filename)) return true // Dockerfile.dev, Dockerfile.prod etc.
204
+ if (/^tsconfig.*\.json$/.test(filename)) return true // tsconfig.custom.json variants
205
+ if (/^docker-compose/.test(filename)) return true // docker-compose.override.yml etc.
206
+ if (!isPatchable(filename)) return true // unknown file → copy to root to be safe
109
207
  return false
110
208
  }
111
209
 
210
+ // ─── Config map building ─────────────────────────────────────────────────────
211
+
112
212
  /**
113
- * Build a map of { cmd → relative config path } based on what actually exists in configsDir.
114
- * configsDir is the absolute path to .root/configs/
115
- * projectRoot is used to compute relative paths in scripts
213
+ * Build a map of { cmd → { path, flag, positional } } based on files in configsDir.
116
214
  */
117
215
  async function buildConfigMap(configsDir, projectRoot) {
118
216
  let files = []
@@ -124,10 +222,8 @@ async function buildConfigMap(configsDir, projectRoot) {
124
222
 
125
223
  const map = {}
126
224
 
127
- // Flag-based tools
128
225
  for (const tool of TOOL_CONFIGS) {
129
- if (!tool.files.length) continue
130
- if (map[tool.cmd]) continue
226
+ if (!tool.files.length || map[tool.cmd]) continue
131
227
  const found = tool.files.find(f => files.includes(f))
132
228
  if (!found) continue
133
229
  const abs = path.join(configsDir, found)
@@ -135,39 +231,36 @@ async function buildConfigMap(configsDir, projectRoot) {
135
231
  map[tool.cmd] = { path: rel.startsWith('.') ? rel : `./${rel}`, flag: tool.flag, positional: false }
136
232
  }
137
233
 
138
- // Positional tools (pm2 etc.)
139
234
  for (const tool of POSITIONAL_TOOLS) {
140
235
  if (map[tool.cmd]) continue
141
236
  const found = tool.files.find(f => files.includes(f))
142
237
  if (!found) continue
143
238
  const abs = path.join(configsDir, found)
144
239
  const rel = path.relative(projectRoot, abs).replace(/\\/g, '/')
145
- map[tool.cmd] = { path: rel.startsWith('.') ? rel : `./${rel}`, flag: null, positional: true, originalFile: found }
240
+ map[tool.cmd] = { path: rel.startsWith('.') ? rel : `./${rel}`, flag: null, positional: true, originalFiles: tool.files }
146
241
  }
147
242
 
148
243
  return map
149
244
  }
150
245
 
246
+ // ─── Script patching ─────────────────────────────────────────────────────────
247
+
151
248
  /**
152
- * Inject --config flag into a single script string where missing.
153
- * Handles semicolons, &&, || (multi-command scripts).
249
+ * Inject --config flag (or replace positional arg) in a single script string.
154
250
  */
155
251
  function patchScriptString(script, configMap) {
156
- // Sort by command length descending to avoid partial matches
157
252
  const sorted = Object.entries(configMap).sort((a, b) => b[0].length - a[0].length)
158
253
 
159
254
  let result = script
160
255
  for (const [cmd, info] of sorted) {
161
256
  if (info.positional) {
162
- // Replace filename argument directly: pm2 start ecosystem.config.cjs pm2 start .root/...
163
- for (const file of (POSITIONAL_TOOLS.find(t => t.cmd === cmd)?.files ?? [])) {
257
+ for (const file of (info.originalFiles ?? [])) {
164
258
  const escapedFile = file.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
165
259
  const escapedCmd = cmd.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/\s+/g, '\\s+')
166
260
  const regex = new RegExp(`(${escapedCmd})\\s+${escapedFile}`, 'g')
167
261
  result = result.replace(regex, `$1 ${info.path}`)
168
262
  }
169
263
  } else {
170
- // Flag-based: insert --config <path> after command if not already present
171
264
  const flag = info.flag
172
265
  const escapedCmd = cmd.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/\s+/g, '\\s+')
173
266
  const escapedFlag = flag.replace(/-/g, '\\-')
@@ -203,16 +296,12 @@ async function patchPackageScripts(projectRoot, configsDir, logger) {
203
296
  }
204
297
  }
205
298
 
206
- if (changed) {
207
- await writeJsonFile(pkgPath, pkg)
208
- }
299
+ if (changed) await writeJsonFile(pkgPath, pkg)
209
300
  return changed
210
301
  }
211
302
 
212
303
  /**
213
304
  * Add "prepare": "rootless prepare --yes" to project's package.json.
214
- * If prepare already exists and doesn't include rootless, prepends to it.
215
- * Returns the final prepare script value.
216
305
  */
217
306
  async function addPrepareHook(projectRoot) {
218
307
  const pkgPath = path.join(projectRoot, 'package.json')
@@ -232,4 +321,14 @@ async function addPrepareHook(projectRoot) {
232
321
  return pkg.scripts.prepare
233
322
  }
234
323
 
235
- export { patchPackageScripts, addPrepareHook, buildConfigMap, patchScriptString, isPatchable, isRootRequired }
324
+ export {
325
+ patchPackageScripts,
326
+ addPrepareHook,
327
+ buildConfigMap,
328
+ patchScriptString,
329
+ isPatchable,
330
+ isRootRequired,
331
+ getAllKnownFiles,
332
+ NEVER_MIGRATE,
333
+ }
334
+