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
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/*-------- Generic debounce utility --------*/
|
|
2
|
+
|
|
3
|
+
function debounce(fn, delay) {
|
|
4
|
+
let timer = null
|
|
5
|
+
|
|
6
|
+
function debounced(...args) {
|
|
7
|
+
clearTimeout(timer)
|
|
8
|
+
timer = setTimeout(() => {
|
|
9
|
+
timer = null
|
|
10
|
+
fn(...args)
|
|
11
|
+
}, delay)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
debounced.cancel = () => {
|
|
15
|
+
clearTimeout(timer)
|
|
16
|
+
timer = null
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return debounced
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export { debounce }
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/*-------- Atomic filesystem helpers — all operations are async --------*/
|
|
2
|
+
|
|
3
|
+
import { createWriteStream, createReadStream } from 'node:fs'
|
|
4
|
+
import { rename, mkdir, access, constants, writeFile, readFile, rm } from 'node:fs/promises'
|
|
5
|
+
import path from 'node:path'
|
|
6
|
+
import { pipeline } from 'node:stream/promises'
|
|
7
|
+
|
|
8
|
+
async function fileExists(filePath) {
|
|
9
|
+
try {
|
|
10
|
+
await access(filePath, constants.F_OK)
|
|
11
|
+
return true
|
|
12
|
+
} catch {
|
|
13
|
+
return false
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function ensureDir(dirPath) {
|
|
18
|
+
await mkdir(dirPath, { recursive: true })
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function atomicWrite(targetPath, content) {
|
|
22
|
+
const tmp = `${targetPath}.tmp`
|
|
23
|
+
await ensureDir(path.dirname(targetPath))
|
|
24
|
+
await writeFile(tmp, content, 'utf8')
|
|
25
|
+
await rename(tmp, targetPath)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function readJsonFile(filePath) {
|
|
29
|
+
const raw = await readFile(filePath, 'utf8')
|
|
30
|
+
return JSON.parse(raw)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function writeJsonFile(filePath, data) {
|
|
34
|
+
await atomicWrite(filePath, JSON.stringify(data, null, 2))
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function removeFile(filePath) {
|
|
38
|
+
await rm(filePath, { force: true })
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function copyFile(src, dest) {
|
|
42
|
+
await ensureDir(path.dirname(dest))
|
|
43
|
+
const readable = createReadStream(src)
|
|
44
|
+
const writable = createWriteStream(dest)
|
|
45
|
+
await pipeline(readable, writable)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export { fileExists, ensureDir, atomicWrite, readJsonFile, writeJsonFile, removeFile, copyFile }
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/*-------- SHA-256 hashing utilities using native node:crypto --------*/
|
|
2
|
+
|
|
3
|
+
import { createHash } from 'node:crypto'
|
|
4
|
+
import { createReadStream } from 'node:fs'
|
|
5
|
+
|
|
6
|
+
function hashString(content) {
|
|
7
|
+
return createHash('sha256').update(content, 'utf8').digest('hex')
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function hashObject(obj) {
|
|
11
|
+
return hashString(JSON.stringify(obj))
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async function hashFile(filePath) {
|
|
15
|
+
return new Promise((resolve, reject) => {
|
|
16
|
+
const hash = createHash('sha256')
|
|
17
|
+
const stream = createReadStream(filePath)
|
|
18
|
+
stream.on('data', chunk => hash.update(chunk))
|
|
19
|
+
stream.on('end', () => resolve(hash.digest('hex')))
|
|
20
|
+
stream.on('error', reject)
|
|
21
|
+
})
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export { hashString, hashObject, hashFile }
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/*-------- Leveled logger — injected via execution context, never imported globally --------*/
|
|
2
|
+
|
|
3
|
+
const LEVELS = { debug: 0, info: 1, warn: 2, error: 3, silent: 4 }
|
|
4
|
+
const PREFIX = '[ROOTLESS]'
|
|
5
|
+
|
|
6
|
+
const COLORS = {
|
|
7
|
+
debug: '\x1b[36m',
|
|
8
|
+
info: '\x1b[32m',
|
|
9
|
+
warn: '\x1b[33m',
|
|
10
|
+
error: '\x1b[31m',
|
|
11
|
+
reset: '\x1b[0m',
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function createLogger({ verbose = false, silent = false } = {}) {
|
|
15
|
+
const minLevel = silent ? LEVELS.silent : verbose ? LEVELS.debug : LEVELS.info
|
|
16
|
+
|
|
17
|
+
function log(level, message) {
|
|
18
|
+
if (LEVELS[level] < minLevel) return
|
|
19
|
+
const color = COLORS[level] ?? ''
|
|
20
|
+
const line = `${color}${PREFIX} ${message}${COLORS.reset}`
|
|
21
|
+
level === 'error' ? process.stderr.write(line + '\n') : process.stdout.write(line + '\n')
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
debug: msg => log('debug', msg),
|
|
26
|
+
info: msg => log('info', msg),
|
|
27
|
+
warn: msg => log('warn', msg),
|
|
28
|
+
error: msg => log('error', msg),
|
|
29
|
+
success: msg => process.stdout.write(`\x1b[32m✔ ${msg}${COLORS.reset}\n`),
|
|
30
|
+
fail: msg => process.stderr.write(`\x1b[31m✖ ${msg}${COLORS.reset}\n`),
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export { createLogger }
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/*-------- Interactive terminal prompt via readline/promises --------*/
|
|
2
|
+
|
|
3
|
+
import { createInterface } from 'node:readline/promises'
|
|
4
|
+
|
|
5
|
+
async function confirm(message) {
|
|
6
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout })
|
|
7
|
+
try {
|
|
8
|
+
const answer = await rl.question(`${message} (y/n): `)
|
|
9
|
+
return answer.trim().toLowerCase() === 'y'
|
|
10
|
+
} finally {
|
|
11
|
+
rl.close()
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export { confirm }
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/*-------- Dirty file Set with debounced flush — deduplicates watch events --------*/
|
|
2
|
+
|
|
3
|
+
import { debounce } from '../utils/debounce.js'
|
|
4
|
+
|
|
5
|
+
const DEBOUNCE_MS = 100
|
|
6
|
+
|
|
7
|
+
function createDirtySet(onFlush) {
|
|
8
|
+
const dirty = new Set()
|
|
9
|
+
|
|
10
|
+
const flush = debounce(async () => {
|
|
11
|
+
if (dirty.size === 0) return
|
|
12
|
+
const snapshot = new Set(dirty)
|
|
13
|
+
dirty.clear()
|
|
14
|
+
await onFlush(snapshot)
|
|
15
|
+
}, DEBOUNCE_MS)
|
|
16
|
+
|
|
17
|
+
function add(filePath) {
|
|
18
|
+
dirty.add(filePath)
|
|
19
|
+
flush()
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function clear() {
|
|
23
|
+
dirty.clear()
|
|
24
|
+
flush.cancel()
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return { add, clear, get size() { return dirty.size } }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export { createDirtySet }
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/*-------- Incremental regeneration — only affected files are rebuilt --------*/
|
|
2
|
+
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { resolveProjectRoot } from '../core/pathResolver.js'
|
|
5
|
+
import { generateFile } from '../core/generator.js'
|
|
6
|
+
|
|
7
|
+
async function runIncrementalRegeneration(changedPaths, ctx) {
|
|
8
|
+
const projectRoot = await resolveProjectRoot()
|
|
9
|
+
const results = []
|
|
10
|
+
|
|
11
|
+
for (const sourcePath of changedPaths) {
|
|
12
|
+
const plugin = ctx.pluginRegistry.resolve(sourcePath)[0] ?? null
|
|
13
|
+
const target = path.join(projectRoot, path.basename(sourcePath))
|
|
14
|
+
|
|
15
|
+
ctx.logger.info(`Changed: ${path.basename(sourcePath)}`)
|
|
16
|
+
|
|
17
|
+
const result = await generateFile({
|
|
18
|
+
source: sourcePath,
|
|
19
|
+
target,
|
|
20
|
+
mode: 'proxy',
|
|
21
|
+
plugin,
|
|
22
|
+
options: ctx.options,
|
|
23
|
+
logger: ctx.logger,
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
results.push(result)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return results
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export { runIncrementalRegeneration }
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/*-------- Chokidar wrapper — normalizes FS events and manages lifecycle --------*/
|
|
2
|
+
|
|
3
|
+
import chokidar from 'chokidar'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import { EventEmitter } from 'node:events'
|
|
6
|
+
|
|
7
|
+
function createWatcher(watchPaths, options = {}) {
|
|
8
|
+
const emitter = new EventEmitter()
|
|
9
|
+
let watcher = null
|
|
10
|
+
|
|
11
|
+
function start() {
|
|
12
|
+
watcher = chokidar.watch(watchPaths, {
|
|
13
|
+
ignoreInitial: options.ignoreInitial ?? true,
|
|
14
|
+
persistent: true,
|
|
15
|
+
ignored: [/(^|[/\\])\..cache/, /\.tmp$/],
|
|
16
|
+
awaitWriteFinish: { stabilityThreshold: 50, pollInterval: 10 },
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
watcher.on('change', filePath => emitter.emit('change', path.normalize(filePath)))
|
|
20
|
+
watcher.on('add', filePath => emitter.emit('add', path.normalize(filePath)))
|
|
21
|
+
watcher.on('unlink', filePath => emitter.emit('unlink', path.normalize(filePath)))
|
|
22
|
+
watcher.on('error', err => emitter.emit('error', err))
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function stop() {
|
|
26
|
+
if (watcher) await watcher.close()
|
|
27
|
+
watcher = null
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function on(event, cb) {
|
|
31
|
+
emitter.on(event, cb)
|
|
32
|
+
return { start, stop, on }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return { start, stop, on }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export { createWatcher }
|