@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 +55 -18
- package/package.json +1 -1
- package/src/agent.js +10 -6
- package/src/cli.js +5 -1
- package/src/cmds/license.js +36 -7
- package/src/install.js +98 -4
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
|
-
|
|
21
|
-
kdna
|
|
22
|
-
kdna
|
|
23
|
-
kdna
|
|
24
|
-
kdna
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
115
|
-
kdna
|
|
116
|
-
kdna
|
|
117
|
-
kdna
|
|
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
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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>',
|
package/src/cmds/license.js
CHANGED
|
@@ -81,7 +81,9 @@ function cmdLicenseGenerate(args) {
|
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
function cmdLicenseVerify(args) {
|
|
84
|
-
const
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|