network-ai 5.3.2 → 5.4.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.
@@ -305,6 +305,83 @@ Connect your AI model to `http://localhost:3001/sse` — it can now:
305
305
 
306
306
  ---
307
307
 
308
+ ### Phase 7 — Multi-Environment Promotion (v5.4.0+)
309
+
310
+ **Goal:** Promote validated config from dev → staging → production through gate-enforced checkpoints.
311
+
312
+ ```bash
313
+ # Initialise the full environment chain in one command
314
+ npx network-ai env init --all
315
+
316
+ # After validating in dev, promote config to st (auto-gate — no approval needed)
317
+ npx network-ai env promote --from dev --to st
318
+
319
+ # Review differences before promoting further
320
+ npx network-ai env diff --from st --to sit
321
+
322
+ # preprod requires a human confirmation
323
+ npx network-ai env promote --from sit --to qa
324
+ npx network-ai env promote --from qa --to preprod --confirmed-by "jane.doe"
325
+
326
+ # prod requires an approval token
327
+ npx network-ai env promote --from preprod --to prod --approved-by "security-board"
328
+ ```
329
+
330
+ **Backup and rollback:**
331
+
332
+ ```bash
333
+ # Create a named backup before a risky change
334
+ npx network-ai env backup create --env prod
335
+
336
+ # List available backups
337
+ npx network-ai env backup list --env prod
338
+
339
+ # Roll back to the latest backup
340
+ npx network-ai env backup restore --env prod --latest
341
+ ```
342
+
343
+ **From TypeScript:**
344
+
345
+ ```typescript
346
+ import { EnvironmentManager } from 'network-ai';
347
+
348
+ const mgr = new EnvironmentManager('.');
349
+ await mgr.initAll();
350
+
351
+ // Promote config files only (live state never promotes)
352
+ const result = await mgr.promote('dev', 'st');
353
+ console.log(result.copiedFiles); // ['trust_levels.json', 'budget_ceilings.json']
354
+
355
+ // Auto-backup destination before overwriting
356
+ const backup = await mgr.backup('st');
357
+ console.log(backup.id); // '2026-05-10T12-00-00-000Z'
358
+
359
+ // Diff two envs
360
+ const diff = await mgr.diff('dev', 'prod');
361
+ for (const f of diff.files) {
362
+ console.log(f.file, f.status, f.changedKeys);
363
+ }
364
+ ```
365
+
366
+ **Source protection** — lock agents inside `data/<env>/`:
367
+
368
+ ```typescript
369
+ import { AgentRuntime, SandboxPolicy } from 'network-ai';
370
+
371
+ const policy: SandboxPolicy = {
372
+ allowedCommands: [],
373
+ allowedPaths: ['data/dev/'],
374
+ sourceProtection: true,
375
+ env: 'dev',
376
+ };
377
+
378
+ const runtime = new AgentRuntime({ policy });
379
+ ```
380
+
381
+ Any `FileAccessor.read/write/list` call outside `data/dev/` will throw `SourceProtectionError` and be caught as `{ success: false, error: '...' }`.
382
+
383
+ ---
384
+
308
385
  ## 5. Enterprise Concerns
309
386
 
310
387
  ### Authentication & IAM
@@ -419,7 +496,7 @@ Run these before declaring the integration production-ready:
419
496
  - [ ] `npx ts-node test-phase4.ts` — 147 behavioral tests pass
420
497
  - [ ] `npx ts-node test-qa.ts` — 67 QA orchestrator tests pass
421
498
  - [ ] `npx ts-node test-phase7.ts` — 94 Phase 7 tests pass (hooks, flow control, composer, semantic search)
422
- - [ ] `npm run test:all` — all 2,711 tests pass across 26 suites
499
+ - [ ] `npm run test:all` — all 2,976 tests pass across 29 suites
423
500
  - [ ] `npm run demo -- --08` runs to completion in < 10 seconds
424
501
 
425
502
  ### Race Condition Safety
@@ -477,7 +554,7 @@ Run these before declaring the integration production-ready:
477
554
  |----------|---------------|
478
555
  | [QUICKSTART.md](QUICKSTART.md) | Get running in 5 minutes |
479
556
  | [QUICKSTART.md § CLI](QUICKSTART.md) | CLI reference — bb, auth, budget, audit commands |
480
- | [references/adapter-system.md](references/adapter-system.md) | All 28 adapters with code examples |
557
+ | [references/adapter-system.md](references/adapter-system.md) | All 29 adapters with code examples |
481
558
  | [references/trust-levels.md](references/trust-levels.md) | Trust scoring formula and agent roles |
482
559
  | [references/auth-guardian.md](references/auth-guardian.md) | Permission system, justification scoring, token lifecycle |
483
560
  | [references/blackboard-schema.md](references/blackboard-schema.md) | Blackboard key conventions and namespacing |
@@ -487,4 +564,4 @@ Run these before declaring the integration production-ready:
487
564
 
488
565
  ---
489
566
 
490
- *Network-AI v5.1.4 · MIT License · https://github.com/Jovancoding/Network-AI*
567
+ *Network-AI v5.4.1 · MIT License · https://github.com/Jovancoding/Network-AI*
package/QUICKSTART.md CHANGED
@@ -42,7 +42,8 @@ npx ts-node setup.ts --check
42
42
  | `a2a` | A2A | none | Agent-to-Agent protocol |
43
43
  | `codex` | Codex | `openai` | OpenAI Codex CLI |
44
44
  | `minimax` | MiniMax | none | MiniMax chat completions |
45
- | `nemoclaw` | NemoClaw | none | NVIDIA sandboxed agent execution |\n| `aps` | APS | none | Delegation-chain trust mapping |
45
+ | `nemoclaw` | NemoClaw | none | NVIDIA sandboxed agent execution |
46
+ | `aps` | APS | none | Delegation-chain trust mapping |
46
47
  | `copilot` | GitHub Copilot | none | Code generate/review/explain/fix/test/refactor |
47
48
  | `langgraph` | LangGraph | `@langchain/langgraph` | Compiled StateGraph execution |
48
49
  | `anthropic-computer-use` | Anthropic Computer Use | `@anthropic-ai/sdk` | Screenshot/click/type/scroll automation |
@@ -323,6 +324,46 @@ network-ai auth check grant_a1b2c3...
323
324
  network-ai auth revoke grant_a1b2c3...
324
325
  ```
325
326
 
327
+ ### Multi-Environment (`env`) — v5.4.0+
328
+
329
+ Isolate agent state across dev / staging / production using the promotion chain.
330
+
331
+ ```bash
332
+ # Initialise all environments at once
333
+ network-ai env init --all
334
+
335
+ # Or initialise a single environment
336
+ network-ai env init --env dev
337
+
338
+ # List environments and key counts
339
+ network-ai env list
340
+
341
+ # Show the promotion chain
342
+ network-ai env chain
343
+
344
+ # Diff two environments (shows +added / -removed / ~changed config keys)
345
+ network-ai env diff --from dev --to prod
346
+
347
+ # Promote config from dev → st (auto-gate, no approval needed)
348
+ network-ai env promote --from dev --to st
349
+
350
+ # Promote to preprod (requires --confirmed-by)
351
+ network-ai env promote --from qa --to preprod --confirmed-by "jane.doe"
352
+
353
+ # Promote to prod (requires --approved-by)
354
+ network-ai env promote --from preprod --to prod --approved-by "security-board"
355
+
356
+ # Backup / restore
357
+ network-ai env backup create --env prod
358
+ network-ai env backup list --env prod
359
+ network-ai env backup restore --env prod --latest
360
+ network-ai env backup prune --env prod --keep 5
361
+ ```
362
+
363
+ Set `NETWORK_AI_ENV=dev` to automatically route all blackboard and Python script operations to `data/dev/`.
364
+
365
+ ---
366
+
326
367
  ### Budget (`budget`)
327
368
 
328
369
  ```bash
package/README.md CHANGED
@@ -5,9 +5,9 @@
5
5
  [![Website](https://img.shields.io/badge/website-network--ai.org-4b9df2?style=flat&logo=web&logoColor=white)](https://network-ai.org/)
6
6
  [![CI](https://github.com/Jovancoding/Network-AI/actions/workflows/ci.yml/badge.svg)](https://github.com/Jovancoding/Network-AI/actions/workflows/ci.yml)
7
7
  [![CodeQL](https://github.com/Jovancoding/Network-AI/actions/workflows/codeql.yml/badge.svg)](https://github.com/Jovancoding/Network-AI/actions/workflows/codeql.yml)
8
- [![Release](https://img.shields.io/badge/release-v5.3.2-blue.svg)](https://github.com/Jovancoding/Network-AI/releases)
8
+ [![Release](https://img.shields.io/badge/release-v5.4.1-blue.svg)](https://github.com/Jovancoding/Network-AI/releases)
9
9
  [![npm](https://img.shields.io/npm/dw/network-ai.svg?label=npm%20downloads)](https://www.npmjs.com/package/network-ai)
10
- [![Tests](https://img.shields.io/badge/tests-2899%20passing-brightgreen.svg)](#testing)
10
+ [![Tests](https://img.shields.io/badge/tests-2976%20passing-brightgreen.svg)](#testing)
11
11
  [![Adapters](https://img.shields.io/badge/frameworks-29%20supported-blueviolet.svg)](#adapter-system)
12
12
  [![License](https://img.shields.io/badge/license-MIT-brightgreen.svg)](LICENSE)
13
13
  [![Socket](https://socket.dev/api/badge/npm/package/network-ai)](https://socket.dev/npm/package/network-ai/overview)
@@ -354,7 +354,7 @@ npx ts-node examples/10-nemoclaw-sandbox-swarm.ts
354
354
 
355
355
  ## Adapter System
356
356
 
357
- 28 adapters, zero adapter dependencies. You bring your own SDK objects.
357
+ 29 adapters, zero adapter dependencies. You bring your own SDK objects.
358
358
 
359
359
  | Adapter | Framework / Protocol | Register method |
360
360
  |---|---|---|
@@ -403,7 +403,7 @@ Extend `BaseAdapter` (or `StreamingBaseAdapter` for streaming) to add your own i
403
403
 
404
404
  | Capability | Network-AI | LangGraph | CrewAI | AutoGen |
405
405
  |---|---|---|---|---|
406
- | Cross-framework agents in one swarm | ✅ 28 built-in adapters | ⚠️ Nodes can call any code; no adapter abstraction | ⚠️ Extensible via tools; CrewAI-native agents only | ⚠️ Extensible via plugins; AutoGen-native agents only |
406
+ | Cross-framework agents in one swarm | ✅ 29 built-in adapters | ⚠️ Nodes can call any code; no adapter abstraction | ⚠️ Extensible via tools; CrewAI-native agents only | ⚠️ Extensible via plugins; AutoGen-native agents only |
407
407
  | Atomic shared state (conflict-safe) | ✅ `propose → validate → commit` mutex | ⚠️ State passed between nodes; last-write-wins | ⚠️ Shared memory available; no conflict resolution | ⚠️ Shared context available; no conflict resolution |
408
408
  | Hard token ceiling per agent | ✅ `FederatedBudget` (first-class API) | ⚠️ Via callbacks / custom middleware | ⚠️ Via callbacks / custom middleware | ⚠️ Built-in token tracking in v0.4+; no swarm-level ceiling |
409
409
  | Permission gating before sensitive ops | ✅ `AuthGuardian` (built-in) | ⚠️ Possible via custom node logic | ⚠️ Possible via custom tools | ⚠️ Possible via custom middleware |
@@ -419,7 +419,7 @@ Extend `BaseAdapter` (or `StreamingBaseAdapter` for streaming) to add your own i
419
419
  npm run test:all # All suites in sequence
420
420
  npm test # Core orchestrator
421
421
  npm run test:security # Security module
422
- npm run test:adapters # All 28 adapters
422
+ npm run test:adapters # All 29 adapters
423
423
  npm run test:streaming # Streaming adapters
424
424
  npm run test:a2a # A2A protocol adapter
425
425
  npm run test:codex # Codex adapter
@@ -429,7 +429,7 @@ npm run test:phase9 # Agent runtime, console, strategy agent
429
429
  npm run test:phase12 # Context Throttler, Partition Planner, Coverage Gate, Route Classifier
430
430
  ```
431
431
 
432
- **2,899 passing assertions across 28 test suites** (`npm run test:all`):
432
+ **2,976 passing assertions across 29 test suites** (`npm run test:all`):
433
433
 
434
434
  | Suite | Assertions | Covers |
435
435
  |---|---|---|
@@ -437,7 +437,7 @@ npm run test:phase12 # Context Throttler, Partition Planner, Coverage Gate,
437
437
  | `test-phase5f.ts` | 127 | SSE transport, `McpCombinedBridge`, extended MCP tools |
438
438
  | `test-phase5g.ts` | 121 | CRDT backend, vector clocks, bidirectional sync |
439
439
  | `test-phase6.ts` | 121 | MCP server, control-plane tools, audit tools |
440
- | `test-adapters.ts` | 218 | All 28 adapters, registry routing, integration, edge cases |
440
+ | `test-adapters.ts` | 218 | All 29 adapters, registry routing, integration, edge cases |
441
441
  | `test-phase5d.ts` | 117 | Pluggable backend (Redis, CRDT, Memory) |
442
442
  | `test-standalone.ts` | 88 | Blackboard, auth, integration, persistence, parallelisation, quality gate |
443
443
  | `test-phase5e.ts` | 87 | Federated budget tracking |
@@ -460,6 +460,7 @@ npm run test:phase12 # Context Throttler, Partition Planner, Coverage Gate,
460
460
  | `test-topology.ts` | 304 | WorkTree, ControlPlane, dashboard server, topology visualization, WebSocket protocol |
461
461
  | `test-rlm-phases.ts` | 123 | FederatedBudget child spending, blackboard metadata API, best-partial result, HookContext depth, sub-goal recursion, semaphore fan-out, PhasePipeline compaction, RLMAdapter end-to-end |
462
462
  | `test-phase12.ts` | 65 | Context Throttler, Partition Planner, Coverage Gate, Route Classifier, EVALUATING FSM state, runTeam integration |
463
+ | `test-env-manager.ts` | 77 | Multi-environment isolation, promotion chain, backup/restore, source protection, NETWORK_AI_ENV, blackboard env routing |
463
464
  | `test.ts` | 39 | Core orchestrator smoke tests |
464
465
 
465
466
  ---
@@ -476,7 +477,7 @@ npm run test:phase12 # Context Throttler, Partition Planner, Coverage Gate,
476
477
  | [AUDIT_LOG_SCHEMA.md](AUDIT_LOG_SCHEMA.md) | Audit log field reference, all event types, scoring formula |
477
478
  | [ADOPTERS.md](ADOPTERS.md) | Known adopters — open a PR to add yourself |
478
479
  | [INTEGRATION_GUIDE.md](INTEGRATION_GUIDE.md) | End-to-end integration walkthrough with v5.0 modules |
479
- | [references/adapter-system.md](references/adapter-system.md) | Adapter architecture, all 28 adapters, writing custom adapters |
480
+ | [references/adapter-system.md](references/adapter-system.md) | Adapter architecture, all 29 adapters, writing custom adapters |
480
481
  | [references/auth-guardian.md](references/auth-guardian.md) | Permission scoring, resource types, IAuthValidator interface |
481
482
  | [references/trust-levels.md](references/trust-levels.md) | Trust level configuration, APS delegation-chain mapping |
482
483
 
package/SKILL.md CHANGED
@@ -60,6 +60,14 @@ pip install filelock # only needed if you see locking issues on Windows
60
60
 
61
61
  The `data/` directory is created automatically on first run. No configuration files, environment variables, or credentials are required.
62
62
 
63
+ > **Multi-environment support (v5.4.0):** All five Python scripts now read the `NETWORK_AI_ENV` environment variable at startup and accept a `--env <name>` CLI argument. When set, all data paths are routed to `data/<env>/` instead of the root `data/` directory. Use this to isolate dev, staging, and production state.
64
+ >
65
+ > ```bash
66
+ > # Run against the dev environment
67
+ > NETWORK_AI_ENV=dev python3 scripts/blackboard.py list
68
+ > python3 scripts/check_permission.py --active-grants --env dev
69
+ > ```
70
+
63
71
  Multi-agent coordination system for complex workflows requiring task delegation, parallel execution, and permission-controlled access to sensitive APIs.
64
72
 
65
73
  ## 🎯 Orchestrator System Instructions
package/bin/cli.ts CHANGED
@@ -16,6 +16,7 @@ import * as readline from 'readline';
16
16
  import { LockedBlackboard } from '../lib/locked-blackboard';
17
17
  import { AuthGuardian } from '../index';
18
18
  import { FederatedBudget } from '../lib/federated-budget';
19
+ import { EnvironmentManager } from '../lib/env-manager';
19
20
 
20
21
  // eslint-disable-next-line @typescript-eslint/no-var-requires
21
22
  const pkg = (() => {
@@ -59,6 +60,7 @@ program
59
60
  .version(pkg.version, '-v, --version')
60
61
  .enablePositionalOptions()
61
62
  .addOption(new Option('--data <path>', 'path to data directory').default('./data'))
63
+ .addOption(new Option('--env <name>', 'target environment (dev|st|sit|qa|sandbox|preprod|prod)'))
62
64
  .addOption(new Option('--json', 'output raw JSON (useful for piping)'));
63
65
 
64
66
  // ── bb (blackboard) ───────────────────────────────────────────────────────────
@@ -302,6 +304,153 @@ auditCmd.command('clear')
302
304
  print(g.json ? { cleared: logFile } : `✓ cleared ${logFile}`, g.json);
303
305
  });
304
306
 
307
+ // ── env (environment management) ──────────────────────────────────────────────
308
+
309
+ const envCmd = program.command('env').description('Multi-environment management (isolation, promotion, backup)');
310
+
311
+ envCmd.command('init')
312
+ .description('Scaffold an environment data directory (all 7 envs if no --env given)')
313
+ .action((_opts: Record<string, unknown>, cmd: Command) => {
314
+ const g = cmd.optsWithGlobals<{ data: string; env?: string; json: boolean }>();
315
+ const mgr = new EnvironmentManager(resolveData(g));
316
+ if (g.env) {
317
+ mgr.init(g.env);
318
+ print(g.json ? { initialized: g.env } : `✓ initialized env '${g.env}'`, g.json);
319
+ } else {
320
+ mgr.initAll();
321
+ print(g.json ? { initialized: mgr.getChain().concat(['sandbox']) } : `✓ all environments initialized`, g.json);
322
+ }
323
+ });
324
+
325
+ envCmd.command('list')
326
+ .description('List all environments with existence and key count')
327
+ .action((_opts: Record<string, unknown>, cmd: Command) => {
328
+ const g = cmd.optsWithGlobals<{ data: string; env?: string; json: boolean }>();
329
+ const mgr = new EnvironmentManager(resolveData(g));
330
+ const envs = mgr.list();
331
+ if (g.json) {
332
+ print(envs, true);
333
+ } else {
334
+ const lines = envs.map(e => `${e.name.padEnd(10)} ${e.exists ? '✓' : '✗'} (${e.keyCount} keys)`);
335
+ console.log(lines.join('\n'));
336
+ }
337
+ });
338
+
339
+ envCmd.command('chain')
340
+ .description('Show the configured promotion chain')
341
+ .action((_opts: Record<string, unknown>, cmd: Command) => {
342
+ const g = cmd.optsWithGlobals<{ data: string; json: boolean }>();
343
+ const mgr = new EnvironmentManager(resolveData(g));
344
+ const chain = mgr.getChain();
345
+ print(g.json ? chain : chain.join(' → '), g.json);
346
+ });
347
+
348
+ envCmd.command('diff')
349
+ .description('Compare config artefacts between two environments')
350
+ .requiredOption('--from <env>', 'source environment')
351
+ .requiredOption('--to <env>', 'target environment')
352
+ .action((opts: { from: string; to: string }, cmd: Command) => {
353
+ const g = cmd.optsWithGlobals<{ data: string; json: boolean }>();
354
+ const mgr = new EnvironmentManager(resolveData(g));
355
+ const result = mgr.diff(opts.from, opts.to);
356
+ if (g.json) {
357
+ print(result, true);
358
+ } else if (result.differences.length === 0) {
359
+ console.log(`No differences between '${opts.from}' and '${opts.to}'`);
360
+ } else {
361
+ for (const d of result.differences) {
362
+ const sym = d.status === 'added' ? '+' : d.status === 'removed' ? '-' : '~';
363
+ console.log(` ${sym} ${d.file} (${d.status})`);
364
+ }
365
+ }
366
+ });
367
+
368
+ envCmd.command('promote')
369
+ .description('Promote config artefacts one step up the chain')
370
+ .requiredOption('--from <env>', 'source environment')
371
+ .requiredOption('--to <env>', 'target environment')
372
+ .option('--confirmed-by <name>', 'required for preprod gate')
373
+ .option('--approved-by <name>', 'required for prod gate')
374
+ .action((opts: { from: string; to: string; confirmedBy?: string; approvedBy?: string }, cmd: Command) => {
375
+ const g = cmd.optsWithGlobals<{ data: string; json: boolean }>();
376
+ const mgr = new EnvironmentManager(resolveData(g));
377
+ try {
378
+ const result = mgr.promote(opts.from, opts.to, {
379
+ confirmedBy: opts.confirmedBy,
380
+ approvedBy: opts.approvedBy,
381
+ });
382
+ print(g.json ? result : `✓ promoted ${opts.from} → ${opts.to} (${result.configsCopied.length} configs copied)`, g.json);
383
+ } catch (err) {
384
+ die(err instanceof Error ? err.message : String(err));
385
+ }
386
+ });
387
+
388
+ // ── env backup subcommand group ───────────────────────────────────────────────
389
+
390
+ const envBackup = envCmd.command('backup').description('Backup management for an environment');
391
+
392
+ envBackup.command('create')
393
+ .description('Create a backup of the environment data directory (alias: network-ai env backup)')
394
+ .action((_opts: Record<string, unknown>, cmd: Command) => {
395
+ const g = cmd.optsWithGlobals<{ data: string; env?: string; json: boolean }>();
396
+ if (!g.env) die('--env <name> is required for backup create');
397
+ const mgr = new EnvironmentManager(resolveData(g));
398
+ const result = mgr.backup(g.env);
399
+ print(g.json ? result : `✓ backup created: ${result.backupId} (${result.filesCount} files)`, g.json);
400
+ });
401
+
402
+ envBackup.command('list')
403
+ .description('List available backups for an environment')
404
+ .action((_opts: Record<string, unknown>, cmd: Command) => {
405
+ const g = cmd.optsWithGlobals<{ data: string; env?: string; json: boolean }>();
406
+ if (!g.env) die('--env <name> is required for backup list');
407
+ const mgr = new EnvironmentManager(resolveData(g));
408
+ const backups = mgr.listBackups(g.env);
409
+ if (g.json) {
410
+ print(backups, true);
411
+ } else if (backups.length === 0) {
412
+ console.log('(no backups)');
413
+ } else {
414
+ for (const b of backups) {
415
+ console.log(` ${b.backupId} ${b.timestamp} ${(b.sizeBytes / 1024).toFixed(1)} KB`);
416
+ }
417
+ }
418
+ });
419
+
420
+ envBackup.command('restore')
421
+ .description('Restore an environment from a backup')
422
+ .option('--backup <id>', 'backup ID to restore')
423
+ .option('--latest', 'restore the most recent backup')
424
+ .action((opts: { backup?: string; latest?: boolean }, cmd: Command) => {
425
+ const g = cmd.optsWithGlobals<{ data: string; env?: string; json: boolean }>();
426
+ if (!g.env) die('--env <name> is required for backup restore');
427
+ if (!opts.backup && !opts.latest) die('provide --backup <id> or --latest');
428
+ const mgr = new EnvironmentManager(resolveData(g));
429
+ let backupId = opts.backup;
430
+ if (opts.latest) {
431
+ const backups = mgr.listBackups(g.env);
432
+ if (backups.length === 0) die(`no backups found for env '${g.env}'`);
433
+ backupId = backups[0].backupId;
434
+ }
435
+ try {
436
+ const result = mgr.restore(g.env, backupId!);
437
+ print(g.json ? result : `✓ restored ${result.filesRestored} files from backup '${result.backupId}'`, g.json);
438
+ } catch (err) {
439
+ die(err instanceof Error ? err.message : String(err));
440
+ }
441
+ });
442
+
443
+ envBackup.command('prune')
444
+ .description('Remove old backups, keeping the N most recent')
445
+ .requiredOption('--keep <n>', 'number of backups to retain', (v) => parseInt(v, 10))
446
+ .action((opts: { keep: number }, cmd: Command) => {
447
+ const g = cmd.optsWithGlobals<{ data: string; env?: string; json: boolean }>();
448
+ if (!g.env) die('--env <name> is required for backup prune');
449
+ const mgr = new EnvironmentManager(resolveData(g));
450
+ const deleted = mgr.pruneBackups(g.env, opts.keep);
451
+ print(g.json ? { deleted } : `✓ pruned ${deleted} backup(s), keeping ${opts.keep}`, g.json);
452
+ });
453
+
305
454
  // ── parse ─────────────────────────────────────────────────────────────────────
306
455
 
307
456
  // Auto-detect MCP stdio mode: when stdin is piped (not a TTY) and no
package/dist/bin/cli.js CHANGED
@@ -49,6 +49,7 @@ const readline = __importStar(require("readline"));
49
49
  const locked_blackboard_1 = require("../lib/locked-blackboard");
50
50
  const index_1 = require("../index");
51
51
  const federated_budget_1 = require("../lib/federated-budget");
52
+ const env_manager_1 = require("../lib/env-manager");
52
53
  // eslint-disable-next-line @typescript-eslint/no-var-requires
53
54
  const pkg = (() => {
54
55
  try {
@@ -93,6 +94,7 @@ program
93
94
  .version(pkg.version, '-v, --version')
94
95
  .enablePositionalOptions()
95
96
  .addOption(new commander_1.Option('--data <path>', 'path to data directory').default('./data'))
97
+ .addOption(new commander_1.Option('--env <name>', 'target environment (dev|st|sit|qa|sandbox|preprod|prod)'))
96
98
  .addOption(new commander_1.Option('--json', 'output raw JSON (useful for piping)'));
97
99
  // ── bb (blackboard) ───────────────────────────────────────────────────────────
98
100
  const bb = program.command('bb').description('Blackboard operations');
@@ -333,6 +335,154 @@ auditCmd.command('clear')
333
335
  fs.writeFileSync(logFile, '');
334
336
  print(g.json ? { cleared: logFile } : `✓ cleared ${logFile}`, g.json);
335
337
  });
338
+ // ── env (environment management) ──────────────────────────────────────────────
339
+ const envCmd = program.command('env').description('Multi-environment management (isolation, promotion, backup)');
340
+ envCmd.command('init')
341
+ .description('Scaffold an environment data directory (all 7 envs if no --env given)')
342
+ .action((_opts, cmd) => {
343
+ const g = cmd.optsWithGlobals();
344
+ const mgr = new env_manager_1.EnvironmentManager(resolveData(g));
345
+ if (g.env) {
346
+ mgr.init(g.env);
347
+ print(g.json ? { initialized: g.env } : `✓ initialized env '${g.env}'`, g.json);
348
+ }
349
+ else {
350
+ mgr.initAll();
351
+ print(g.json ? { initialized: mgr.getChain().concat(['sandbox']) } : `✓ all environments initialized`, g.json);
352
+ }
353
+ });
354
+ envCmd.command('list')
355
+ .description('List all environments with existence and key count')
356
+ .action((_opts, cmd) => {
357
+ const g = cmd.optsWithGlobals();
358
+ const mgr = new env_manager_1.EnvironmentManager(resolveData(g));
359
+ const envs = mgr.list();
360
+ if (g.json) {
361
+ print(envs, true);
362
+ }
363
+ else {
364
+ const lines = envs.map(e => `${e.name.padEnd(10)} ${e.exists ? '✓' : '✗'} (${e.keyCount} keys)`);
365
+ console.log(lines.join('\n'));
366
+ }
367
+ });
368
+ envCmd.command('chain')
369
+ .description('Show the configured promotion chain')
370
+ .action((_opts, cmd) => {
371
+ const g = cmd.optsWithGlobals();
372
+ const mgr = new env_manager_1.EnvironmentManager(resolveData(g));
373
+ const chain = mgr.getChain();
374
+ print(g.json ? chain : chain.join(' → '), g.json);
375
+ });
376
+ envCmd.command('diff')
377
+ .description('Compare config artefacts between two environments')
378
+ .requiredOption('--from <env>', 'source environment')
379
+ .requiredOption('--to <env>', 'target environment')
380
+ .action((opts, cmd) => {
381
+ const g = cmd.optsWithGlobals();
382
+ const mgr = new env_manager_1.EnvironmentManager(resolveData(g));
383
+ const result = mgr.diff(opts.from, opts.to);
384
+ if (g.json) {
385
+ print(result, true);
386
+ }
387
+ else if (result.differences.length === 0) {
388
+ console.log(`No differences between '${opts.from}' and '${opts.to}'`);
389
+ }
390
+ else {
391
+ for (const d of result.differences) {
392
+ const sym = d.status === 'added' ? '+' : d.status === 'removed' ? '-' : '~';
393
+ console.log(` ${sym} ${d.file} (${d.status})`);
394
+ }
395
+ }
396
+ });
397
+ envCmd.command('promote')
398
+ .description('Promote config artefacts one step up the chain')
399
+ .requiredOption('--from <env>', 'source environment')
400
+ .requiredOption('--to <env>', 'target environment')
401
+ .option('--confirmed-by <name>', 'required for preprod gate')
402
+ .option('--approved-by <name>', 'required for prod gate')
403
+ .action((opts, cmd) => {
404
+ const g = cmd.optsWithGlobals();
405
+ const mgr = new env_manager_1.EnvironmentManager(resolveData(g));
406
+ try {
407
+ const result = mgr.promote(opts.from, opts.to, {
408
+ confirmedBy: opts.confirmedBy,
409
+ approvedBy: opts.approvedBy,
410
+ });
411
+ print(g.json ? result : `✓ promoted ${opts.from} → ${opts.to} (${result.configsCopied.length} configs copied)`, g.json);
412
+ }
413
+ catch (err) {
414
+ die(err instanceof Error ? err.message : String(err));
415
+ }
416
+ });
417
+ // ── env backup subcommand group ───────────────────────────────────────────────
418
+ const envBackup = envCmd.command('backup').description('Backup management for an environment');
419
+ envBackup.command('create')
420
+ .description('Create a backup of the environment data directory (alias: network-ai env backup)')
421
+ .action((_opts, cmd) => {
422
+ const g = cmd.optsWithGlobals();
423
+ if (!g.env)
424
+ die('--env <name> is required for backup create');
425
+ const mgr = new env_manager_1.EnvironmentManager(resolveData(g));
426
+ const result = mgr.backup(g.env);
427
+ print(g.json ? result : `✓ backup created: ${result.backupId} (${result.filesCount} files)`, g.json);
428
+ });
429
+ envBackup.command('list')
430
+ .description('List available backups for an environment')
431
+ .action((_opts, cmd) => {
432
+ const g = cmd.optsWithGlobals();
433
+ if (!g.env)
434
+ die('--env <name> is required for backup list');
435
+ const mgr = new env_manager_1.EnvironmentManager(resolveData(g));
436
+ const backups = mgr.listBackups(g.env);
437
+ if (g.json) {
438
+ print(backups, true);
439
+ }
440
+ else if (backups.length === 0) {
441
+ console.log('(no backups)');
442
+ }
443
+ else {
444
+ for (const b of backups) {
445
+ console.log(` ${b.backupId} ${b.timestamp} ${(b.sizeBytes / 1024).toFixed(1)} KB`);
446
+ }
447
+ }
448
+ });
449
+ envBackup.command('restore')
450
+ .description('Restore an environment from a backup')
451
+ .option('--backup <id>', 'backup ID to restore')
452
+ .option('--latest', 'restore the most recent backup')
453
+ .action((opts, cmd) => {
454
+ const g = cmd.optsWithGlobals();
455
+ if (!g.env)
456
+ die('--env <name> is required for backup restore');
457
+ if (!opts.backup && !opts.latest)
458
+ die('provide --backup <id> or --latest');
459
+ const mgr = new env_manager_1.EnvironmentManager(resolveData(g));
460
+ let backupId = opts.backup;
461
+ if (opts.latest) {
462
+ const backups = mgr.listBackups(g.env);
463
+ if (backups.length === 0)
464
+ die(`no backups found for env '${g.env}'`);
465
+ backupId = backups[0].backupId;
466
+ }
467
+ try {
468
+ const result = mgr.restore(g.env, backupId);
469
+ print(g.json ? result : `✓ restored ${result.filesRestored} files from backup '${result.backupId}'`, g.json);
470
+ }
471
+ catch (err) {
472
+ die(err instanceof Error ? err.message : String(err));
473
+ }
474
+ });
475
+ envBackup.command('prune')
476
+ .description('Remove old backups, keeping the N most recent')
477
+ .requiredOption('--keep <n>', 'number of backups to retain', (v) => parseInt(v, 10))
478
+ .action((opts, cmd) => {
479
+ const g = cmd.optsWithGlobals();
480
+ if (!g.env)
481
+ die('--env <name> is required for backup prune');
482
+ const mgr = new env_manager_1.EnvironmentManager(resolveData(g));
483
+ const deleted = mgr.pruneBackups(g.env, opts.keep);
484
+ print(g.json ? { deleted } : `✓ pruned ${deleted} backup(s), keeping ${opts.keep}`, g.json);
485
+ });
336
486
  // ── parse ─────────────────────────────────────────────────────────────────────
337
487
  // Auto-detect MCP stdio mode: when stdin is piped (not a TTY) and no
338
488
  // subcommand was given, start the MCP server in stdio transport mode.