rootless-config 1.1.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 +1 -1
- package/src/cli/commands/migrate.js +23 -9
- package/src/core/cliWrapper.js +36 -10
- package/src/core/scriptPatcher.js +246 -39
package/package.json
CHANGED
|
@@ -5,16 +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
|
-
|
|
11
|
-
const ENV_PATTERN
|
|
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.
|
|
12
15
|
|
|
13
16
|
async function findMigratableFiles(projectRoot) {
|
|
17
|
+
const knownFiles = getAllKnownFiles()
|
|
14
18
|
const entries = await readdir(projectRoot, { withFileTypes: true })
|
|
15
19
|
return entries
|
|
16
|
-
.filter(e =>
|
|
17
|
-
|
|
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
|
+
})
|
|
18
31
|
.map(e => e.name)
|
|
19
32
|
.sort()
|
|
20
33
|
}
|
|
@@ -62,18 +75,19 @@ export default {
|
|
|
62
75
|
for (const name of candidates) {
|
|
63
76
|
const src = path.join(projectRoot, name)
|
|
64
77
|
const isEnv = ENV_PATTERN.test(name)
|
|
65
|
-
const
|
|
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)
|
|
66
82
|
|
|
67
83
|
await ensureDir(destDir)
|
|
68
84
|
const content = await readFile(src, 'utf8')
|
|
69
85
|
await atomicWrite(path.join(destDir, name), content)
|
|
70
86
|
|
|
71
87
|
if (isCleanMode) {
|
|
72
|
-
// Clean mode: delete original — no proxy file in root
|
|
73
88
|
await unlink(src)
|
|
74
|
-
logger.success(`Moved to .root/${
|
|
89
|
+
logger.success(`Moved to .root/${destSubdir}/${name} (deleted from root)`)
|
|
75
90
|
} else {
|
|
76
|
-
// Proxy mode: replace original with re-export stub
|
|
77
91
|
const rel = path.relative(path.dirname(src), path.join(destDir, name)).replace(/\\/g, '/')
|
|
78
92
|
const relPath = rel.startsWith('.') ? rel : `./${rel}`
|
|
79
93
|
await atomicWrite(src, `export { default } from "${relPath}"\n`)
|
package/src/core/cliWrapper.js
CHANGED
|
@@ -13,7 +13,7 @@ import { readManifest, removeFromManifest } from './generatedManifest.js'
|
|
|
13
13
|
import { removeHash } from './fileHashCache.js'
|
|
14
14
|
import { resolveProjectRoot, resolveContainerPath, setContainerOverride } from './pathResolver.js'
|
|
15
15
|
import { fileExists, removeFile } from '../utils/fsUtils.js'
|
|
16
|
-
import { patchPackageScripts } from './scriptPatcher.js'
|
|
16
|
+
import { patchPackageScripts, isRootRequired } from './scriptPatcher.js'
|
|
17
17
|
import { ValidationError } from '../types/errors.js'
|
|
18
18
|
|
|
19
19
|
async function loadConfig(containerPath) {
|
|
@@ -50,28 +50,54 @@ async function preparePipeline(options = {}) {
|
|
|
50
50
|
const containerPath = await resolveContainerPath()
|
|
51
51
|
const config = ctx.container.manifest ?? {}
|
|
52
52
|
const graph = await getGraph(config)
|
|
53
|
-
const isCleanMode = (config.mode ?? '
|
|
53
|
+
const isCleanMode = (config.mode ?? 'clean') === 'clean'
|
|
54
54
|
|
|
55
55
|
const projectRoot = await resolveProjectRoot()
|
|
56
56
|
|
|
57
57
|
if (isCleanMode) {
|
|
58
|
-
|
|
59
|
-
const envDir = path.join(containerPath, 'env')
|
|
58
|
+
const envDir = path.join(containerPath, 'env')
|
|
60
59
|
const configsDir = path.join(containerPath, 'configs')
|
|
60
|
+
const assetsDir = path.join(containerPath, 'assets')
|
|
61
61
|
|
|
62
|
-
const
|
|
62
|
+
const copyEntries = []
|
|
63
|
+
|
|
64
|
+
// 1. env/* — all .env files always go to root
|
|
63
65
|
if (await fileExists(envDir)) {
|
|
64
|
-
const
|
|
65
|
-
for (const f of envFiles) {
|
|
66
|
+
for (const f of await readdir(envDir, { withFileTypes: true })) {
|
|
66
67
|
if (!f.isFile()) continue
|
|
67
68
|
const absPath = path.join(envDir, f.name)
|
|
68
69
|
const plugin = ctx.pluginRegistry.resolve(absPath)[0] ?? null
|
|
69
|
-
|
|
70
|
+
copyEntries.push({ source: absPath, target: path.join(projectRoot, f.name), mode: 'copy', plugin })
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// 2. assets/* — favicon, robots.txt, manifests etc. always go to root
|
|
75
|
+
if (await fileExists(assetsDir)) {
|
|
76
|
+
for (const f of await readdir(assetsDir, { withFileTypes: true })) {
|
|
77
|
+
if (!f.isFile()) continue
|
|
78
|
+
const absPath = path.join(assetsDir, f.name)
|
|
79
|
+
const plugin = ctx.pluginRegistry.resolve(absPath)[0] ?? null
|
|
80
|
+
copyEntries.push({ source: absPath, target: path.join(projectRoot, f.name), mode: 'copy', plugin })
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// 3. configs/* — split by whether the file can be redirected via CLI flags
|
|
85
|
+
// isRootRequired(name) = true → copy to root (tool can't be redirected: .nvmrc, .editorconfig, etc.)
|
|
86
|
+
// isRootRequired(name) = false → skip copy, only patch scripts below
|
|
87
|
+
if (await fileExists(configsDir)) {
|
|
88
|
+
for (const f of await readdir(configsDir, { withFileTypes: true })) {
|
|
89
|
+
if (!f.isFile()) continue
|
|
90
|
+
if (isRootRequired(f.name)) {
|
|
91
|
+
const absPath = path.join(configsDir, f.name)
|
|
92
|
+
const plugin = ctx.pluginRegistry.resolve(absPath)[0] ?? null
|
|
93
|
+
copyEntries.push({ source: absPath, target: path.join(projectRoot, f.name), mode: 'copy', plugin })
|
|
94
|
+
}
|
|
70
95
|
}
|
|
71
96
|
}
|
|
72
97
|
|
|
73
|
-
const results = await generateFiles(
|
|
98
|
+
const results = await generateFiles(copyEntries, options, ctx.logger)
|
|
74
99
|
|
|
100
|
+
// 4. Patch package.json scripts for all patchable tool configs (vite, eslint, jest, pm2, etc.)
|
|
75
101
|
const patched = await patchPackageScripts(projectRoot, configsDir, ctx.logger)
|
|
76
102
|
if (patched) ctx.logger.success('package.json scripts patched with --config flags')
|
|
77
103
|
else ctx.logger.debug('No script patching needed')
|
|
@@ -80,7 +106,7 @@ async function preparePipeline(options = {}) {
|
|
|
80
106
|
return results
|
|
81
107
|
}
|
|
82
108
|
|
|
83
|
-
// Proxy mode
|
|
109
|
+
// Proxy mode: generate proxy/copy for every file in .root/
|
|
84
110
|
const entries = await buildGenerationEntries(containerPath, graph, ctx)
|
|
85
111
|
const mapped = entries.map(e => ({
|
|
86
112
|
...e,
|
|
@@ -1,31 +1,216 @@
|
|
|
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
|
-
*
|
|
9
|
-
*
|
|
10
|
+
* Tools that support a --config (or similar) flag.
|
|
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).
|
|
10
13
|
*/
|
|
11
14
|
const TOOL_CONFIGS = [
|
|
12
|
-
|
|
13
|
-
{ cmd: 'vite',
|
|
14
|
-
{ cmd: '
|
|
15
|
-
{ cmd: '
|
|
16
|
-
|
|
17
|
-
{ cmd: '
|
|
18
|
-
{ cmd: '
|
|
19
|
-
|
|
20
|
-
{ cmd: '
|
|
21
|
-
|
|
22
|
-
{ cmd: '
|
|
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'] },
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Tools that use the config file as a POSITIONAL argument (not a --flag).
|
|
71
|
+
*/
|
|
72
|
+
const POSITIONAL_TOOLS = [
|
|
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'] },
|
|
23
78
|
]
|
|
24
79
|
|
|
25
80
|
/**
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
81
|
+
* Files that MUST be physically present in the project root.
|
|
82
|
+
* Tools discover them by convention — no CLI redirect is possible.
|
|
83
|
+
* These are COPIED to root by `rootless prepare`.
|
|
84
|
+
*/
|
|
85
|
+
const ROOT_REQUIRED_FILES = new Set([
|
|
86
|
+
// Node / npm
|
|
87
|
+
'.npmrc',
|
|
88
|
+
// Node version managers
|
|
89
|
+
'.nvmrc', '.node-version',
|
|
90
|
+
// Package managers
|
|
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',
|
|
96
|
+
// Editor
|
|
97
|
+
'.editorconfig',
|
|
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',
|
|
123
|
+
// Browser targets
|
|
124
|
+
'.browserslistrc', 'browserslist',
|
|
125
|
+
// Commit / git hooks
|
|
126
|
+
'.huskyrc', '.huskyrc.json', '.huskyrc.js', '.huskyrc.yaml', '.huskyrc.yml',
|
|
127
|
+
'commitlint.config.js', 'commitlint.config.cjs', 'commitlint.config.mjs',
|
|
128
|
+
'.commitlintrc', '.commitlintrc.js', '.commitlintrc.json', '.commitlintrc.yaml', '.commitlintrc.yml',
|
|
129
|
+
'lint-staged.config.js', 'lint-staged.config.cjs', 'lint-staged.config.mjs',
|
|
130
|
+
'.lintstagedrc', '.lintstagedrc.js', '.lintstagedrc.json', '.lintstagedrc.cjs',
|
|
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
|
|
135
|
+
'.czrc', '.cz.json',
|
|
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)
|
|
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
|
+
])
|
|
150
|
+
|
|
151
|
+
/**
|
|
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).
|
|
188
|
+
*/
|
|
189
|
+
function isPatchable(filename) {
|
|
190
|
+
return (
|
|
191
|
+
TOOL_CONFIGS.some(t => t.files.includes(filename)) ||
|
|
192
|
+
POSITIONAL_TOOLS.some(t => t.files.includes(filename))
|
|
193
|
+
)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Returns true if the file must be physically present in the project root.
|
|
198
|
+
*/
|
|
199
|
+
function isRootRequired(filename) {
|
|
200
|
+
if (NEVER_MIGRATE.has(filename)) return false
|
|
201
|
+
if (ROOT_REQUIRED_FILES.has(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
|
|
207
|
+
return false
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// ─── Config map building ─────────────────────────────────────────────────────
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Build a map of { cmd → { path, flag, positional } } based on files in configsDir.
|
|
29
214
|
*/
|
|
30
215
|
async function buildConfigMap(configsDir, projectRoot) {
|
|
31
216
|
let files = []
|
|
@@ -36,36 +221,52 @@ async function buildConfigMap(configsDir, projectRoot) {
|
|
|
36
221
|
}
|
|
37
222
|
|
|
38
223
|
const map = {}
|
|
224
|
+
|
|
39
225
|
for (const tool of TOOL_CONFIGS) {
|
|
40
|
-
if (map[tool.cmd]) continue
|
|
226
|
+
if (!tool.files.length || map[tool.cmd]) continue
|
|
41
227
|
const found = tool.files.find(f => files.includes(f))
|
|
42
228
|
if (!found) continue
|
|
43
229
|
const abs = path.join(configsDir, found)
|
|
44
230
|
const rel = path.relative(projectRoot, abs).replace(/\\/g, '/')
|
|
45
|
-
map[tool.cmd] = rel.startsWith('.') ? rel : `./${rel}
|
|
231
|
+
map[tool.cmd] = { path: rel.startsWith('.') ? rel : `./${rel}`, flag: tool.flag, positional: false }
|
|
46
232
|
}
|
|
233
|
+
|
|
234
|
+
for (const tool of POSITIONAL_TOOLS) {
|
|
235
|
+
if (map[tool.cmd]) continue
|
|
236
|
+
const found = tool.files.find(f => files.includes(f))
|
|
237
|
+
if (!found) continue
|
|
238
|
+
const abs = path.join(configsDir, found)
|
|
239
|
+
const rel = path.relative(projectRoot, abs).replace(/\\/g, '/')
|
|
240
|
+
map[tool.cmd] = { path: rel.startsWith('.') ? rel : `./${rel}`, flag: null, positional: true, originalFiles: tool.files }
|
|
241
|
+
}
|
|
242
|
+
|
|
47
243
|
return map
|
|
48
244
|
}
|
|
49
245
|
|
|
246
|
+
// ─── Script patching ─────────────────────────────────────────────────────────
|
|
247
|
+
|
|
50
248
|
/**
|
|
51
|
-
* Inject --config flag
|
|
52
|
-
* Handles semicolons, &&, || (multi-command scripts).
|
|
249
|
+
* Inject --config flag (or replace positional arg) in a single script string.
|
|
53
250
|
*/
|
|
54
251
|
function patchScriptString(script, configMap) {
|
|
55
|
-
// Process longer commands first to avoid partial matches (vite build before vite)
|
|
56
252
|
const sorted = Object.entries(configMap).sort((a, b) => b[0].length - a[0].length)
|
|
57
253
|
|
|
58
254
|
let result = script
|
|
59
|
-
for (const [cmd,
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
255
|
+
for (const [cmd, info] of sorted) {
|
|
256
|
+
if (info.positional) {
|
|
257
|
+
for (const file of (info.originalFiles ?? [])) {
|
|
258
|
+
const escapedFile = file.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
259
|
+
const escapedCmd = cmd.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/\s+/g, '\\s+')
|
|
260
|
+
const regex = new RegExp(`(${escapedCmd})\\s+${escapedFile}`, 'g')
|
|
261
|
+
result = result.replace(regex, `$1 ${info.path}`)
|
|
262
|
+
}
|
|
263
|
+
} else {
|
|
264
|
+
const flag = info.flag
|
|
265
|
+
const escapedCmd = cmd.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/\s+/g, '\\s+')
|
|
266
|
+
const escapedFlag = flag.replace(/-/g, '\\-')
|
|
267
|
+
const regex = new RegExp(`((?:^|[;&|\\s]))(${escapedCmd})(?!\\s+${escapedFlag})`, 'g')
|
|
268
|
+
result = result.replace(regex, (_, pre, match) => `${pre}${match} ${flag} ${info.path}`)
|
|
269
|
+
}
|
|
69
270
|
}
|
|
70
271
|
return result
|
|
71
272
|
}
|
|
@@ -95,16 +296,12 @@ async function patchPackageScripts(projectRoot, configsDir, logger) {
|
|
|
95
296
|
}
|
|
96
297
|
}
|
|
97
298
|
|
|
98
|
-
if (changed)
|
|
99
|
-
await writeJsonFile(pkgPath, pkg)
|
|
100
|
-
}
|
|
299
|
+
if (changed) await writeJsonFile(pkgPath, pkg)
|
|
101
300
|
return changed
|
|
102
301
|
}
|
|
103
302
|
|
|
104
303
|
/**
|
|
105
304
|
* Add "prepare": "rootless prepare --yes" to project's package.json.
|
|
106
|
-
* If prepare already exists and doesn't include rootless, prepends to it.
|
|
107
|
-
* Returns the final prepare script value.
|
|
108
305
|
*/
|
|
109
306
|
async function addPrepareHook(projectRoot) {
|
|
110
307
|
const pkgPath = path.join(projectRoot, 'package.json')
|
|
@@ -114,7 +311,7 @@ async function addPrepareHook(projectRoot) {
|
|
|
114
311
|
if (!pkg.scripts) pkg.scripts = {}
|
|
115
312
|
|
|
116
313
|
const existing = pkg.scripts.prepare ?? ''
|
|
117
|
-
if (existing.includes('rootless prepare')) return existing
|
|
314
|
+
if (existing.includes('rootless prepare')) return existing
|
|
118
315
|
|
|
119
316
|
pkg.scripts.prepare = existing
|
|
120
317
|
? `rootless prepare --yes && ${existing}`
|
|
@@ -124,4 +321,14 @@ async function addPrepareHook(projectRoot) {
|
|
|
124
321
|
return pkg.scripts.prepare
|
|
125
322
|
}
|
|
126
323
|
|
|
127
|
-
export {
|
|
324
|
+
export {
|
|
325
|
+
patchPackageScripts,
|
|
326
|
+
addPrepareHook,
|
|
327
|
+
buildConfigMap,
|
|
328
|
+
patchScriptString,
|
|
329
|
+
isPatchable,
|
|
330
|
+
isRootRequired,
|
|
331
|
+
getAllKnownFiles,
|
|
332
|
+
NEVER_MIGRATE,
|
|
333
|
+
}
|
|
334
|
+
|