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.
- package/INTEGRATION_GUIDE.md +80 -3
- package/QUICKSTART.md +42 -1
- package/README.md +9 -8
- package/SKILL.md +8 -0
- package/bin/cli.ts +149 -0
- package/dist/bin/cli.js +150 -0
- package/dist/bin/cli.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -2
- package/dist/index.js.map +1 -1
- package/dist/lib/agent-runtime.d.ts +28 -0
- package/dist/lib/agent-runtime.d.ts.map +1 -1
- package/dist/lib/agent-runtime.js +68 -1
- package/dist/lib/agent-runtime.js.map +1 -1
- package/dist/lib/env-manager.d.ts +179 -0
- package/dist/lib/env-manager.d.ts.map +1 -0
- package/dist/lib/env-manager.js +500 -0
- package/dist/lib/env-manager.js.map +1 -0
- package/dist/lib/locked-blackboard.d.ts +6 -0
- package/dist/lib/locked-blackboard.d.ts.map +1 -1
- package/dist/lib/locked-blackboard.js +24 -5
- package/dist/lib/locked-blackboard.js.map +1 -1
- package/dist/lib/mcp-tool-consumer.js +1 -1
- package/package.json +1 -1
- package/scripts/blackboard.py +25 -4
- package/scripts/check_permission.py +28 -2
- package/scripts/context_manager.py +28 -2
- package/scripts/swarm_guard.py +25 -2
- package/scripts/validate_token.py +23 -1
package/INTEGRATION_GUIDE.md
CHANGED
|
@@ -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,
|
|
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
|
|
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
|
|
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
|
|
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
|
[](https://network-ai.org/)
|
|
6
6
|
[](https://github.com/Jovancoding/Network-AI/actions/workflows/ci.yml)
|
|
7
7
|
[](https://github.com/Jovancoding/Network-AI/actions/workflows/codeql.yml)
|
|
8
|
-
[](https://github.com/Jovancoding/Network-AI/releases)
|
|
9
9
|
[](https://www.npmjs.com/package/network-ai)
|
|
10
|
-
[](#testing)
|
|
11
11
|
[](#adapter-system)
|
|
12
12
|
[](LICENSE)
|
|
13
13
|
[](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
|
-
|
|
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 | ✅
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
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.
|