humanenv 0.1.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 (36) hide show
  1. package/README.md +303 -0
  2. package/package.json +38 -0
  3. package/packages/cli/package.json +15 -0
  4. package/packages/cli/src/bin.js +228 -0
  5. package/packages/client/bin.js +174 -0
  6. package/packages/client/build.js +57 -0
  7. package/packages/client/dist/cli.js +1041 -0
  8. package/packages/client/dist/index.cjs +333 -0
  9. package/packages/client/dist/index.mjs +296 -0
  10. package/packages/client/package.json +24 -0
  11. package/packages/client/src/cli/bin.js +228 -0
  12. package/packages/client/src/cli/entry.js +465 -0
  13. package/packages/client/src/index.ts +31 -0
  14. package/packages/client/src/shared/buffer-shim.d.ts +4 -0
  15. package/packages/client/src/shared/crypto.ts +98 -0
  16. package/packages/client/src/shared/errors.ts +32 -0
  17. package/packages/client/src/shared/index.ts +3 -0
  18. package/packages/client/src/shared/types.ts +118 -0
  19. package/packages/client/src/ws-manager.ts +263 -0
  20. package/packages/server/package.json +21 -0
  21. package/packages/server/src/auth.ts +13 -0
  22. package/packages/server/src/db/index.ts +19 -0
  23. package/packages/server/src/db/interface.ts +33 -0
  24. package/packages/server/src/db/mongo.ts +166 -0
  25. package/packages/server/src/db/sqlite.ts +180 -0
  26. package/packages/server/src/index.ts +123 -0
  27. package/packages/server/src/pk-manager.ts +79 -0
  28. package/packages/server/src/routes/index.ts +110 -0
  29. package/packages/server/src/views/index.ejs +359 -0
  30. package/packages/server/src/ws/router.ts +263 -0
  31. package/packages/shared/package.json +13 -0
  32. package/packages/shared/src/buffer-shim.d.ts +4 -0
  33. package/packages/shared/src/crypto.ts +98 -0
  34. package/packages/shared/src/errors.ts +32 -0
  35. package/packages/shared/src/index.ts +3 -0
  36. package/packages/shared/src/types.ts +119 -0
@@ -0,0 +1,228 @@
1
+ #!/usr/bin/env node
2
+ const { Command } = require('commander')
3
+ const fs = require('fs')
4
+ const path = require('path')
5
+ const os = require('os')
6
+ const { generateFingerprint, SKILL_CONTENT } = require('../shared/index')
7
+
8
+ const program = new Command()
9
+ const CREDENTIALS_DIR = path.join(os.homedir(), '.humanenv')
10
+
11
+ function ensureCredentialsDir() {
12
+ if (!fs.existsSync(CREDENTIALS_DIR)) fs.mkdirSync(CREDENTIALS_DIR, { recursive: true })
13
+ }
14
+
15
+ function readCredentials() {
16
+ const p = path.join(CREDENTIALS_DIR, 'credentials.json')
17
+ if (!fs.existsSync(p)) return null
18
+ try { return JSON.parse(fs.readFileSync(p, 'utf8')) } catch { return null }
19
+ }
20
+
21
+ function writeCredentials(data) {
22
+ ensureCredentialsDir()
23
+ fs.writeFileSync(path.join(CREDENTIALS_DIR, 'credentials.json'), JSON.stringify(data, null, 2), 'utf8')
24
+ }
25
+
26
+ function ensureSkillFile() {
27
+ const skillPath = path.join(process.cwd(), '.agents', 'skills', 'humanenv-usage', 'SKILL.md')
28
+ if (!fs.existsSync(skillPath)) {
29
+ fs.mkdirSync(path.dirname(skillPath), { recursive: true })
30
+ fs.writeFileSync(skillPath, SKILL_CONTENT, 'utf8')
31
+ if (process.stdout.isTTY) console.log('Generated .agents/skills/humanenv-usage/SKILL.md')
32
+ }
33
+ }
34
+
35
+ // ============================================================
36
+ // Main entry: humanenv (no args)
37
+ // ============================================================
38
+
39
+ program
40
+ .action(() => {
41
+ ensureSkillFile()
42
+ if (!process.stdout.isTTY) {
43
+ // Non-TTY: output skill content for agents
44
+ const skillPath = path.join(process.cwd(), '.agents', 'skills', 'humanenv-usage', 'SKILL.md')
45
+ console.log(fs.readFileSync(skillPath, 'utf8'))
46
+ } else {
47
+ // TTY: show human-friendly help
48
+ console.log('HumanEnv - Secure environment variable injection')
49
+ console.log('')
50
+ console.log('Usage:')
51
+ console.log(' humanenv auth --project-name <name> --server-url <url> [--api-key <key>]')
52
+ console.log(' humanenv auth --project-name <name> --server-url <url> --generate-api-key')
53
+ console.log(' humanenv get <key>')
54
+ console.log(' humanenv set <key> <value>')
55
+ console.log(' humanenv server [--port 3056] [--basicAuth]')
56
+ console.log('')
57
+ }
58
+ })
59
+
60
+ // ============================================================
61
+ // Auth command
62
+ // ============================================================
63
+
64
+ program
65
+ .command('auth')
66
+ .option('--project-name <name>')
67
+ .option('--server-url <url>')
68
+ .option('--api-key <key>')
69
+ .option('--generate-api-key', false)
70
+ .action(async (opts) => {
71
+ ensureSkillFile()
72
+ if (!opts.projectName || !opts.serverUrl) {
73
+ console.error('Error: --project-name and --server-url required')
74
+ process.exit(1)
75
+ }
76
+
77
+ const creds = {
78
+ projectName: opts.projectName,
79
+ serverUrl: opts.serverUrl,
80
+ apiKey: opts.apiKey || undefined,
81
+ }
82
+ writeCredentials(creds)
83
+
84
+ if (opts.generateApiKey) {
85
+ const { HumanEnvClient } = require('../ws-manager')
86
+ const client = new HumanEnvClient({
87
+ serverUrl: opts.serverUrl,
88
+ projectName: opts.projectName,
89
+ projectApiKey: opts.apiKey || '',
90
+ maxRetries: 3,
91
+ })
92
+
93
+ try {
94
+ await client.connect()
95
+ const result = await new Promise((resolve, reject) => {
96
+ client.disconnect()
97
+ // For CLI generate-api-key, we use a simple HTTP call instead
98
+ // since WS API key generation is complex
99
+ resolve(null)
100
+ })
101
+ console.log('API key generation request sent. Admin must approve in dashboard.')
102
+ } catch (e) {
103
+ console.error('Failed to connect:', e.message)
104
+ process.exit(1)
105
+ }
106
+ } else {
107
+ // Verify credentials by connecting
108
+ const { HumanEnvClient } = require('../ws-manager')
109
+ const client = new HumanEnvClient({
110
+ serverUrl: opts.serverUrl,
111
+ projectName: opts.projectName,
112
+ projectApiKey: opts.apiKey || '',
113
+ maxRetries: 3,
114
+ })
115
+
116
+ try {
117
+ await client.connect()
118
+ console.log('Authenticated successfully.')
119
+ client.disconnect()
120
+ } catch (e) {
121
+ console.error('Auth failed:', e.message)
122
+ process.exit(1)
123
+ }
124
+ }
125
+
126
+ console.log('Credentials stored in', path.join(CREDENTIALS_DIR, 'credentials.json'))
127
+ })
128
+
129
+ // ============================================================
130
+ // Get command
131
+ // ============================================================
132
+
133
+ program
134
+ .command('get')
135
+ .argument('<key>', 'Environment variable key')
136
+ .action(async (key) => {
137
+ const creds = readCredentials()
138
+ if (!creds) {
139
+ console.error('Error: Not authenticated. Run: humanenv auth --project-name <name> --server-url <url>')
140
+ process.exit(1)
141
+ }
142
+
143
+ const { HumanEnvClient } = require('../ws-manager')
144
+ const client = new HumanEnvClient({
145
+ serverUrl: creds.serverUrl,
146
+ projectName: creds.projectName,
147
+ projectApiKey: creds.apiKey || '',
148
+ maxRetries: 3,
149
+ })
150
+
151
+ try {
152
+ await client.connect()
153
+ const value = await client.get(key)
154
+ // Non-TTY: output raw value only
155
+ if (!process.stdout.isTTY) {
156
+ process.stdout.write(value)
157
+ } else {
158
+ console.log(value)
159
+ }
160
+ client.disconnect()
161
+ } catch (e) {
162
+ console.error('Failed to get env:', e.message)
163
+ process.exit(1)
164
+ }
165
+ })
166
+
167
+ // ============================================================
168
+ // Set command
169
+ // ============================================================
170
+
171
+ program
172
+ .command('set')
173
+ .argument('<key>', 'Environment variable key')
174
+ .argument('<value>', 'Environment variable value')
175
+ .action(async (key, value) => {
176
+ const creds = readCredentials()
177
+ if (!creds) {
178
+ console.error('Error: Not authenticated. Run: humanenv auth --project-name <name> --server-url <url>')
179
+ process.exit(1)
180
+ }
181
+
182
+ const { HumanEnvClient } = require('../ws-manager')
183
+ const client = new HumanEnvClient({
184
+ serverUrl: creds.serverUrl,
185
+ projectName: creds.projectName,
186
+ projectApiKey: creds.apiKey || '',
187
+ maxRetries: 3,
188
+ })
189
+
190
+ try {
191
+ await client.connect()
192
+ await client.set(key, value)
193
+ console.log('Set', key)
194
+ client.disconnect()
195
+ } catch (e) {
196
+ console.error('Failed to set env:', e.message)
197
+ process.exit(1)
198
+ }
199
+ })
200
+
201
+ // ============================================================
202
+ // Server command (delegates to server package)
203
+ // ============================================================
204
+
205
+ program
206
+ .command('server')
207
+ .option('--port <number>')
208
+ .option('--basicAuth', false)
209
+ .action((opts) => {
210
+ const portArg = opts.port ? `--port=${opts.port}` : ''
211
+ const basicAuthArg = opts.basicAuth ? '--basicAuth' : ''
212
+ const serverPath = path.join(__dirname, '..', '..', 'server', 'src', 'index.ts')
213
+ const serverJs = fs.existsSync(path.join(__dirname, '..', '..', 'server', 'dist', 'index.js'))
214
+ ? path.join(__dirname, '..', '..', 'server', 'dist', 'index.js')
215
+ : null
216
+
217
+ if (serverJs && fs.existsSync(serverJs)) {
218
+ require('child_process').fork(serverJs, [portArg, basicAuthArg].filter(Boolean), { stdio: 'inherit' })
219
+ } else {
220
+ // Fallback: run via tsx
221
+ const { spawn } = require('child_process')
222
+ const args = [serverPath, portArg, basicAuthArg].filter(Boolean)
223
+ const child = spawn('npx', ['tsx', ...args], { stdio: 'inherit', shell: process.platform === 'win32' })
224
+ child.on('close', (code) => process.exit(code))
225
+ }
226
+ })
227
+
228
+ program.parse(process.argv)
@@ -0,0 +1,465 @@
1
+ // humanenv CLI - entry point for bundling
2
+ const { Command } = require('commander')
3
+ const fs = require('fs')
4
+ const path = require('path')
5
+ const os = require('os')
6
+ const { execSync } = require('child_process')
7
+
8
+ const { generateFingerprint, SKILL_CONTENT } = require('../shared/index')
9
+ const { HumanEnvClient } = require('../ws-manager')
10
+
11
+ const CREDENTIALS_DIR = path.join(os.homedir(), '.humanenv')
12
+
13
+ // --json / -j detected before Commander strips args
14
+ const isJson = process.argv.includes('--json') || process.argv.includes('-j')
15
+ const cleanArgs = process.argv.slice(2).filter(a => a !== '--json' && a !== '-j')
16
+ const hasCmd = ['auth', 'get', 'set', 'server'].some(c => cleanArgs.includes(c))
17
+ const wantsHelp = cleanArgs.includes('--help') || cleanArgs.includes('-h')
18
+
19
+ // Handle --help --json before anything else
20
+ if (wantsHelp && isJson) {
21
+ const commandName = cleanArgs.find(a => ['auth', 'get', 'set', 'server'].includes(a))
22
+ const commandsConfig = {
23
+ 'auth': {
24
+ command: 'auth',
25
+ description: 'Authenticate with a HumanEnv server',
26
+ usage: 'humanenv auth [options]',
27
+ options: [
28
+ { flags: '--project-name <name>', description: 'Project name', required: false, optional: true, shorthand: 'pn' },
29
+ { flags: '--pn <name>', description: 'Project name (shorthand)', required: false, optional: true, shorthand: null },
30
+ { flags: '--server-url <url>', description: 'Server URL', required: false, optional: true, shorthand: 'su' },
31
+ { flags: '--su <url>', description: 'Server URL (shorthand)', required: false, optional: true, shorthand: null },
32
+ { flags: '--api-key <key>', description: 'API key (optional)', required: false, optional: true, shorthand: null },
33
+ { flags: '--generate-api-key', description: 'Request a new API key from the server', required: false, optional: false, shorthand: null },
34
+ { flags: '-h, --help', description: 'Display help', required: false, optional: false, shorthand: 'h' }
35
+ ],
36
+ arguments: []
37
+ },
38
+ 'get': {
39
+ command: 'get',
40
+ description: 'Retrieve an environment variable',
41
+ usage: 'humanenv get <key>',
42
+ options: [
43
+ { flags: '-h, --help', description: 'Display help', required: false, optional: false, shorthand: 'h' }
44
+ ],
45
+ arguments: [{ name: 'key', required: true, description: 'Environment variable key' }]
46
+ },
47
+ 'set': {
48
+ command: 'set',
49
+ description: 'Set an environment variable',
50
+ usage: 'humanenv set <key> <value>',
51
+ options: [
52
+ { flags: '-h, --help', description: 'Display help', required: false, optional: false, shorthand: 'h' }
53
+ ],
54
+ arguments: [
55
+ { name: 'key', required: true, description: 'Environment variable key' },
56
+ { name: 'value', required: true, description: 'Environment variable value' }
57
+ ]
58
+ },
59
+ 'server': {
60
+ command: 'server',
61
+ description: 'Start the HumanEnv admin server',
62
+ usage: 'humanenv server [options]',
63
+ options: [
64
+ { flags: '--port <port>', description: 'Port to listen on (default: 3056)', required: false, optional: true, shorthand: null },
65
+ { flags: '--basicAuth', description: 'Enable basic auth for admin UI', required: false, optional: false, shorthand: null },
66
+ { flags: '-h, --help', description: 'Display help', required: false, optional: false, shorthand: 'h' }
67
+ ],
68
+ arguments: []
69
+ }
70
+ }
71
+
72
+ if (commandName && commandsConfig[commandName]) {
73
+ console.log(JSON.stringify(commandsConfig[commandName], null, 2))
74
+ } else {
75
+ console.log(JSON.stringify({
76
+ command: 'humanenv',
77
+ description: 'Secure environment variable injection',
78
+ usage: 'humanenv [command] [options]',
79
+ commands: ['auth', 'get', 'set', 'server'],
80
+ options: [
81
+ { flags: '--json', description: 'Output in JSON format' },
82
+ { flags: '-j', description: 'Shorthand for --json' },
83
+ { flags: '-h, --help', description: 'Display help' }
84
+ ]
85
+ }, null, 2))
86
+ }
87
+ process.exit(0)
88
+ }
89
+
90
+ // ==========================================================
91
+ // AGENTS_FRIENDLY error helper
92
+ // ==========================================================
93
+ function errJson(code, msg, hint, extras) {
94
+ return JSON.stringify(Object.assign(
95
+ { success: false, code, error: msg },
96
+ hint ? { hint } : {},
97
+ extras || {}
98
+ ), null, 2)
99
+ }
100
+
101
+ function failJson(code, msg, hint, extras) {
102
+ if (isJson) {
103
+ console.error(errJson(code, msg, hint, extras))
104
+ } else {
105
+ console.error('Error:', msg)
106
+ if (hint) console.error('Hint:', hint)
107
+ }
108
+ process.exit(1)
109
+ }
110
+
111
+ // ==========================================================
112
+ // No sub-command → show fingerprint + usage (before Commander runs)
113
+ // ==========================================================
114
+ if (!hasCmd && !wantsHelp) {
115
+ ensureSkillFile()
116
+ const sp = path.join(process.cwd(), '.agents', 'skills', 'humanenv-usage', 'SKILL.md')
117
+ const creds = readCredentials()
118
+ const fp = generateFingerprint()
119
+
120
+ if (!process.stdout.isTTY || isJson) {
121
+ // Non-TTY / --json: skill content + auth status JSON
122
+ if (fs.existsSync(sp)) console.log(fs.readFileSync(sp, 'utf8'))
123
+ console.log('')
124
+ if (creds.projectName && creds.serverUrl) {
125
+ console.error(JSON.stringify({
126
+ code: 'AUTH_STATUS', status: 'credentials_found',
127
+ fingerprint: fp, projectName: creds.projectName, serverUrl: creds.serverUrl,
128
+ step: 'Run humanenv auth to connect, then ask the admin to whitelist your fingerprint in the dashboard.'
129
+ }))
130
+ } else {
131
+ console.error(JSON.stringify({
132
+ code: 'NOT_AUTHENTICATED', fingerprint: fp,
133
+ step: 'Run: humanenv auth --project-name <name> --server-url <url> (shorthand: --pn / --su)'
134
+ }))
135
+ }
136
+ } else {
137
+ // TTY: fingerprint + guidance
138
+ console.log('HumanEnv - Secure environment variable injection')
139
+ console.log('')
140
+ console.log('Your fingerprint:', fp)
141
+ console.log('Share this with the project admin to whitelist.')
142
+ if (creds.serverUrl) console.log('Admin UI:', creds.serverUrl)
143
+ console.log('')
144
+ if (creds.projectName && creds.serverUrl) {
145
+ console.log('Credentials configured:')
146
+ console.log(' projectName:', creds.projectName)
147
+ console.log(' serverUrl:', creds.serverUrl)
148
+ console.log('')
149
+ console.log('Next steps:')
150
+ console.log(' 1. Run: humanenv auth')
151
+ console.log(' 2. Admin whitelists your fingerprint in the dashboard at', creds.serverUrl)
152
+ console.log(' 3. Start using: humanenv get <key>')
153
+ } else {
154
+ console.log('Usage:')
155
+ console.log(' humanenv auth --project-name <name> --server-url <url> [--api-key <key>]')
156
+ console.log(' humanenv get <key>')
157
+ console.log(' humanenv set <key> <value>')
158
+ console.log(' humanenv server [--port 3056] [--basicAuth]')
159
+ }
160
+ console.log('')
161
+ console.log('Aliases:')
162
+ console.log(' --pn short for --project-name')
163
+ console.log(' --su short for --server-url')
164
+ console.log(' --json structured JSON output (agent-friendly)')
165
+ console.log('')
166
+ }
167
+ process.exit(0)
168
+ }
169
+
170
+ // ==========================================================
171
+ // Helpers
172
+ // ==========================================================
173
+ function ensureSkillFile() {
174
+ const sp = path.join(process.cwd(), '.agents', 'skills', 'humanenv-usage', 'SKILL.md')
175
+ if (!fs.existsSync(sp)) {
176
+ fs.mkdirSync(path.dirname(sp), { recursive: true })
177
+ fs.writeFileSync(sp, SKILL_CONTENT, 'utf8')
178
+ }
179
+ return sp
180
+ }
181
+
182
+ function readCredentials() {
183
+ const p = path.join(CREDENTIALS_DIR, 'credentials.json')
184
+ if (!fs.existsSync(p)) return {}
185
+ try { return JSON.parse(fs.readFileSync(p, 'utf8')) } catch { return {} }
186
+ }
187
+
188
+ function writeCredentials(d) {
189
+ if (!fs.existsSync(CREDENTIALS_DIR)) fs.mkdirSync(CREDENTIALS_DIR, { recursive: true })
190
+ fs.writeFileSync(path.join(CREDENTIALS_DIR, 'credentials.json'), JSON.stringify(d, null, 2), 'utf8')
191
+ }
192
+
193
+ function sleep(ms) { return new Promise(r => setTimeout(r, ms)) }
194
+
195
+ const pName = o => o.projectName || o.pn
196
+ const pUrl = o => o.serverUrl || o.su
197
+ const nameOpt = ['--project-name <name>', 'Project name']
198
+ const urlOpt = ['--server-url <url>', 'Server URL']
199
+ const pnOpt = ['--pn <name>', 'Project name (shorthand)']
200
+ const suOpt = ['--su <url>', 'Server URL (shorthand)']
201
+
202
+ // ==========================================================
203
+ // Auth
204
+ // ==========================================================
205
+ async function runAuth(opts) {
206
+ ensureSkillFile()
207
+ const projectName = pName(opts)
208
+ const serverUrl = pUrl(opts)
209
+ if (!projectName || !serverUrl) {
210
+ failJson('MISSING_ARGS',
211
+ '--project-name and --server-url are required',
212
+ 'Run: humanenv auth --project-name <name> --server-url <url> (shorthand: --pn / --su)')
213
+ return
214
+ }
215
+
216
+ writeCredentials({ projectName, serverUrl, apiKey: opts.apiKey || undefined })
217
+
218
+ // --- generate-api-key one-shot ---
219
+ if (opts.generateApiKey) {
220
+ const client = new HumanEnvClient({ serverUrl, projectName, projectApiKey: opts.apiKey || '', maxRetries: 1 })
221
+ try {
222
+ await client.connect()
223
+ const key = await client.generateApiKey()
224
+ if (isJson) console.error(errJson('API_KEY_GENERATED', 'API key generated.', null, { success: true, apiKey: key }))
225
+ else console.log('API key generated:', key)
226
+ } catch (e) {
227
+ const fingerprint = generateFingerprint()
228
+ failJson('GENERATE_API_KEY_FAILED',
229
+ `API key generation failed: ${e.message}`,
230
+ 'Ensure the server is running, the project exists, and an admin is online to approve.',
231
+ { fingerprint, projectName, serverUrl })
232
+ } finally { client.disconnect() }
233
+ return
234
+ }
235
+
236
+ // --- normal auth ---
237
+ let client = new HumanEnvClient({ serverUrl, projectName, projectApiKey: opts.apiKey || '', maxRetries: 1 })
238
+ try {
239
+ await client.connect()
240
+ } catch (e) {
241
+ const fingerprint = generateFingerprint()
242
+ let hint = 'Re-run auth to retry.'
243
+ if (/ECONNREFUSED|ENOTFOUND/i.test(e.message)) {
244
+ hint = `Start the server first: humanenv server (or check --server-url)`
245
+ } else if (/project.*not.*found/i.test(e.message)) {
246
+ hint = `Create the project first via the admin UI at ${serverUrl}`
247
+ } else if (/api.*key.*invalid/i.test(e.message)) {
248
+ hint = 'Re-run auth with --generate-api-key to request a new key from the admin.'
249
+ }
250
+ failJson('AUTH_FAILED',
251
+ `Auth failed: ${e.message}`, hint,
252
+ { fingerprint, projectName, serverUrl })
253
+ }
254
+
255
+ if (client.whitelistStatus === 'approved') {
256
+ if (isJson) {
257
+ console.error(errJson('AUTH_OK', 'Authentication successful.', null, { success: true, whitelisted: true }))
258
+ } else {
259
+ console.log('Authentication successful.')
260
+ }
261
+ if (client) client.disconnect()
262
+ return
263
+ }
264
+
265
+ // Not approved yet
266
+ const fingerprint = generateFingerprint()
267
+
268
+ if (process.stdout.isTTY && !isJson) {
269
+ // TTY mode: poll every 1s until approved or 120s timeout
270
+ console.log('Auth OK, not whitelisted yet.')
271
+ console.log('Fingerprint:', fingerprint)
272
+ console.log('Waiting for admin approval... (120s timeout, Ctrl+C to abort)')
273
+
274
+ let attempts = 0
275
+ while (attempts < 120) {
276
+ attempts++
277
+ await sleep(1000)
278
+ try {
279
+ if (client) client.disconnect()
280
+ await sleep(100)
281
+ client = new HumanEnvClient({ serverUrl, projectName, projectApiKey: opts.apiKey || '', maxRetries: 1 })
282
+ await client.connect()
283
+ if (client.whitelistStatus === 'approved') {
284
+ console.log('\nApproved. You can now use humanenv get/set.')
285
+ return
286
+ }
287
+ } catch { /* still pending, keep polling */ }
288
+ }
289
+
290
+ // Timeout
291
+ console.log('\nTimed out waiting for approval.')
292
+ console.log('To approve manually: open the admin UI at ' + serverUrl)
293
+ console.log('Then go to the Whitelist tab and approve fingerprint:', fingerprint)
294
+ return
295
+ }
296
+
297
+ // Non-TTY / --json: report immediately, don't wait
298
+ if (isJson) {
299
+ console.error(errJson('AUTH_PENDING',
300
+ 'Auth OK but not whitelisted yet.',
301
+ `Share this fingerprint with the admin to approve it.`,
302
+ { success: true, whitelisted: false, fingerprint, projectName, serverUrl,
303
+ step: 'admin_must_approve_whitelist' }))
304
+ } else {
305
+ console.log('Auth OK, not whitelisted yet.')
306
+ console.log('Fingerprint:', fingerprint)
307
+ console.log('Share this with the admin to approve it.')
308
+ }
309
+
310
+ if (client) client.disconnect()
311
+ }
312
+
313
+ // ==========================================================
314
+ // Resolve creds with overrides (get / set)
315
+ // ==========================================================
316
+ function resolveCreds(opts) {
317
+ const st = readCredentials()
318
+ const projectName = pName(opts) || st.projectName
319
+ const serverUrl = pUrl(opts) || st.serverUrl
320
+
321
+ if (!projectName || !serverUrl) {
322
+ failJson('NOT_AUTHENTICATED',
323
+ 'No credentials found.',
324
+ 'Run: humanenv auth --project-name <name> --server-url <url> (or pass --pn / --su per-command)',
325
+ { tip: 'You can also set HUMANENV_PROJECT_NAME and HUMANENV_SERVER_URL env vars.' })
326
+ }
327
+
328
+ return {
329
+ projectName,
330
+ serverUrl,
331
+ apiKey: opts.apiKey || st.apiKey || '',
332
+ fingerprint: generateFingerprint()
333
+ }
334
+ }
335
+
336
+ // ==========================================================
337
+ // Program
338
+ // ==========================================================
339
+ const program = new Command()
340
+
341
+ program
342
+ .command('auth')
343
+ .description('Authenticate with a HumanEnv server')
344
+ .option(...nameOpt).option(...pnOpt).option(...urlOpt).option(...suOpt)
345
+ .option('--api-key <key>', 'API key (optional)')
346
+ .option('--generate-api-key', 'Request a new API key from the server')
347
+ .action(async o => { await runAuth(o) })
348
+
349
+ program
350
+ .command('get')
351
+ .description('Retrieve an environment variable')
352
+ .argument('<key>', 'Environment variable key')
353
+ .option(...nameOpt).option(...pnOpt).option(...urlOpt).option(...suOpt)
354
+ .action(async (key, opts) => {
355
+ const c = resolveCreds(opts)
356
+ const cli = new HumanEnvClient({
357
+ serverUrl: c.serverUrl,
358
+ projectName: c.projectName,
359
+ projectApiKey: c.apiKey || '',
360
+ maxRetries: 3
361
+ })
362
+ try {
363
+ await cli.connect()
364
+ const value = await cli.get(key)
365
+ if (isJson) {
366
+ console.error(JSON.stringify({ success: true, code: 'ENV_RETRIEVED', key, hasValue: !!value }, null, 2))
367
+ process.stdout.write(value)
368
+ } else if (!process.stdout.isTTY) {
369
+ process.stdout.write(value)
370
+ } else {
371
+ console.log(value)
372
+ }
373
+ } catch (e) {
374
+ const fingerprint = c.fingerprint
375
+ let hint = null
376
+ let code = 'GET_FAILED'
377
+
378
+ if (/whitelist/i.test(e.message) || /not.*approved/i.test(e.message)) {
379
+ code = 'CLIENT_NOT_WHITELISTED'
380
+ hint = `Run humanenv auth to submit your fingerprint. Then ask the admin to approve it at ${c.serverUrl}`
381
+ } else if (/api.?mode/i.test(e.message)) {
382
+ code = 'ENV_API_MODE_ONLY'
383
+ hint = 'This env flag is API-mode only: use the SDK (await humanenv.get()) instead of CLI.'
384
+ } else if (/not.*found|key.*not.*exist/i.test(e.message)) {
385
+ code = 'ENV_KEY_NOT_FOUND'
386
+ hint = `Key "${key}" does not exist. Create it via humanenv set or the admin UI.`
387
+ } else if (/ECONNREFUSED|ENOTFOUND/i.test(e.message)) {
388
+ hint = `Start the server first: humanenv server (or verify ${c.serverUrl})`
389
+ } else if (/timeout/i.test(e.message)) {
390
+ hint = 'Server took too long to respond. Check network connectivity.'
391
+ } else if (/invalid.*api.*key/i.test(e.message)) {
392
+ hint = 'Your API key may have expired. Run humanenv auth --generate-api-key for a new one.'
393
+ }
394
+
395
+ failJson(code, `Failed to get env: ${e.message}`, hint,
396
+ { key, fingerprint, projectName: c.projectName, serverUrl: c.serverUrl })
397
+ } finally { cli.disconnect() }
398
+ })
399
+
400
+ program
401
+ .command('set')
402
+ .description('Set an environment variable')
403
+ .argument('<key>', 'Environment variable key')
404
+ .argument('<value>', 'Environment variable value')
405
+ .option(...nameOpt).option(...pnOpt).option(...urlOpt).option(...suOpt)
406
+ .action(async (key, value, opts) => {
407
+ const c = resolveCreds(opts)
408
+ const cli = new HumanEnvClient({
409
+ serverUrl: c.serverUrl,
410
+ projectName: c.projectName,
411
+ projectApiKey: c.apiKey || '',
412
+ maxRetries: 3
413
+ })
414
+ try {
415
+ await cli.connect()
416
+ await cli.set(key, value)
417
+ if (isJson) {
418
+ console.error(JSON.stringify({ success: true, code: 'ENV_SET', key }, null, 2))
419
+ } else {
420
+ console.log('Set', key)
421
+ }
422
+ } catch (e) {
423
+ const fingerprint = c.fingerprint
424
+ let hint = null
425
+ let code = 'SET_FAILED'
426
+
427
+ if (/whitelist/i.test(e.message) || /not.*approved/i.test(e.message)) {
428
+ code = 'CLIENT_NOT_WHITELISTED'
429
+ hint = `Run humanenv auth to submit your fingerprint. Then ask the admin to approve it at ${c.serverUrl}`
430
+ } else if (/ECONNREFUSED|ENOTFOUND/i.test(e.message)) {
431
+ hint = `Start the server first: humanenv server (or verify ${c.serverUrl})`
432
+ } else if (/timeout/i.test(e.message)) {
433
+ hint = 'Server took too long to respond. Check network connectivity.'
434
+ }
435
+
436
+ failJson(code, `Failed to set env: ${e.message}`, hint,
437
+ { key, fingerprint, projectName: c.projectName, serverUrl: c.serverUrl })
438
+ } finally { cli.disconnect() }
439
+ })
440
+
441
+ program
442
+ .command('server')
443
+ .description('Start the HumanEnv admin server')
444
+ .option('--port <port>', 'Port to listen on (default: 3056)')
445
+ .option('--basicAuth', 'Enable basic auth for admin UI')
446
+ .action(opts => {
447
+ const args = []
448
+ if (opts.port) args.push('--port', opts.port)
449
+ if (opts.basicAuth) args.push('--basicAuth')
450
+ try {
451
+ execSync(`npx tsx ${path.join(__dirname, '..', '..', 'server', 'src', 'index.ts')} ${args.join(' ')}`,
452
+ { stdio: 'inherit' })
453
+ } catch {
454
+ try {
455
+ execSync(`node ${path.join(__dirname, '..', '..', 'server', 'dist', 'index.js')} ${args.join(' ')}`,
456
+ { stdio: 'inherit' })
457
+ } catch {
458
+ failJson('SERVER_NOT_FOUND',
459
+ 'Server binary not found.',
460
+ 'Install humanenv-server or run from the monorepo.')
461
+ }
462
+ }
463
+ })
464
+
465
+ program.parse(['node', 'humanenv', ...cleanArgs])