rootless-config 1.2.0 → 1.4.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.4.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,21 @@ 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, isRootRequired, isPatchable, isWebAsset, 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
+ // A file is migratable when rootless knows what to do with it:
11
+ // - isRootRequired must be in root (will be copied there by prepare)
12
+ // - isPatchable → can be redirected via --config flag in package.json scripts
13
+ // Anything that's NEVER_MIGRATE (lock files, package.json) is excluded.
14
14
 
15
15
  async function findMigratableFiles(projectRoot) {
16
16
  const entries = await readdir(projectRoot, { withFileTypes: true })
17
17
  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')
18
+ .filter(e => {
19
+ if (!e.isFile()) return false
20
+ if (NEVER_MIGRATE.has(e.name)) return false
21
+ return isRootRequired(e.name) || isPatchable(e.name)
22
+ })
24
23
  .map(e => e.name)
25
24
  .sort()
26
25
  }
@@ -67,19 +66,18 @@ export default {
67
66
 
68
67
  for (const name of candidates) {
69
68
  const src = path.join(projectRoot, name)
70
- const isEnv = ENV_PATTERN.test(name)
71
- const destDir = path.join(containerPath, isEnv ? 'env' : 'configs')
69
+ const isEnv = /^\.env/.test(name)
70
+ const destSubdir = isEnv ? 'env' : (isWebAsset(name) ? 'assets' : 'configs')
71
+ const destDir = path.join(containerPath, destSubdir)
72
72
 
73
73
  await ensureDir(destDir)
74
74
  const content = await readFile(src, 'utf8')
75
75
  await atomicWrite(path.join(destDir, name), content)
76
76
 
77
77
  if (isCleanMode) {
78
- // Clean mode: delete original — no proxy file in root
79
78
  await unlink(src)
80
- logger.success(`Moved to .root/${isEnv ? 'env' : 'configs'}/${name} (deleted from root)`)
79
+ logger.success(`Moved to .root/${destSubdir}/${name} (deleted from root)`)
81
80
  } else {
82
- // Proxy mode: replace original with re-export stub
83
81
  const rel = path.relative(path.dirname(src), path.join(destDir, name)).replace(/\\/g, '/')
84
82
  const relPath = rel.startsWith('.') ? rel : `./${rel}`
85
83
  await atomicWrite(src, `export { default } from "${relPath}"\n`)
@@ -1,92 +1,331 @@
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',
149
+ // GitHub Pages
150
+ '.nojekyll', 'CNAME', 'CXNAME',
151
+ // Hosting platform
152
+ '_redirects', '_headers', 'vercel.json', 'netlify.toml', 'wrangler.toml', 'fly.toml',
153
+ // Status / health
154
+ 'status.json', 'health.json',
155
+ // NGINX / server
156
+ 'nginx.conf', 'nginx.config',
157
+ // Service workers
158
+ 'sw.js', 'service-worker.js',
159
+ // Web entry points
160
+ 'index.html', 'index.htm', 'index.php',
161
+ ])
162
+
163
+ // ─── Pattern-based root-required detection ───────────────────────────────────
164
+
165
+ /**
166
+ * Regex patterns for files that MUST be physically in the project root,
167
+ * matched against the basename only.
168
+ * Used for file families whose exact names can vary
169
+ * (e.g. icon-192.png, 404.htm, logo-dark.svg, server.ps1 …).
170
+ */
171
+ const ROOT_REQUIRED_PATTERNS = [
172
+ // Environment files
173
+ /^\.env/,
174
+ // Docker variants
175
+ /^Dockerfile(\..+)?$/,
176
+ /^docker-compose.*\.(yml|yaml)$/,
177
+ // tsconfig variants
178
+ /^tsconfig.*\.json$/,
179
+ // GitHub Pages & hosting markers
180
+ /^\.nojekyll$/,
181
+ /^CNAME$/i,
182
+ /^CXNAME$/i,
183
+ // Error pages — 404.html, 404.htm, 404.md, 500.html …
184
+ /^[45]\d{2}\.(html?|php|md|txt)$/,
185
+ // Web entry points
186
+ /^index\.(html?|php)$/,
187
+ // index-style assets — index-ui.css, index.app.js, index.min.js …
188
+ /^index[-.].+\.(css|js|mjs)$/,
189
+ // Service workers
190
+ /^sw\.js$/,
191
+ /^service-worker.*\.js$/,
192
+ /^workbox-.*\.js$/,
193
+ // Web manifests
194
+ /^manifest.*\.json$/,
195
+ /^site\.webmanifest$/,
196
+ // Favicon & icons
197
+ /^favicon(\..+)?$/,
198
+ /^apple-touch-icon.*\.(png|jpg)$/,
199
+ /^icon.*\.(png|ico|svg|jpg|jpeg)$/,
200
+ /^android-chrome-.*\.(png|jpg)$/,
201
+ /^mstile-.*\.png$/,
202
+ /^browserconfig\.xml$/,
203
+ // Social & OG images
204
+ /^og[-_]image.*\.(png|jpg|jpeg|webp)$/,
205
+ /^twitter[-_]image.*\.(png|jpg|jpeg|webp)$/,
206
+ /^social[-_]image.*\.(png|jpg|jpeg|webp)$/,
207
+ /^opengraph.*\.(png|jpg|jpeg|webp)$/,
208
+ // Logo & branding
209
+ /^logo.*\.(png|svg|jpg|jpeg|webp)$/,
210
+ // Screenshots & splash screens
211
+ /^splash.*\.(png|jpg|jpeg)$/,
212
+ /^screenshot.*\.(png|jpg|jpeg)$/,
213
+ // Sitemaps & SEO
214
+ /^sitemap.*\.(xml|txt)$/,
215
+ // NGINX & server configs
216
+ /^nginx.*\.(conf|config)$/,
217
+ /\.nginx$/,
218
+ /^\.htaccess$/,
219
+ /^Procfile(\..+)?$/,
220
+ // Shell / batch scripts in root (server.ps1, server.run.cmd, start.sh …)
221
+ /\.ps1$/,
222
+ /\.sh$/,
223
+ /\.(bat|cmd)$/,
224
+ // Version manager dotfiles
225
+ /^\.nvmrc$/,
226
+ /^\.node-version$/,
227
+ // Git
228
+ /^\.gitignore$/,
229
+ /^\.gitattributes$/,
230
+ /^\.gitmodules$/,
231
+ /^CODEOWNERS$/,
232
+ // Docker misc
233
+ /^\.dockerignore$/,
234
+ // Documentation (any extension, any case)
235
+ /^README(\..+)?$/i,
236
+ /^CHANGELOG(\..+)?$/i,
237
+ /^OVERVIEW(\..+)?$/i,
238
+ /^LICENSE(\..+)?$/i,
239
+ /^CONTRIBUTING(\..+)?$/i,
240
+ /^CODE_OF_CONDUCT(\..+)?$/i,
241
+ /^SECURITY(\..+)?$/i,
242
+ /^SUPPORT(\..+)?$/i,
243
+ /^COPYING(\..+)?$/i,
244
+ /^AUTHORS(\..+)?$/i,
245
+ /^NOTICE(\..+)?$/i,
246
+ // Hosting platform
247
+ /^_redirects$/,
248
+ /^_headers$/,
249
+ /^vercel\.json$/,
250
+ /^netlify\.toml$/,
251
+ /^wrangler\.toml$/,
252
+ /^fly\.toml$/,
253
+ // Status / health check files
254
+ /^status\.json$/,
255
+ /^health\.json$/,
256
+ ]
257
+
258
+ /**
259
+ * Patterns for web assets served directly from the root by a web server.
260
+ * Files matching these go into .root/assets/ so they get copied to root on `prepare`.
261
+ */
262
+ const WEB_ASSET_PATTERNS = [
263
+ /^favicon(\..+)?$/,
264
+ /^apple-touch-icon.*\.(png|jpg)$/,
265
+ /^icon.*\.(png|ico|svg|jpg|jpeg)$/,
266
+ /^android-chrome-.*\.(png|jpg)$/,
267
+ /^mstile-.*\.png$/,
268
+ /^browserconfig\.xml$/,
269
+ /^og[-_]image.*\.(png|jpg|jpeg|webp)$/,
270
+ /^twitter[-_]image.*\.(png|jpg|jpeg|webp)$/,
271
+ /^social[-_]image.*\.(png|jpg|jpeg|webp)$/,
272
+ /^opengraph.*\.(png|jpg|jpeg|webp)$/,
273
+ /^logo.*\.(png|svg|jpg|jpeg|webp)$/,
274
+ /^splash.*\.(png|jpg|jpeg)$/,
275
+ /^screenshot.*\.(png|jpg|jpeg)$/,
276
+ /^[45]\d{2}\.(html?|php|md|txt)$/,
277
+ /^index\.(html?|php)$/,
278
+ /^index[-.].+\.(css|js|mjs)$/,
279
+ /^sw\.js$/,
280
+ /^service-worker.*\.js$/,
281
+ /^workbox-.*\.js$/,
282
+ /^manifest.*\.json$/,
283
+ /^site\.webmanifest$/,
284
+ /^sitemap.*\.(xml|txt)$/,
285
+ /^robots\.txt$/,
286
+ /^status\.json$/,
287
+ /^health\.json$/,
288
+ /^_redirects$/,
289
+ /^_headers$/,
290
+ ]
291
+
292
+ /**
293
+ * Lock files and core manifests that should NEVER be migrated.
294
+ * They must always live in the project root and are managed by package managers.
295
+ */
296
+ const NEVER_MIGRATE = new Set([
297
+ 'package.json',
298
+ 'package-lock.json',
299
+ 'yarn.lock',
300
+ 'pnpm-lock.yaml',
301
+ 'npm-shrinkwrap.json',
85
302
  ])
86
303
 
304
+ // ─── Helper functions ────────────────────────────────────────────────────────
305
+
87
306
  /**
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.
307
+ * Returns all config file names known to rootless across all categories.
308
+ * Excludes NEVER_MIGRATE entries. Used by the migrate command.
309
+ */
310
+ function getAllKnownFiles() {
311
+ const all = new Set()
312
+ for (const tool of TOOL_CONFIGS) {
313
+ for (const f of tool.files) all.add(f)
314
+ }
315
+ for (const tool of POSITIONAL_TOOLS) {
316
+ for (const f of tool.files) all.add(f)
317
+ }
318
+ for (const f of ROOT_REQUIRED_FILES) {
319
+ all.add(f)
320
+ }
321
+ for (const f of NEVER_MIGRATE) {
322
+ all.delete(f)
323
+ }
324
+ return all
325
+ }
326
+
327
+ /**
328
+ * Returns true if the file can be redirected via CLI flags (not copied to root).
90
329
  */
91
330
  function isPatchable(filename) {
92
331
  return (
@@ -96,23 +335,30 @@ function isPatchable(filename) {
96
335
  }
97
336
 
98
337
  /**
99
- * Returns true if the file must always be physically present in root.
338
+ * Returns true if the file must be physically present in the project root.
339
+ * Checks exact names first, then the pattern registry.
100
340
  */
101
341
  function isRootRequired(filename) {
342
+ if (NEVER_MIGRATE.has(filename)) return false
102
343
  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
344
+ if (ROOT_REQUIRED_PATTERNS.some(p => p.test(filename))) return true
345
+ if (!isPatchable(filename)) return true // unknown file → safe default: copy to root
109
346
  return false
110
347
  }
111
348
 
112
349
  /**
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
350
+ * Returns true if the file is a web asset that should be served
351
+ * directly from the project root by a web server (images, HTML, SW, manifest …).
352
+ * These land in .root/assets/ and are copied to root on `prepare`.
353
+ */
354
+ function isWebAsset(filename) {
355
+ return WEB_ASSET_PATTERNS.some(p => p.test(filename))
356
+ }
357
+
358
+ // ─── Config map building ─────────────────────────────────────────────────────
359
+
360
+ /**
361
+ * Build a map of { cmd → { path, flag, positional } } based on files in configsDir.
116
362
  */
117
363
  async function buildConfigMap(configsDir, projectRoot) {
118
364
  let files = []
@@ -124,10 +370,8 @@ async function buildConfigMap(configsDir, projectRoot) {
124
370
 
125
371
  const map = {}
126
372
 
127
- // Flag-based tools
128
373
  for (const tool of TOOL_CONFIGS) {
129
- if (!tool.files.length) continue
130
- if (map[tool.cmd]) continue
374
+ if (!tool.files.length || map[tool.cmd]) continue
131
375
  const found = tool.files.find(f => files.includes(f))
132
376
  if (!found) continue
133
377
  const abs = path.join(configsDir, found)
@@ -135,39 +379,36 @@ async function buildConfigMap(configsDir, projectRoot) {
135
379
  map[tool.cmd] = { path: rel.startsWith('.') ? rel : `./${rel}`, flag: tool.flag, positional: false }
136
380
  }
137
381
 
138
- // Positional tools (pm2 etc.)
139
382
  for (const tool of POSITIONAL_TOOLS) {
140
383
  if (map[tool.cmd]) continue
141
384
  const found = tool.files.find(f => files.includes(f))
142
385
  if (!found) continue
143
386
  const abs = path.join(configsDir, found)
144
387
  const rel = path.relative(projectRoot, abs).replace(/\\/g, '/')
145
- map[tool.cmd] = { path: rel.startsWith('.') ? rel : `./${rel}`, flag: null, positional: true, originalFile: found }
388
+ map[tool.cmd] = { path: rel.startsWith('.') ? rel : `./${rel}`, flag: null, positional: true, originalFiles: tool.files }
146
389
  }
147
390
 
148
391
  return map
149
392
  }
150
393
 
394
+ // ─── Script patching ─────────────────────────────────────────────────────────
395
+
151
396
  /**
152
- * Inject --config flag into a single script string where missing.
153
- * Handles semicolons, &&, || (multi-command scripts).
397
+ * Inject --config flag (or replace positional arg) in a single script string.
154
398
  */
155
399
  function patchScriptString(script, configMap) {
156
- // Sort by command length descending to avoid partial matches
157
400
  const sorted = Object.entries(configMap).sort((a, b) => b[0].length - a[0].length)
158
401
 
159
402
  let result = script
160
403
  for (const [cmd, info] of sorted) {
161
404
  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 ?? [])) {
405
+ for (const file of (info.originalFiles ?? [])) {
164
406
  const escapedFile = file.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
165
407
  const escapedCmd = cmd.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/\s+/g, '\\s+')
166
408
  const regex = new RegExp(`(${escapedCmd})\\s+${escapedFile}`, 'g')
167
409
  result = result.replace(regex, `$1 ${info.path}`)
168
410
  }
169
411
  } else {
170
- // Flag-based: insert --config <path> after command if not already present
171
412
  const flag = info.flag
172
413
  const escapedCmd = cmd.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/\s+/g, '\\s+')
173
414
  const escapedFlag = flag.replace(/-/g, '\\-')
@@ -203,16 +444,12 @@ async function patchPackageScripts(projectRoot, configsDir, logger) {
203
444
  }
204
445
  }
205
446
 
206
- if (changed) {
207
- await writeJsonFile(pkgPath, pkg)
208
- }
447
+ if (changed) await writeJsonFile(pkgPath, pkg)
209
448
  return changed
210
449
  }
211
450
 
212
451
  /**
213
452
  * 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
453
  */
217
454
  async function addPrepareHook(projectRoot) {
218
455
  const pkgPath = path.join(projectRoot, 'package.json')
@@ -232,4 +469,17 @@ async function addPrepareHook(projectRoot) {
232
469
  return pkg.scripts.prepare
233
470
  }
234
471
 
235
- export { patchPackageScripts, addPrepareHook, buildConfigMap, patchScriptString, isPatchable, isRootRequired }
472
+ export {
473
+ patchPackageScripts,
474
+ addPrepareHook,
475
+ buildConfigMap,
476
+ patchScriptString,
477
+ isPatchable,
478
+ isRootRequired,
479
+ isWebAsset,
480
+ getAllKnownFiles,
481
+ NEVER_MIGRATE,
482
+ ROOT_REQUIRED_PATTERNS,
483
+ WEB_ASSET_PATTERNS,
484
+ }
485
+