@aikdna/kdna-cli 0.14.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,24 +4,24 @@
4
4
 
5
5
  KDNA CLI 是 AI Agent 加载、验证、组合、测试和治理领域判断的运行控制平面。
6
6
 
7
- CLI 不是 Studio,不是 Chat,不是 Governance Console。它是这些产品共同依赖的底层协议接口。
8
-
9
7
  Part of the [KDNA](https://github.com/knowledge-dna/KDNA) ecosystem.
10
8
 
11
9
  ## Install
12
10
 
13
11
  ```bash
14
12
  npm install -g @aikdna/kdna-cli
13
+ kdna setup
15
14
  ```
16
15
 
17
- ## Quick Start
16
+ ## Quick Start (5 minutes)
18
17
 
19
18
  ```bash
20
- kdna install @aikdna/writing # Install a domain
21
- kdna verify @aikdna/writing # 3-layer verification
22
- kdna available # List installed domains
23
- kdna match "improve this post" # Find relevant domains
24
- kdna load @aikdna/writing # Load for agent consumption
19
+ npm install -g @aikdna/kdna-cli
20
+ kdna setup
21
+ kdna install @aikdna/writing
22
+ kdna verify @aikdna/writing --judgment
23
+ kdna compare @aikdna/writing --input "help me improve this post"
24
+ kdna doctor --agents
25
25
  ```
26
26
 
27
27
  ## Commands by Role
@@ -34,7 +34,8 @@ kdna load @aikdna/writing # Load for agent consumption
34
34
  | `kdna validate <path>` | Validate domain structure |
35
35
  | `kdna validate --schema <path>` | Schema-only validation |
36
36
  | `kdna pack <path>` | Pack into .kdna container |
37
- | `kdna unpack <file>` | Unpack .kdna container |
37
+ | `kdna pack <path> --encrypt --license <file>` | Pack encrypted .kdnae container |
38
+ | `kdna unpack <file>` | Unpack .kdna or .kdnae container |
38
39
  | `kdna inspect <path>` | Inspect domain or .kdna file |
39
40
  | `kdna publish <path>` | Pack + sign + publish to registry |
40
41
  | `kdna publish --check <path>` | Quality gate check only |
@@ -47,15 +48,42 @@ kdna load @aikdna/writing # Load for agent consumption
47
48
  | `kdna available [--json]` | List installed domains with v2.1 fields |
48
49
  | `kdna match "<task>" [--json]` | Signal matching — find relevant domains |
49
50
  | `kdna load <name> [--as=prompt\|json\|raw]` | Emit domain in agent-ready format |
51
+ | `kdna postvalidate <name> --output <file>` | Post-generation judgment check |
50
52
 
51
53
  ### Testing & Verification
52
54
 
53
55
  | Command | Description |
54
56
  |---------|-------------|
55
- | `kdna verify <name>` | 3-layer verification: structure + trust + judgment |
57
+ | `kdna verify <name>` | 3-layer: structure + trust + judgment |
56
58
  | `kdna compare <name> --input "..."` | With/without KDNA reasoning diff |
59
+ | `kdna compare <name> --input "..." --report-md` | Markdown report with scoring |
60
+ | `kdna compare <name> --input "..." --report-json` | JSON report with scoring |
57
61
  | `kdna diff <name>@<v1> <name>@<v2>` | Judgment-level diff between versions |
58
- | `kdna doctor` | Check runtime environment health |
62
+
63
+ ### Diagnostics & Trace
64
+
65
+ | Command | Description |
66
+ |---------|-------------|
67
+ | `kdna doctor` | System health check |
68
+ | `kdna doctor --agents` | Agent integration check (Codex/Claude/OpenCode/Cursor/Gemini) |
69
+ | `kdna doctor --json` | Machine-readable health report |
70
+ | `kdna trace` | View recent load/postvalidate traces |
71
+ | `kdna trace --json` | Machine-readable trace output |
72
+ | `kdna trace --export <file>` | Export traces for audit |
73
+ | `kdna trace --since 7d\|30d\|90d` | Filter by time range |
74
+ | `kdna history` | Recent domain usage (last 20) |
75
+ | `kdna history --stats` | Aggregate by domain and agent |
76
+ | `kdna history --domain <name>` | Filter by domain |
77
+
78
+ ### License & Authorization
79
+
80
+ | Command | Description |
81
+ |---------|-------------|
82
+ | `kdna license generate <domain> --to <email>` | Generate signed license |
83
+ | `kdna license install <license.json>` | Register license for auto-decrypt |
84
+ | `kdna license verify <license.json>` | Verify license signature and validity |
85
+ | `kdna license bind <license.json>` | Bind license to this machine |
86
+ | `kdna license show <license.json>` | Display license details |
59
87
 
60
88
  ### Cluster Composition
61
89
 
@@ -68,6 +96,8 @@ kdna load @aikdna/writing # Load for agent consumption
68
96
  | Command | Description |
69
97
  |---------|-------------|
70
98
  | `kdna install <name>` | Install domain from registry |
99
+ | `kdna install ./file.kdna` | Install from local .kdna file |
100
+ | `kdna install ./file.kdnae` | Install from encrypted .kdnae (auto-decrypt with license) |
71
101
  | `kdna remove <name>` | Uninstall a domain |
72
102
  | `kdna update <name>` | Update installed domain |
73
103
  | `kdna info <name>` | Show domain metadata and trust status |
@@ -90,6 +120,14 @@ kdna load @aikdna/writing # Load for agent consumption
90
120
  |---------|-------------|
91
121
  | `kdna setup` | One-command setup: CLI + skill + data root |
92
122
 
123
+ ### Environment Variables
124
+
125
+ | Variable | Purpose |
126
+ |----------|---------|
127
+ | `KDNA_AGENT` | Override agent name in trace logs (e.g. `claude_code`, `codex`, `opencode`) |
128
+ | `KDNA_REGISTRY_URL` | Override canonical registry URL |
129
+ | `KDNA_IDENTITY_DIR` | Override identity key directory |
130
+
93
131
  ## Exit Codes
94
132
 
95
133
  | Code | Name | Meaning |
@@ -111,10 +149,10 @@ Machine-consumable commands support `--json` for structured output:
111
149
  ```bash
112
150
  kdna verify @aikdna/writing --json
113
151
  kdna available --json
114
- kdna match "help me write" --json
115
- kdna search writing --json
116
- kdna info @aikdna/writing --json
117
- kdna doctor --json
152
+ kdna doctor --agents --json
153
+ kdna trace --json
154
+ kdna history --json
155
+ kdna license verify --json <file>
118
156
  ```
119
157
 
120
158
  ## Product Matrix
@@ -123,14 +161,12 @@ kdna doctor --json
123
161
  |-------|---------|---------------|
124
162
  | Protocol | KDNA SPEC | Define judgment asset format |
125
163
  | Core Library | @aikdna/kdna-core | load / validate / compose / render |
126
- | Runtime | @aikdna/kdna-cli | Agent runtime + compile + verify + test + publish |
164
+ | Runtime | @aikdna/kdna-cli | Agent runtime + compile + verify + test + publish + license |
127
165
  | Authoring | KDNA Studio | Human-led judgment production |
128
166
  | Consumption | KDNAChat | Load, use, compare |
129
167
  | Governance | KDNA Governance Console | Approve, release, audit |
130
168
  | Distribution | Registry | Discover, install, trade |
131
169
 
132
- CLI 不应该成为一个"命令行 Studio",而是所有 KDNA 产品共同依赖的协议控制平面。
133
-
134
170
  ## Development
135
171
 
136
172
  ```bash
@@ -144,6 +180,7 @@ npm test
144
180
 
145
181
  - [@aikdna/kdna-core](https://github.com/knowledge-dna/KDNA/tree/main/packages/kdna-core) — Pure logic library
146
182
  - [KDNA Registry](https://github.com/knowledge-dna/kdna-registry) — Domain catalog
183
+ - [KDNA SPEC](https://github.com/knowledge-dna/KDNA) — Protocol specification
147
184
  - [aikdna.com](https://aikdna.com) — Website
148
185
 
149
186
  ## License
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aikdna/kdna-cli",
3
- "version": "0.14.0",
3
+ "version": "0.16.0",
4
4
  "description": "KDNA CLI — create, validate, install, and manage domain cognition packages for AI agents.",
5
5
  "type": "commonjs",
6
6
  "bin": {
package/src/agent.js CHANGED
@@ -34,6 +34,10 @@ const path = require('path');
34
34
  const { parseName } = require('./registry');
35
35
  const { recordTrace } = require('./cmds/trace');
36
36
 
37
+ function detectAgent() {
38
+ return process.env.KDNA_AGENT || 'cli';
39
+ }
40
+
37
41
  const USER_KDNA_DIR = path.join(process.env.HOME || process.env.USERPROFILE || '.', '.kdna');
38
42
  const INSTALL_DIR = path.join(USER_KDNA_DIR, 'domains');
39
43
 
@@ -367,7 +371,7 @@ function cmdLoad(input, args = []) {
367
371
  // JSON format
368
372
  if (format === 'json') {
369
373
  process.stdout.write(JSON.stringify({ manifest, core, patterns: pat }, null, 2) + '\n');
370
- recordTrace({ timestamp: new Date().toISOString(), agent: 'cli', domain: parsed.full, format: 'json' });
374
+ recordTrace({ timestamp: new Date().toISOString(), agent: detectAgent(), domain: parsed.full, format: 'json' });
371
375
  return;
372
376
  }
373
377
 
@@ -380,20 +384,20 @@ function cmdLoad(input, args = []) {
380
384
  process.stdout.write(fs.readFileSync(p, 'utf8'));
381
385
  }
382
386
  }
383
- recordTrace({ timestamp: new Date().toISOString(), agent: 'cli', domain: parsed.full, format: 'raw' });
387
+ recordTrace({ timestamp: new Date().toISOString(), agent: detectAgent(), domain: parsed.full, format: 'raw' });
384
388
  return;
385
389
  }
386
390
 
387
391
  // Load profiles
388
392
  if (profile) {
389
393
  emitProfile(parsed, manifest, core, pat, profile, profileInput);
390
- recordTrace({ timestamp: new Date().toISOString(), agent: 'cli', domain: parsed.full, format: `profile:${profile}` });
394
+ recordTrace({ timestamp: new Date().toISOString(), agent: detectAgent(), domain: parsed.full, format: `profile:${profile}` });
391
395
  return;
392
396
  }
393
397
 
394
398
  // Default: --as=prompt — compact text optimized for system-prompt injection.
395
399
  emitCompact(parsed, manifest, core, pat);
396
- recordTrace({ timestamp: new Date().toISOString(), agent: 'cli', domain: parsed.full, format: 'prompt' });
400
+ recordTrace({ timestamp: new Date().toISOString(), agent: detectAgent(), domain: parsed.full, format: 'prompt' });
397
401
  }
398
402
 
399
403
  // ─── Load profiles ─────────────────────────────────────────────────────
@@ -821,7 +825,7 @@ function cmdPostvalidate(args = []) {
821
825
  console.log(JSON.stringify(result, null, 2));
822
826
  recordTrace({
823
827
  timestamp: new Date().toISOString(),
824
- agent: 'cli',
828
+ agent: detectAgent(),
825
829
  domain: parsed.full,
826
830
  type: 'postvalidate',
827
831
  postvalidate: { result: results.violations.length ? 'fail' : 'pass', violations: results.violations.length, passed: results.passed.length },
@@ -851,7 +855,7 @@ function cmdPostvalidate(args = []) {
851
855
 
852
856
  recordTrace({
853
857
  timestamp: new Date().toISOString(),
854
- agent: 'cli',
858
+ agent: detectAgent(),
855
859
  domain: parsed.full,
856
860
  type: 'postvalidate',
857
861
  postvalidate: { result: results.violations.length ? 'fail' : 'pass', violations: results.violations.length, passed: results.passed.length },
package/src/cli.js CHANGED
@@ -24,7 +24,7 @@ const { cmdIdentity } = require('./cmds/identity');
24
24
  const { cmdSetup } = require('./cmds/setup');
25
25
  const { cmdDoctor } = require('./cmds/doctor');
26
26
  const { cmdTrace, cmdHistory } = require('./cmds/trace');
27
- const { cmdLicenseGenerate, cmdLicenseVerify, cmdLicenseBind, cmdLicenseShow } = require('./cmds/license');
27
+ const { cmdLicenseGenerate, cmdLicenseVerify, cmdLicenseBind, cmdLicenseShow, cmdLicenseInstall } = require('./cmds/license');
28
28
  const { cmdPreview, cmdProject, cmdEval, cmdExport, cmdDemo } = require('./cmds/legacy');
29
29
  const { cmdStudioScaffold, cmdCardsValidate, cmdLockVerify, cmdStudioCompile, cmdStudioReadiness } = require('./cmds/studio');
30
30
  const { cmdTestRun, cmdTestImport } = require('./cmds/test');
@@ -138,6 +138,7 @@ Trace & Diagnostics:
138
138
 
139
139
  License & Authorization:
140
140
  license generate <domain> --to <email> Generate signed license
141
+ license install <license.json> Register license for auto-decrypt
141
142
  license verify <license.json> Verify license signature
142
143
  license bind <license.json> Bind license to this machine
143
144
  license show <license.json> Display license details
@@ -436,10 +437,13 @@ switch (cmd) {
436
437
  cmdLicenseBind(rest);
437
438
  } else if (sub === 'show') {
438
439
  cmdLicenseShow(rest);
440
+ } else if (sub === 'install') {
441
+ cmdLicenseInstall(rest);
439
442
  } else {
440
443
  error(
441
444
  'Usage:\n' +
442
445
  ' kdna license generate <domain> --to <email> [--expires <date>]\n' +
446
+ ' kdna license install <license.json>\n' +
443
447
  ' kdna license verify <license.json>\n' +
444
448
  ' kdna license bind <license.json>\n' +
445
449
  ' kdna license show <license.json>',
@@ -81,7 +81,9 @@ function cmdLicenseGenerate(args) {
81
81
  }
82
82
 
83
83
  function cmdLicenseVerify(args) {
84
- const licensePath = args[0];
84
+ const jsonMode = args.includes('--json');
85
+ const filtered = args.filter(a => !a.startsWith('--'));
86
+ const licensePath = filtered[0];
85
87
  if (!licensePath) error('Usage: kdna license verify <license.json>', EXIT.INPUT_ERROR);
86
88
 
87
89
  let license;
@@ -96,8 +98,6 @@ function cmdLicenseVerify(args) {
96
98
  const fp = machineFingerprint();
97
99
  const result = verifyLicense(license, publicKey, fp);
98
100
 
99
- const jsonMode = args.includes('--json');
100
-
101
101
  if (jsonMode) {
102
102
  console.log(JSON.stringify({
103
103
  domain: license.domain,
@@ -135,7 +135,8 @@ function cmdLicenseVerify(args) {
135
135
  }
136
136
 
137
137
  function cmdLicenseBind(args) {
138
- const licensePath = args[0];
138
+ const filtered = args.filter(a => !a.startsWith('--'));
139
+ const licensePath = filtered[0];
139
140
  if (!licensePath) error('Usage: kdna license bind <license.json>', EXIT.INPUT_ERROR);
140
141
 
141
142
  let license;
@@ -165,9 +166,9 @@ function cmdLicenseBind(args) {
165
166
  }
166
167
 
167
168
  function cmdLicenseShow(args) {
168
- const licensePath = args[0];
169
+ const filtered = args.filter(a => !a.startsWith('--'));
170
+ const licensePath = filtered[0];
169
171
  if (!licensePath) {
170
- // Try to find license in current directory
171
172
  const local = path.join(process.cwd(), 'license.json');
172
173
  if (fs.existsSync(local)) return cmdLicenseVerify([local, ...args.slice(1)]);
173
174
  error('Usage: kdna license show <license.json>', EXIT.INPUT_ERROR);
@@ -175,4 +176,32 @@ function cmdLicenseShow(args) {
175
176
  cmdLicenseVerify(args);
176
177
  }
177
178
 
178
- module.exports = { cmdLicenseGenerate, cmdLicenseVerify, cmdLicenseBind, cmdLicenseShow };
179
+ function cmdLicenseInstall(args) {
180
+ const licensePath = args[0];
181
+ if (!licensePath) error('Usage: kdna license install <license.json>', EXIT.INPUT_ERROR);
182
+
183
+ let license;
184
+ try {
185
+ license = JSON.parse(fs.readFileSync(licensePath, 'utf8'));
186
+ } catch {
187
+ error(`Cannot read license file: ${licensePath}`, EXIT.INPUT_ERROR);
188
+ }
189
+
190
+ if (!license.domain) error('License missing domain field', EXIT.INPUT_ERROR);
191
+
192
+ const licenseDir = path.join(process.env.HOME || process.env.USERPROFILE || '.', '.kdna', 'licenses');
193
+ fs.mkdirSync(licenseDir, { recursive: true });
194
+
195
+ const safeName = license.domain.replace(/^@/, '').replace('/', '-');
196
+ const dest = path.join(licenseDir, `${safeName}.json`);
197
+
198
+ fs.writeFileSync(dest, JSON.stringify(license, null, 2) + '\n');
199
+
200
+ console.log(`License installed for ${license.domain}`);
201
+ console.log(` License ID: ${license.license_id || 'unknown'}`);
202
+ console.log(` Saved to: ${dest}`);
203
+ console.log('');
204
+ console.log(`Now install the domain: kdna install ${license.domain}`);
205
+ }
206
+
207
+ module.exports = { cmdLicenseGenerate, cmdLicenseVerify, cmdLicenseBind, cmdLicenseShow, cmdLicenseInstall };
package/src/install.js CHANGED
@@ -20,6 +20,7 @@ const crypto = require('crypto');
20
20
  const { execSync, execFileSync } = require('child_process');
21
21
  const { RegistryResolver, parseName } = require('./registry');
22
22
  const { EXIT, error } = require('./cmds/_common');
23
+ const { decrypt, deriveKey, machineFingerprint, isEncryptedContainer, ENCRYPTED_FILES } = require('./cmds/encrypt');
23
24
 
24
25
  const USER_KDNA_DIR = path.join(process.env.HOME || process.env.USERPROFILE || '.', '.kdna');
25
26
  const INSTALL_DIR = path.join(USER_KDNA_DIR, 'domains');
@@ -199,9 +200,9 @@ function warnLegacy() {
199
200
  // ─── Source parsing ─────────────────────────────────────────────────────
200
201
 
201
202
  function parseSource(input) {
202
- // Local file
203
+ // Local file (.kdna or .kdnae)
203
204
  if (
204
- input.endsWith('.kdna') &&
205
+ (input.endsWith('.kdna') || input.endsWith('.kdnae')) &&
205
206
  (input.startsWith('./') || input.startsWith('/') || input.startsWith('~/'))
206
207
  ) {
207
208
  const resolved = path.resolve(input.replace(/^~/, process.env.HOME || ''));
@@ -283,6 +284,53 @@ print('ok')
283
284
  }
284
285
  }
285
286
 
287
+ function extractAndDecrypt(kdnaPath, destDir, licenseKey) {
288
+ extractKdna(kdnaPath, destDir);
289
+ const fp = machineFingerprint();
290
+ const key = deriveKey(licenseKey, fp);
291
+
292
+ for (const f of fs.readdirSync(destDir)) {
293
+ if (ENCRYPTED_FILES.includes(f)) {
294
+ try {
295
+ const fullPath = path.join(destDir, f);
296
+ const encrypted = fs.readFileSync(fullPath);
297
+ const decrypted = decrypt(encrypted, key);
298
+ fs.writeFileSync(fullPath, decrypted);
299
+ } catch (err) {
300
+ fs.rmSync(destDir, { recursive: true, force: true });
301
+ error(`Failed to decrypt ${f}: ${err.message}. Wrong license key?`);
302
+ }
303
+ }
304
+ }
305
+ }
306
+
307
+ function findLicense(domainName) {
308
+ const licenseDir = path.join(USER_KDNA_DIR, 'licenses');
309
+ const licensePath = path.join(licenseDir, `${domainName.replace(/^@/, '').replace('/', '-')}.json`);
310
+ if (fs.existsSync(licensePath)) {
311
+ try {
312
+ return JSON.parse(fs.readFileSync(licensePath, 'utf8'));
313
+ } catch { /* invalid license */ }
314
+ }
315
+ return null;
316
+ }
317
+
318
+ function findLicenseForDomain(domainFull) {
319
+ const licenseDir = path.join(USER_KDNA_DIR, 'licenses');
320
+ if (!fs.existsSync(licenseDir)) return null;
321
+ // Try exact match: @aikdna/writing → @aikdna-writing.json
322
+ const candidates = [domainFull.replace(/^@/, '').replace('/', '-')];
323
+ // Also try just the domain name
324
+ candidates.push(domainFull.split('/').pop());
325
+ for (const c of candidates) {
326
+ const p = path.join(licenseDir, `${c}.json`);
327
+ if (fs.existsSync(p)) {
328
+ try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch { /* skip */ }
329
+ }
330
+ }
331
+ return null;
332
+ }
333
+
286
334
  // ─── Signature verification ────────────────────────────────────────────
287
335
 
288
336
  function verifySignature({ destDir, scope, entry, lenient = true }) {
@@ -613,9 +661,53 @@ function installFromLocalFile(filePath, _yes, jsonMode = false) {
613
661
  const abs = path.resolve(filePath);
614
662
  if (!fs.existsSync(abs) || !fs.statSync(abs).isFile()) error(`Not a file: ${abs}`);
615
663
 
664
+ const isEncrypted = isEncryptedContainer(abs);
616
665
  const tmpDir = path.join(INSTALL_DIR, '.local-tmp-' + Date.now());
617
666
  ensureDir(tmpDir);
618
- extractKdna(abs, tmpDir);
667
+
668
+ if (isEncrypted) {
669
+ // Find license for this .kdnae file
670
+ // First check the license directory, then ask for --license flag from args
671
+ const licenseFromArgs = process.argv.includes('--license')
672
+ ? process.argv[process.argv.indexOf('--license') + 1]
673
+ : null;
674
+ let licenseKey = null;
675
+
676
+ if (licenseFromArgs && fs.existsSync(licenseFromArgs)) {
677
+ try {
678
+ const lic = JSON.parse(fs.readFileSync(licenseFromArgs, 'utf8'));
679
+ licenseKey = lic.license_id;
680
+ } catch { /* invalid license file */ }
681
+ }
682
+
683
+ if (!licenseKey) {
684
+ // Try to auto-discover from ~/.kdna/licenses/
685
+ const manifest = readJson(path.join(tmpDir, 'kdna.json'));
686
+ // We need to extract just the manifest first to get the domain name
687
+ extractKdna(abs, tmpDir);
688
+ const mf = readJson(path.join(tmpDir, 'kdna.json'));
689
+ if (mf?.name) {
690
+ const lic = findLicenseForDomain(mf.name);
691
+ if (lic) licenseKey = lic.license_id;
692
+ }
693
+ if (!licenseKey) {
694
+ fs.rmSync(tmpDir, { recursive: true, force: true });
695
+ error(
696
+ `Cannot install encrypted .kdnae without a license.\n` +
697
+ `Save the license to ~/.kdna/licenses/ or use --license <file>.`,
698
+ EXIT.TRUST_FAILED,
699
+ );
700
+ }
701
+ // Re-extract for decryption
702
+ fs.rmSync(tmpDir, { recursive: true, force: true });
703
+ ensureDir(tmpDir);
704
+ }
705
+
706
+ console.log(' Decrypting .kdnae container...');
707
+ extractAndDecrypt(abs, tmpDir, licenseKey);
708
+ } else {
709
+ extractKdna(abs, tmpDir);
710
+ }
619
711
 
620
712
  const manifest = readJson(path.join(tmpDir, 'kdna.json'));
621
713
  const declared = manifest?.name;
@@ -636,6 +728,7 @@ function installFromLocalFile(filePath, _yes, jsonMode = false) {
636
728
  destManifest._source = {
637
729
  type: 'local-file',
638
730
  path: abs,
731
+ encrypted: isEncrypted,
639
732
  installed_at: new Date().toISOString(),
640
733
  };
641
734
  fs.writeFileSync(path.join(dest, 'kdna.json'), JSON.stringify(destManifest, null, 2) + '\n');
@@ -647,9 +740,10 @@ function installFromLocalFile(filePath, _yes, jsonMode = false) {
647
740
  path: dest,
648
741
  source: 'local-file',
649
742
  source_path: abs,
743
+ encrypted: isEncrypted,
650
744
  }));
651
745
  } else {
652
- console.log(`✓ Installed ${declared} from local file`);
746
+ console.log(`✓ Installed ${declared} from ${isEncrypted ? 'encrypted' : 'local'} file`);
653
747
  console.log(` Location: ${dest}`);
654
748
  }
655
749
  }