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.
- package/README.md +173 -0
- package/bin/rootless.js +15 -0
- package/package.json +46 -0
- package/src/cli/commandRegistry.js +42 -0
- package/src/cli/commands/benchmark.js +43 -0
- package/src/cli/commands/clean.js +16 -0
- package/src/cli/commands/completion.js +59 -0
- package/src/cli/commands/debug.js +50 -0
- package/src/cli/commands/doctor.js +63 -0
- package/src/cli/commands/graph.js +34 -0
- package/src/cli/commands/init.js +53 -0
- package/src/cli/commands/inspect.js +48 -0
- package/src/cli/commands/migrate.js +63 -0
- package/src/cli/commands/prepare.js +18 -0
- package/src/cli/commands/stats.js +40 -0
- package/src/cli/commands/status.js +47 -0
- package/src/cli/commands/validate.js +34 -0
- package/src/cli/commands/watch.js +40 -0
- package/src/cli/index.js +53 -0
- package/src/core/cliWrapper.js +106 -0
- package/src/core/configGraph.js +50 -0
- package/src/core/configSchema.js +45 -0
- package/src/core/containerLoader.js +67 -0
- package/src/core/executionContext.js +27 -0
- package/src/core/fileHashCache.js +39 -0
- package/src/core/generatedManifest.js +35 -0
- package/src/core/generator.js +59 -0
- package/src/core/overrideStrategy.js +15 -0
- package/src/core/pathResolver.js +91 -0
- package/src/core/remoteLoader.js +64 -0
- package/src/core/rootSearch.js +36 -0
- package/src/core/versionCheck.js +42 -0
- package/src/experimental/capsule.js +11 -0
- package/src/experimental/index.js +20 -0
- package/src/experimental/virtualFS.js +13 -0
- package/src/index.js +39 -0
- package/src/plugins/builtins/assetPlugin.js +34 -0
- package/src/plugins/builtins/configPlugin.js +38 -0
- package/src/plugins/builtins/envPlugin.js +33 -0
- package/src/plugins/builtins/index.js +9 -0
- package/src/plugins/pluginLoader.js +35 -0
- package/src/plugins/pluginRegistry.js +36 -0
- package/src/types/configTypes.js +23 -0
- package/src/types/errors.js +43 -0
- package/src/types/pluginInterface.js +25 -0
- package/src/utils/debounce.js +22 -0
- package/src/utils/fsUtils.js +48 -0
- package/src/utils/hashUtils.js +24 -0
- package/src/utils/logger.js +34 -0
- package/src/utils/prompt.js +15 -0
- package/src/watch/dirtySet.js +30 -0
- package/src/watch/incrementalRegeneration.js +32 -0
- 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
|
package/bin/rootless.js
ADDED
|
@@ -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
|
+
}
|