rootless-config 1.0.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.
Files changed (53) hide show
  1. package/README.md +173 -0
  2. package/bin/rootless.js +15 -0
  3. package/package.json +46 -0
  4. package/src/cli/commandRegistry.js +42 -0
  5. package/src/cli/commands/benchmark.js +43 -0
  6. package/src/cli/commands/clean.js +16 -0
  7. package/src/cli/commands/completion.js +59 -0
  8. package/src/cli/commands/debug.js +50 -0
  9. package/src/cli/commands/doctor.js +63 -0
  10. package/src/cli/commands/graph.js +34 -0
  11. package/src/cli/commands/init.js +53 -0
  12. package/src/cli/commands/inspect.js +48 -0
  13. package/src/cli/commands/migrate.js +63 -0
  14. package/src/cli/commands/prepare.js +18 -0
  15. package/src/cli/commands/stats.js +40 -0
  16. package/src/cli/commands/status.js +47 -0
  17. package/src/cli/commands/validate.js +34 -0
  18. package/src/cli/commands/watch.js +40 -0
  19. package/src/cli/index.js +53 -0
  20. package/src/core/cliWrapper.js +106 -0
  21. package/src/core/configGraph.js +50 -0
  22. package/src/core/configSchema.js +45 -0
  23. package/src/core/containerLoader.js +67 -0
  24. package/src/core/executionContext.js +27 -0
  25. package/src/core/fileHashCache.js +39 -0
  26. package/src/core/generatedManifest.js +35 -0
  27. package/src/core/generator.js +59 -0
  28. package/src/core/overrideStrategy.js +15 -0
  29. package/src/core/pathResolver.js +91 -0
  30. package/src/core/remoteLoader.js +64 -0
  31. package/src/core/rootSearch.js +36 -0
  32. package/src/core/versionCheck.js +42 -0
  33. package/src/experimental/capsule.js +11 -0
  34. package/src/experimental/index.js +20 -0
  35. package/src/experimental/virtualFS.js +13 -0
  36. package/src/index.js +39 -0
  37. package/src/plugins/builtins/assetPlugin.js +34 -0
  38. package/src/plugins/builtins/configPlugin.js +38 -0
  39. package/src/plugins/builtins/envPlugin.js +33 -0
  40. package/src/plugins/builtins/index.js +9 -0
  41. package/src/plugins/pluginLoader.js +35 -0
  42. package/src/plugins/pluginRegistry.js +36 -0
  43. package/src/types/configTypes.js +23 -0
  44. package/src/types/errors.js +43 -0
  45. package/src/types/pluginInterface.js +25 -0
  46. package/src/utils/debounce.js +22 -0
  47. package/src/utils/fsUtils.js +48 -0
  48. package/src/utils/hashUtils.js +24 -0
  49. package/src/utils/logger.js +34 -0
  50. package/src/utils/prompt.js +15 -0
  51. package/src/watch/dirtySet.js +30 -0
  52. package/src/watch/incrementalRegeneration.js +32 -0
  53. package/src/watch/watcher.js +38 -0
package/README.md ADDED
@@ -0,0 +1,173 @@
1
+ # rootless-config
2
+
3
+ > Keep your project root clean. Store all config files in `.root/`, generate them where tools expect them.
4
+
5
+ ## Problem
6
+
7
+ Every modern JS project accumulates a pile of config files at the root:
8
+
9
+ ```
10
+ vite.config.js
11
+ eslint.config.js
12
+ prettier.config.js
13
+ postcss.config.js
14
+ tailwind.config.js
15
+ .env
16
+ ```
17
+
18
+ They aren't your code. They clutter navigation, git diffs, and onboarding.
19
+
20
+ ## Solution
21
+
22
+ Move them into `.root/`. `rootless-config` generates proxy files (or copies) exactly where tools expect them.
23
+
24
+ ```
25
+ project/
26
+ .root/
27
+ configs/
28
+ vite.config.js
29
+ eslint.config.js
30
+ env/
31
+ .env
32
+ assets/
33
+ favicon.ico
34
+ src/
35
+ package.json
36
+ ```
37
+
38
+ After `rootless prepare`:
39
+
40
+ ```
41
+ vite.config.js ← proxy → .root/configs/vite.config.js
42
+ eslint.config.js ← proxy → .root/configs/eslint.config.js
43
+ .env ← copy from .root/env/.env
44
+ ```
45
+
46
+ All generated files are git-ignored automatically.
47
+
48
+ ## Installation
49
+
50
+ ```bash
51
+ npm install -D rootless-config
52
+ ```
53
+
54
+ ## Quick Start
55
+
56
+ ```bash
57
+ rootless init # scaffold .root/ structure
58
+ # move your configs into .root/configs/, .root/env/, .root/assets/
59
+ rootless prepare # generate proxy/copy files
60
+ ```
61
+
62
+ Add to `package.json`:
63
+
64
+ ```json
65
+ {
66
+ "scripts": {
67
+ "dev": "rootless prepare && vite"
68
+ }
69
+ }
70
+ ```
71
+
72
+ ## Project Structure
73
+
74
+ ```
75
+ .root/
76
+ configs/ ← *.config.js, *.config.ts
77
+ env/ ← .env, .env.production
78
+ assets/ ← favicon.ico, robots.txt
79
+ rootless.config.json
80
+ rootless.version
81
+ ```
82
+
83
+ ## Commands
84
+
85
+ | Command | Description |
86
+ |---------|-------------|
87
+ | `rootless init` | Scaffold a new `.root/` container |
88
+ | `rootless prepare` | Generate proxy/copy files |
89
+ | `rootless watch` | Watch `.root/` and regenerate on change |
90
+ | `rootless clean` | Remove generated files |
91
+ | `rootless doctor` | Health-check the container |
92
+ | `rootless status` | Show status of generated files |
93
+ | `rootless migrate` | Move existing configs into `.root/` |
94
+ | `rootless inspect` | Show detected config sources |
95
+ | `rootless graph` | Display config dependency graph |
96
+ | `rootless validate` | Validate `rootless.config.json` |
97
+ | `rootless debug` | Dump internal state |
98
+ | `rootless stats` | Show project statistics |
99
+ | `rootless benchmark` | Measure prepare pipeline timings |
100
+ | `rootless completion` | Generate shell autocompletion |
101
+
102
+ ### Flags
103
+
104
+ ```bash
105
+ rootless prepare --yes # auto-approve file override prompts (CI-friendly)
106
+ rootless prepare --no # auto-decline file override prompts
107
+ rootless prepare --verbose # verbose output
108
+ rootless prepare --silent # suppress all output
109
+ ```
110
+
111
+ ## Programmatic API
112
+
113
+ ```js
114
+ import { prepare, watch, clean } from 'rootless-config'
115
+
116
+ await prepare()
117
+ await prepare({ yes: true })
118
+
119
+ const watcher = await watch()
120
+ // ...
121
+ await watcher.stop()
122
+
123
+ await clean()
124
+ ```
125
+
126
+ ## rootless.config.json
127
+
128
+ ```json
129
+ {
130
+ "mode": "proxy",
131
+ "containerPath": ".root",
132
+ "plugins": [],
133
+ "remote": [],
134
+ "experimental": {
135
+ "virtualFS": false,
136
+ "capsule": false
137
+ }
138
+ }
139
+ ```
140
+
141
+ | Field | Type | Default | Description |
142
+ |-------|------|---------|-------------|
143
+ | `mode` | `"proxy" \| "copy"` | `"proxy"` | Generation mode |
144
+ | `containerPath` | `string` | `".root"` | Path to container (relative to project root) |
145
+ | `plugins` | `string[]` | `[]` | External plugin module names |
146
+ | `remote` | `string[]` | `[]` | Remote config URLs |
147
+ | `experimental` | `object` | `{}` | Feature flags |
148
+
149
+ ## FAQ
150
+
151
+ **Q: Will this break my tools?**
152
+ Proxy mode generates `export { default } from "./.root/configs/vite.config.js"` — tools load the original file through the re-export. Copy mode duplicates the file.
153
+
154
+ **Q: What about CI?**
155
+ Use `rootless prepare --yes` or set `CI=true` in environment — overrides are auto-approved.
156
+
157
+ **Q: Monorepo?**
158
+ Place `.root/` at the monorepo root. Each sub-package finds it automatically via upward traversal. Or set `containerPath` per package.
159
+
160
+ **Q: My config file already exists at root?**
161
+ `rootless prepare` will ask `Override? (y/n)`. Existing unmanaged files are never silently overwritten.
162
+
163
+ ## Documentation
164
+
165
+ - [Beginner Guide](docs/beginner-guide.md)
166
+ - [Advanced Guide](docs/advanced.md)
167
+ - [Plugin API](docs/plugin-api.md)
168
+ - [CLI Reference](docs/cli-reference.md)
169
+ - [Architecture](docs/architecture.md)
170
+
171
+ ## License
172
+
173
+ MIT
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { run } from '../src/cli/index.js'
4
+
5
+ process.on('unhandledRejection', err => {
6
+ process.stderr.write(`[ROOTLESS] Fatal: ${err?.message ?? err}\n`)
7
+ process.exit(1)
8
+ })
9
+
10
+ process.on('uncaughtException', err => {
11
+ process.stderr.write(`[ROOTLESS] Fatal: ${err?.message ?? err}\n`)
12
+ process.exit(1)
13
+ })
14
+
15
+ await run(process.argv)
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "rootless-config",
3
+ "version": "1.0.0",
4
+ "description": "Store project config files outside the project root, auto-deploy them where tools expect them.",
5
+ "type": "module",
6
+ "bin": {
7
+ "rootless": "./bin/rootless.js"
8
+ },
9
+ "main": "./src/index.js",
10
+ "exports": {
11
+ ".": "./src/index.js"
12
+ },
13
+ "files": [
14
+ "bin",
15
+ "src",
16
+ "README.md"
17
+ ],
18
+ "engines": {
19
+ "node": ">=18.0.0"
20
+ },
21
+ "scripts": {
22
+ "test": "vitest run",
23
+ "test:watch": "vitest",
24
+ "test:coverage": "vitest run --coverage",
25
+ "lint": "eslint src bin"
26
+ },
27
+ "dependencies": {
28
+ "chokidar": "^3.6.0",
29
+ "commander": "^12.1.0",
30
+ "cosmiconfig": "^9.0.0",
31
+ "fs-extra": "^11.2.0"
32
+ },
33
+ "devDependencies": {
34
+ "@vitest/coverage-v8": "^2.0.0",
35
+ "eslint": "^9.0.0",
36
+ "vitest": "^2.0.0"
37
+ },
38
+ "keywords": [
39
+ "config",
40
+ "rootless",
41
+ "cli",
42
+ "devtool",
43
+ "zero-root"
44
+ ],
45
+ "license": "MIT"
46
+ }
@@ -0,0 +1,42 @@
1
+ /*-------- Auto-discovers and registers CLI commands from src/cli/commands/ --------*/
2
+
3
+ import path from 'node:path'
4
+ import { fileURLToPath } from 'node:url'
5
+ import { readdir } from 'node:fs/promises'
6
+ import { RcsError } from '../types/errors.js'
7
+
8
+ const COMMANDS_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), 'commands')
9
+
10
+ function createCommandRegistry() {
11
+ const commands = new Map()
12
+
13
+ async function discover() {
14
+ const entries = await readdir(COMMANDS_DIR, { withFileTypes: true })
15
+ const jsFiles = entries.filter(e => e.isFile() && e.name.endsWith('.js'))
16
+
17
+ for (const file of jsFiles) {
18
+ const mod = await import(path.join(COMMANDS_DIR, file.name))
19
+ const descriptor = mod.default ?? mod
20
+ if (!descriptor?.name || typeof descriptor?.handler !== 'function') continue
21
+ commands.set(descriptor.name, descriptor)
22
+ }
23
+ }
24
+
25
+ function list() {
26
+ return [...commands.values()]
27
+ }
28
+
29
+ function get(name) {
30
+ return commands.get(name) ?? null
31
+ }
32
+
33
+ async function dispatch(name, args, context) {
34
+ const cmd = commands.get(name)
35
+ if (!cmd) throw new RcsError(`Unknown command: ${name}`, 'UNKNOWN_COMMAND')
36
+ return cmd.handler(args, context)
37
+ }
38
+
39
+ return { discover, list, get, dispatch }
40
+ }
41
+
42
+ export { createCommandRegistry }
@@ -0,0 +1,43 @@
1
+ /*-------- rootless benchmark — measures prepare pipeline phase timings --------*/
2
+
3
+ import { createLogger } from '../../utils/logger.js'
4
+ import { buildContext, loadConfig } from '../../core/cliWrapper.js'
5
+ import { getGraph } from '../../core/configGraph.js'
6
+ import { preparePipeline } from '../../core/cliWrapper.js'
7
+ import { resolveContainerPath } from '../../core/pathResolver.js'
8
+
9
+ async function time(label, fn) {
10
+ const start = performance.now()
11
+ await fn()
12
+ return { label, ms: (performance.now() - start).toFixed(2) }
13
+ }
14
+
15
+ export default {
16
+ name: 'benchmark',
17
+ description: 'Measure prepare pipeline phase timings',
18
+
19
+ async handler(args) {
20
+ const logger = createLogger({ verbose: args.verbose ?? false })
21
+ const timings = []
22
+
23
+ await time('total prepare', async () => {
24
+ const containerPath = await resolveContainerPath()
25
+
26
+ const configTiming = await time('config load', () => loadConfig(containerPath))
27
+ timings.push(configTiming)
28
+
29
+ const config = await loadConfig(containerPath)
30
+ const graphTiming = await time('graph build', () => getGraph(config))
31
+ timings.push(graphTiming)
32
+
33
+ const pipeTiming = await time('full prepare pipeline', () => preparePipeline({ yes: true, silent: true }))
34
+ timings.push(pipeTiming)
35
+ }).then(t => timings.push(t))
36
+
37
+ process.stdout.write('\nBenchmark Results\n\n')
38
+ for (const { label, ms } of timings) {
39
+ process.stdout.write(` ${label.padEnd(30)} ${ms}ms\n`)
40
+ }
41
+ process.stdout.write('\n')
42
+ },
43
+ }
@@ -0,0 +1,16 @@
1
+ /*-------- rootless clean — removes only files tracked in .generated.json --------*/
2
+
3
+ import { cleanPipeline } from '../../core/cliWrapper.js'
4
+
5
+ export default {
6
+ name: 'clean',
7
+ description: 'Remove generated files tracked in .root/.generated.json',
8
+
9
+ async handler(args) {
10
+ const options = {
11
+ verbose: args.verbose ?? false,
12
+ silent: args.silent ?? false,
13
+ }
14
+ await cleanPipeline(options)
15
+ },
16
+ }
@@ -0,0 +1,59 @@
1
+ /*-------- rootless completion — generates shell autocompletion scripts --------*/
2
+
3
+ import { createCommandRegistry } from '../commandRegistry.js'
4
+
5
+ const SHELLS = ['bash', 'zsh', 'fish']
6
+
7
+ function generateBash(commands) {
8
+ const names = commands.map(c => c.name).join(' ')
9
+ return `
10
+ _rootless_completions() {
11
+ local cur="\${COMP_WORDS[COMP_CWORD]}"
12
+ COMPREPLY=($(compgen -W "${names}" -- "$cur"))
13
+ }
14
+ complete -F _rootless_completions rootless
15
+ `.trim()
16
+ }
17
+
18
+ function generateZsh(commands) {
19
+ const entries = commands.map(c => ` '${c.name}:${c.description}'`).join('\n')
20
+ return `
21
+ #compdef rootless
22
+ _rootless() {
23
+ local -a commands
24
+ commands=(
25
+ ${entries}
26
+ )
27
+ _describe 'command' commands
28
+ }
29
+ _rootless "$@"
30
+ `.trim()
31
+ }
32
+
33
+ function generateFish(commands) {
34
+ return commands
35
+ .map(c => `complete -c rootless -n '__fish_use_subcommand' -a '${c.name}' -d '${c.description}'`)
36
+ .join('\n')
37
+ }
38
+
39
+ const GENERATORS = { bash: generateBash, zsh: generateZsh, fish: generateFish }
40
+
41
+ export default {
42
+ name: 'completion',
43
+ description: 'Generate shell autocompletion script (bash, zsh, fish)',
44
+
45
+ async handler(args) {
46
+ const shell = args._[0] ?? 'bash'
47
+ if (!SHELLS.includes(shell)) {
48
+ process.stderr.write(`Unknown shell: ${shell}. Supported: ${SHELLS.join(', ')}\n`)
49
+ process.exitCode = 1
50
+ return
51
+ }
52
+
53
+ const registry = createCommandRegistry()
54
+ await registry.discover()
55
+ const commands = registry.list()
56
+
57
+ process.stdout.write(GENERATORS[shell](commands) + '\n')
58
+ },
59
+ }
@@ -0,0 +1,50 @@
1
+ /*-------- rootless debug — dumps full internal state for diagnostics --------*/
2
+
3
+ import { createLogger } from '../../utils/logger.js'
4
+ import { resolveProjectRoot, resolveContainerPath, resolveCachePath } from '../../core/pathResolver.js'
5
+ import { loadHashCache } from '../../core/fileHashCache.js'
6
+ import { readManifest } from '../../core/generatedManifest.js'
7
+ import { checkVersionCompatibility } from '../../core/versionCheck.js'
8
+ import { loadCachedGraph } from '../../core/configGraph.js'
9
+ import { fileExists } from '../../utils/fsUtils.js'
10
+ import path from 'node:path'
11
+
12
+ export default {
13
+ name: 'debug',
14
+ description: 'Dump internal state: paths, cache, graph, version info',
15
+
16
+ async handler() {
17
+ const projectRoot = await resolveProjectRoot()
18
+ const containerPath = await resolveContainerPath()
19
+ const cachePath = await resolveCachePath()
20
+
21
+ process.stdout.write('\nRootless Debug Dump\n')
22
+ process.stdout.write('===================\n\n')
23
+ process.stdout.write(`Project root: ${projectRoot}\n`)
24
+ process.stdout.write(`Container path: ${containerPath}\n`)
25
+ process.stdout.write(`Cache path: ${cachePath}\n\n`)
26
+
27
+ const { cliVersion, projectVersion, compatible } = await checkVersionCompatibility()
28
+ process.stdout.write(`CLI version: ${cliVersion}\n`)
29
+ process.stdout.write(`Container ver: ${projectVersion ?? 'none'}\n`)
30
+ process.stdout.write(`Compatible: ${compatible}\n\n`)
31
+
32
+ const managed = await readManifest()
33
+ process.stdout.write(`Managed files (${managed.length}):\n`)
34
+ for (const f of managed) process.stdout.write(` ${path.relative(projectRoot, f)}\n`)
35
+
36
+ const hashCache = await loadHashCache()
37
+ const hashEntries = Object.entries(hashCache.files ?? {})
38
+ process.stdout.write(`\nHash cache entries: ${hashEntries.length}\n`)
39
+
40
+ const graph = await loadCachedGraph()
41
+ if (graph) {
42
+ const nodeCount = Object.keys(graph.nodes ?? {}).length
43
+ process.stdout.write(`\nCached graph: ${nodeCount} nodes (hash: ${graph.configHash ?? '?'})\n`)
44
+ } else {
45
+ process.stdout.write('\nCached graph: none\n')
46
+ }
47
+
48
+ process.stdout.write('\n')
49
+ },
50
+ }
@@ -0,0 +1,63 @@
1
+ /*-------- rootless doctor — health checks for container structure and config --------*/
2
+
3
+ import path from 'node:path'
4
+ import { createLogger } from '../../utils/logger.js'
5
+ import { resolveContainerPath } from '../../core/pathResolver.js'
6
+ import { fileExists } from '../../utils/fsUtils.js'
7
+ import { validateConfig } from '../../core/configSchema.js'
8
+ import { checkVersionCompatibility } from '../../core/versionCheck.js'
9
+
10
+ export default {
11
+ name: 'doctor',
12
+ description: 'Check .root container structure, config validity, and version compatibility',
13
+
14
+ async handler(args) {
15
+ const logger = createLogger({ verbose: args.verbose ?? false })
16
+ let allOk = true
17
+
18
+ const containerPath = await resolveContainerPath()
19
+ const containerExists = await fileExists(containerPath)
20
+
21
+ if (!containerExists) {
22
+ logger.fail(`.root container not found at: ${containerPath}`)
23
+ logger.info('Run: rootless init')
24
+ process.exitCode = 1
25
+ return
26
+ }
27
+ logger.success(`.root found: ${containerPath}`)
28
+
29
+ const manifestPath = path.join(containerPath, 'rootless.config.json')
30
+ if (await fileExists(manifestPath)) {
31
+ const { readJsonFile } = await import('../../utils/fsUtils.js')
32
+ const raw = await readJsonFile(manifestPath)
33
+ const { valid, errors } = validateConfig(raw)
34
+ if (!valid) {
35
+ errors.forEach(e => logger.fail(`Config error: ${e.message}`))
36
+ allOk = false
37
+ } else {
38
+ logger.success('rootless.config.json is valid')
39
+ }
40
+ } else {
41
+ logger.warn('No rootless.config.json found — will use auto-discovery')
42
+ }
43
+
44
+ const { compatible, projectVersion, cliVersion } = await checkVersionCompatibility()
45
+ if (!compatible) {
46
+ logger.fail(`Version mismatch: container=${projectVersion}, cli=${cliVersion}`)
47
+ logger.info('Run: rootless migrate')
48
+ allOk = false
49
+ } else {
50
+ logger.success(`Version compatible (${cliVersion})`)
51
+ }
52
+
53
+ const knownDirs = ['configs', 'env', 'assets']
54
+ for (const dir of knownDirs) {
55
+ const dirPath = path.join(containerPath, dir)
56
+ if (await fileExists(dirPath)) {
57
+ logger.success(`Found: .root/${dir}/`)
58
+ }
59
+ }
60
+
61
+ if (!allOk) process.exitCode = 1
62
+ },
63
+ }
@@ -0,0 +1,34 @@
1
+ /*-------- rootless graph — renders config dependency graph as ASCII tree --------*/
2
+
3
+ import { createLogger } from '../../utils/logger.js'
4
+ import { buildContext, loadConfig } from '../../core/cliWrapper.js'
5
+ import { getGraph } from '../../core/configGraph.js'
6
+ import { resolveContainerPath } from '../../core/pathResolver.js'
7
+ import path from 'node:path'
8
+
9
+ function renderGraph(graph) {
10
+ const nodes = Object.values(graph.nodes)
11
+ if (nodes.length === 0) return 'No config nodes found.'
12
+
13
+ return nodes
14
+ .map((node, i) => {
15
+ const isLast = i === nodes.length - 1
16
+ return `${isLast ? '└─' : '├─'} ${node.name}: .root/${node.relPath}`
17
+ })
18
+ .join('\n')
19
+ }
20
+
21
+ export default {
22
+ name: 'graph',
23
+ description: 'Display the config dependency graph',
24
+
25
+ async handler(args) {
26
+ const logger = createLogger({ verbose: args.verbose ?? false })
27
+ const containerPath = await resolveContainerPath()
28
+ const config = await loadConfig(containerPath)
29
+ const graph = await getGraph(config)
30
+
31
+ process.stdout.write('\nConfig Dependency Graph\n\n')
32
+ process.stdout.write(renderGraph(graph) + '\n\n')
33
+ },
34
+ }
@@ -0,0 +1,53 @@
1
+ /*-------- rootless init — scaffolds a new .root container structure --------*/
2
+
3
+ import path from 'node:path'
4
+ import { createLogger } from '../../utils/logger.js'
5
+ import { fileExists, ensureDir, atomicWrite, writeJsonFile } from '../../utils/fsUtils.js'
6
+ import { confirm } from '../../utils/prompt.js'
7
+ import { resolveProjectRoot } from '../../core/pathResolver.js'
8
+ import { getLibraryVersion } from '../../core/versionCheck.js'
9
+
10
+ const DIRS = ['configs', 'env', 'assets']
11
+
12
+ const DEFAULT_CONFIG = {
13
+ mode: 'proxy',
14
+ experimental: {
15
+ virtualFS: false,
16
+ capsule: false,
17
+ profiles: false,
18
+ remoteConfigs: false,
19
+ },
20
+ }
21
+
22
+ export default {
23
+ name: 'init',
24
+ description: 'Interactively scaffold a new .root container in the project',
25
+
26
+ async handler(args) {
27
+ const logger = createLogger({ verbose: args.verbose ?? false })
28
+ const projectRoot = await resolveProjectRoot()
29
+ const containerPath = path.join(projectRoot, '.root')
30
+
31
+ if (await fileExists(containerPath)) {
32
+ const proceed = await confirm('.root already exists. Reinitialize?')
33
+ if (!proceed) {
34
+ logger.info('Init cancelled')
35
+ return
36
+ }
37
+ }
38
+
39
+ for (const dir of DIRS) {
40
+ await ensureDir(path.join(containerPath, dir))
41
+ logger.success(`Created: .root/${dir}/`)
42
+ }
43
+
44
+ await writeJsonFile(path.join(containerPath, 'rootless.config.json'), DEFAULT_CONFIG)
45
+ logger.success('Created: .root/rootless.config.json')
46
+
47
+ await atomicWrite(path.join(containerPath, 'rootless.version'), getLibraryVersion())
48
+ logger.success('Created: .root/rootless.version')
49
+
50
+ logger.info('Done. Move your config files into .root/configs/, .root/env/, or .root/assets/')
51
+ logger.info('Then run: rootless prepare')
52
+ },
53
+ }
@@ -0,0 +1,48 @@
1
+ /*-------- rootless inspect — shows resolved config sources --------*/
2
+
3
+ import path from 'node:path'
4
+ import { createLogger } from '../../utils/logger.js'
5
+ import { resolveContainerPath } from '../../core/pathResolver.js'
6
+ import { fileExists } from '../../utils/fsUtils.js'
7
+ import { discoverFiles } from '../../core/containerLoader.js'
8
+
9
+ export default {
10
+ name: 'inspect',
11
+ description: 'Show all detected config sources and their paths in the container',
12
+
13
+ async handler(args) {
14
+ const logger = createLogger({ verbose: args.verbose ?? false })
15
+ const containerPath = await resolveContainerPath()
16
+
17
+ if (!(await fileExists(containerPath))) {
18
+ logger.fail(`Container not found: ${containerPath}`)
19
+ process.exitCode = 1
20
+ return
21
+ }
22
+
23
+ process.stdout.write('\nConfig Inspector\n\n')
24
+
25
+ const files = await discoverFiles(containerPath)
26
+ for (const [category, filePaths] of Object.entries(files)) {
27
+ process.stdout.write(`[${category}]\n`)
28
+ for (const fp of filePaths) {
29
+ const rel = path.relative(containerPath, fp)
30
+ process.stdout.write(` ${path.basename(fp)}\n source: .root/${rel.replace(/\\/g, '/')}\n`)
31
+ }
32
+ }
33
+
34
+ const manifestPath = path.join(containerPath, 'rootless.config.json')
35
+ if (await fileExists(manifestPath)) {
36
+ const { readJsonFile } = await import('../../utils/fsUtils.js')
37
+ const manifest = await readJsonFile(manifestPath)
38
+ process.stdout.write('\n[manifest entries]\n')
39
+ for (const [key, val] of Object.entries(manifest)) {
40
+ if (typeof val === 'string') {
41
+ process.stdout.write(` ${key}: .root/${val}\n`)
42
+ }
43
+ }
44
+ }
45
+
46
+ process.stdout.write('\n')
47
+ },
48
+ }