rootless-config 1.6.0 → 1.6.1

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.6.0",
3
+ "version": "1.6.1",
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": {
@@ -0,0 +1,144 @@
1
+ /*-------- rootless activate — install PS profile hook for auto PATH injection --------*/
2
+
3
+ import { readFile, writeFile, mkdir } from 'node:fs/promises'
4
+ import path from 'node:path'
5
+ import os from 'node:os'
6
+ import { spawn } from 'node:child_process'
7
+ import { fileExists } from '../../utils/fsUtils.js'
8
+ import { createLogger } from '../../utils/logger.js'
9
+
10
+ // The block we inject into $PROFILE — delimited so we can detect/remove it
11
+ const HOOK_START = '# <rootless-hook>'
12
+ const HOOK_END = '# </rootless-hook>'
13
+
14
+ const HOOK_BODY = `
15
+ # <rootless-hook>
16
+ # Auto-injected by rootless-config — adds .root/assets + .root/configs to PATH
17
+ # when entering a project that has a .root/ container.
18
+ function _Rootless_Activate {
19
+ param([string]$Dir = $PWD)
20
+ $rootPath = Join-Path $Dir ".root"
21
+ if (-not (Test-Path $rootPath -PathType Container)) { return }
22
+ $slots = @(
23
+ (Join-Path $rootPath "assets"),
24
+ (Join-Path $rootPath "configs")
25
+ )
26
+ foreach ($slot in $slots) {
27
+ if ((Test-Path $slot) -and ($env:PATH -notlike "*$slot*")) {
28
+ $env:PATH = "$slot;$env:PATH"
29
+ }
30
+ }
31
+ }
32
+
33
+ # Hook into the prompt so activation runs after every cd
34
+ $_RootlessOriginalPrompt = if (Get-Command prompt -ErrorAction SilentlyContinue) {
35
+ ${function:prompt}
36
+ } else { $null }
37
+
38
+ function global:prompt {
39
+ _Rootless_Activate
40
+ if ($_RootlessOriginalPrompt) {
41
+ & $_RootlessOriginalPrompt
42
+ } else {
43
+ "PS $($PWD)> "
44
+ }
45
+ }
46
+
47
+ # Activate immediately for current directory
48
+ _Rootless_Activate
49
+ # </rootless-hook>
50
+ `
51
+
52
+ async function getProfilePath() {
53
+ // Ask PowerShell for the actual profile path
54
+ return new Promise((resolve) => {
55
+ let out = ''
56
+ const ps = spawn('powershell', ['-NoProfile', '-Command', 'echo $PROFILE'], {
57
+ stdio: ['ignore', 'pipe', 'ignore'],
58
+ })
59
+ ps.stdout.on('data', d => { out += d.toString() })
60
+ ps.on('exit', () => resolve(out.trim()))
61
+ })
62
+ }
63
+
64
+ async function readProfile(profilePath) {
65
+ try { return await readFile(profilePath, 'utf8') } catch { return '' }
66
+ }
67
+
68
+ async function isHookInstalled(profilePath) {
69
+ const content = await readProfile(profilePath)
70
+ return content.includes(HOOK_START)
71
+ }
72
+
73
+ async function installHook(profilePath) {
74
+ await mkdir(path.dirname(profilePath), { recursive: true })
75
+ const existing = await readProfile(profilePath)
76
+ await writeFile(profilePath, existing + '\n' + HOOK_BODY, 'utf8')
77
+ }
78
+
79
+ async function removeHook(profilePath) {
80
+ const content = await readProfile(profilePath)
81
+ const startIdx = content.indexOf(HOOK_START)
82
+ const endIdx = content.indexOf(HOOK_END)
83
+ if (startIdx === -1) return false
84
+ const cleaned = content.slice(0, startIdx) + content.slice(endIdx + HOOK_END.length)
85
+ await writeFile(profilePath, cleaned.trim() + '\n', 'utf8')
86
+ return true
87
+ }
88
+
89
+ export default {
90
+ name: 'activate',
91
+ description: 'Install PowerShell profile hook — .root/ files become available as commands automatically',
92
+
93
+ async handler(args) {
94
+ const logger = createLogger({ verbose: args.verbose ?? false })
95
+
96
+ // --remove flag
97
+ if (args.remove) {
98
+ const profilePath = await getProfilePath()
99
+ const removed = await removeHook(profilePath)
100
+ if (removed) {
101
+ logger.success('Rootless hook removed from PowerShell profile.')
102
+ } else {
103
+ logger.info('No rootless hook found in profile.')
104
+ }
105
+ return
106
+ }
107
+
108
+ // --env flag: just print the activation snippet for current session
109
+ if (args.env) {
110
+ process.stdout.write(HOOK_BODY + '\n')
111
+ return
112
+ }
113
+
114
+ const profilePath = await getProfilePath()
115
+
116
+ if (await isHookInstalled(profilePath)) {
117
+ logger.success('Rootless hook already installed.')
118
+ logger.info(`Profile: ${profilePath}`)
119
+ logger.info('')
120
+ logger.info('Already active in all new PowerShell sessions.')
121
+ logger.info('To activate in THIS session right now:')
122
+ logger.info('')
123
+ logger.info(' rootless activate --env | Invoke-Expression')
124
+ return
125
+ }
126
+
127
+ await installHook(profilePath)
128
+
129
+ logger.success('Rootless hook installed!')
130
+ logger.info(`Profile: ${profilePath}`)
131
+ logger.info('')
132
+ logger.info('From now on, in ANY new PowerShell session:')
133
+ logger.info(' → navigate to a project with .root/')
134
+ logger.info(' → all files in .root/assets/ and .root/configs/ are in PATH')
135
+ logger.info(' → type .server.run, .server.ps1 etc. directly')
136
+ logger.info('')
137
+ logger.info('To activate in THIS session right now:')
138
+ logger.info('')
139
+ logger.info(' rootless activate --env | Invoke-Expression')
140
+ logger.info('')
141
+ logger.info('To remove the hook later:')
142
+ logger.info(' rootless activate --remove')
143
+ },
144
+ }
package/src/cli/index.js CHANGED
@@ -25,6 +25,11 @@ async function run(argv) {
25
25
  sub.option('--no-yes', 'Auto-decline all file override prompts')
26
26
  }
27
27
 
28
+ if (cmd.name === 'activate') {
29
+ sub.option('--remove', 'Remove the hook from PowerShell profile')
30
+ sub.option('--env', 'Print activation snippet for current session only (pipe to Invoke-Expression)')
31
+ }
32
+
28
33
  if (cmd.name === 'serve') {
29
34
  sub.option('--port <number>', 'Port to listen on (default: 3000)')
30
35
  }