@aikdna/kdna-cli 0.17.0 → 0.19.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/src/registry.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * RegistryResolver — KDNA v0.7 registry client.
2
+ * RegistryResolver — KDNA asset-first registry client.
3
3
  *
4
4
  * Responsibilities:
5
5
  * 1. Resolve names: bare → @aikdna/bare, validate @scope/name format
@@ -7,7 +7,7 @@
7
7
  * 3. Cache registry metadata locally
8
8
  * 4. Surface scope trust info to install/publish
9
9
  *
10
- * Schema v2.0 — see kdna-registry/SCHEMA.md
10
+ * Schema v3.0 — see kdna-registry/SCHEMA.md
11
11
  */
12
12
 
13
13
  const fs = require('fs');
@@ -22,6 +22,7 @@ const DEFAULT_OFFICIAL_SCOPE = '@aikdna';
22
22
  const CANONICAL_REGISTRY_URL =
23
23
  process.env.KDNA_REGISTRY_URL ||
24
24
  'https://raw.githubusercontent.com/aikdna/kdna-registry/main/domains.json';
25
+ const REQUIRED_SCHEMA_VERSION = '3.0';
25
26
 
26
27
  const NAME_RE = /^@([a-z][a-z0-9-]*)\/([a-z][a-z0-9_]*)$/;
27
28
  const BARE_NAME_RE = /^[a-z][a-z0-9_]*$/;
@@ -39,6 +40,63 @@ function writeJson(file, data) {
39
40
  fs.writeFileSync(file, JSON.stringify(data, null, 2) + '\n');
40
41
  }
41
42
 
43
+ function parseDate(value) {
44
+ const date = value ? new Date(value) : null;
45
+ return date && !Number.isNaN(date.getTime()) ? date : null;
46
+ }
47
+
48
+ function registryTrustIssues(registry, { now = new Date() } = {}) {
49
+ const issues = [];
50
+ const trust = registry?.trust || {};
51
+
52
+ if (!registry || registry.schema_version !== REQUIRED_SCHEMA_VERSION) {
53
+ issues.push(
54
+ `Registry schema_version must be ${REQUIRED_SCHEMA_VERSION}, got ${JSON.stringify(registry?.schema_version)}`,
55
+ );
56
+ }
57
+
58
+ if (!trust.model) issues.push('registry.trust.model is required');
59
+ if (!trust.snapshot) issues.push('registry.trust.snapshot is required');
60
+ if (!trust.timestamp) issues.push('registry.trust.timestamp is required');
61
+
62
+ const snapshotVersion = trust.snapshot?.registry_version;
63
+ if (snapshotVersion && snapshotVersion !== registry.registry_version) {
64
+ issues.push(
65
+ `registry.trust.snapshot.registry_version ${snapshotVersion} does not match registry_version ${registry.registry_version}`,
66
+ );
67
+ }
68
+
69
+ const snapshotExpires = parseDate(trust.snapshot?.expires_at);
70
+ const timestampExpires = parseDate(trust.timestamp?.expires_at);
71
+ if (!snapshotExpires) issues.push('registry.trust.snapshot.expires_at must be an ISO timestamp');
72
+ if (!timestampExpires)
73
+ issues.push('registry.trust.timestamp.expires_at must be an ISO timestamp');
74
+ if (snapshotExpires && snapshotExpires <= now) {
75
+ issues.push(`registry snapshot expired at ${trust.snapshot.expires_at}`);
76
+ }
77
+ if (timestampExpires && timestampExpires <= now) {
78
+ issues.push(`registry timestamp expired at ${trust.timestamp.expires_at}`);
79
+ }
80
+
81
+ return issues;
82
+ }
83
+
84
+ function registryRevocations(registry) {
85
+ return registry?.trust?.revocations || [];
86
+ }
87
+
88
+ function isEntryRevoked(registry, entry) {
89
+ const revocations = registryRevocations(registry);
90
+ return (
91
+ revocations.find((rev) => {
92
+ if (rev.name && rev.name !== entry.name) return false;
93
+ if (rev.version && rev.version !== entry.version) return false;
94
+ if (rev.asset_digest && rev.asset_digest !== entry.asset_digest) return false;
95
+ return rev.name || rev.asset_digest;
96
+ }) || null
97
+ );
98
+ }
99
+
42
100
  // ─── Name parsing ───────────────────────────────────────────────────────
43
101
 
44
102
  /**
@@ -151,7 +209,17 @@ class RegistryResolver {
151
209
  _loadRegistryForScope(scopeName) {
152
210
  if (this._registries.has(scopeName)) return this._registries.get(scopeName);
153
211
  const source = this._sourceForScope(scopeName);
154
- const data = source.load({ allowNetwork: this.allowNetwork, refresh: this.refresh });
212
+ let data = source.load({ allowNetwork: this.allowNetwork, refresh: this.refresh });
213
+ let trustIssues = data ? registryTrustIssues(data) : [];
214
+ if (trustIssues.length && this.allowNetwork && !this.refresh) {
215
+ data = source.load({ allowNetwork: true, refresh: true });
216
+ trustIssues = data ? registryTrustIssues(data) : [];
217
+ }
218
+ if (trustIssues.length) {
219
+ throw new Error(
220
+ `Registry trust check failed:\n${trustIssues.map((i) => `- ${i}`).join('\n')}`,
221
+ );
222
+ }
155
223
  this._registries.set(scopeName, data);
156
224
  return data;
157
225
  }
@@ -186,12 +254,6 @@ class RegistryResolver {
186
254
  );
187
255
  }
188
256
 
189
- if (registry.schema_version && registry.schema_version !== '2.0') {
190
- throw new Error(
191
- `Registry schema_version ${registry.schema_version} not supported. This CLI requires 2.0.`,
192
- );
193
- }
194
-
195
257
  const scope = registry.scopes?.[parsed.scope];
196
258
  if (!scope) {
197
259
  throw new Error(`Scope ${parsed.scope} not registered in registry.`);
@@ -215,6 +277,13 @@ class RegistryResolver {
215
277
  throw new Error(`${entry.name}@${entry.version} has been yanked${when}.${reason}${replace}`);
216
278
  }
217
279
 
280
+ const revocation = isEntryRevoked(registry, entry);
281
+ if (revocation) {
282
+ const reason = revocation.reason ? `\nReason: ${revocation.reason}` : '';
283
+ const when = revocation.revoked_at ? ` (revoked ${revocation.revoked_at.slice(0, 10)})` : '';
284
+ throw new Error(`${entry.name}@${entry.version} has been revoked${when}.${reason}`);
285
+ }
286
+
218
287
  return { parsed, scope, entry, registry };
219
288
  }
220
289
 
@@ -250,6 +319,9 @@ function fetchRegistry() {
250
319
  module.exports = {
251
320
  RegistryResolver,
252
321
  parseName,
322
+ REQUIRED_SCHEMA_VERSION,
323
+ registryTrustIssues,
324
+ isEntryRevoked,
253
325
  loadRegistry,
254
326
  fetchRegistry,
255
327
  CANONICAL_REGISTRY_URL,
package/src/setup.js CHANGED
@@ -15,9 +15,11 @@
15
15
  const fs = require('fs');
16
16
  const path = require('path');
17
17
 
18
- const USER_KDNA_DIR = path.join(process.env.HOME || process.env.USERPROFILE || '.', '.kdna');
19
- const DOMAINS_DIR = path.join(USER_KDNA_DIR, 'domains');
20
- const CLUSTERS_DIR = path.join(USER_KDNA_DIR, 'clusters');
18
+ const PATHS = require('./paths');
19
+
20
+ const USER_KDNA_DIR = PATHS.root;
21
+ const DOMAINS_DIR = PATHS.domains.root;
22
+ const CLUSTERS_DIR = PATHS.clusters;
21
23
  const SKILLS_REPO = 'https://raw.githubusercontent.com/aikdna/kdna-skills/main';
22
24
 
23
25
  const AGENTS = [
@@ -119,15 +121,23 @@ async function cmdSetup() {
119
121
  const pkg = require(path.join(__dirname, '..', 'package.json'));
120
122
  log(`KDNA CLI v${pkg.version}`);
121
123
 
122
- // 2. KDNA data root
123
- ensureDir(DOMAINS_DIR);
124
+ // 2. KDNA data root — .kdna asset store
125
+ ensureDir(PATHS.root);
126
+ ensureDir(PATHS.packages);
124
127
  ensureDir(CLUSTERS_DIR);
128
+ ensureDir(PATHS.registry);
129
+ ensureDir(PATHS.traces);
130
+ ensureDir(PATHS.feedback);
131
+ ensureDir(PATHS.evals);
132
+ ensureDir(PATHS.cache);
133
+ ensureDir(PATHS.identity);
134
+ ensureDir(PATHS.licenses);
125
135
  log(`Data root: ${USER_KDNA_DIR}/`);
126
136
 
127
- // 2b. Clean legacy (un-scoped) domain directories from pre-v0.7
137
+ // 2b. Directory installs are not part of the runtime model.
128
138
  if (fs.existsSync(DOMAINS_DIR)) {
129
139
  const legacy = fs.readdirSync(DOMAINS_DIR).filter((e) => {
130
- if (e.startsWith('@') || e.startsWith('.')) return false;
140
+ if (e.startsWith('.')) return false;
131
141
  try {
132
142
  return fs.statSync(path.join(DOMAINS_DIR, e)).isDirectory();
133
143
  } catch {
@@ -136,19 +146,8 @@ async function cmdSetup() {
136
146
  });
137
147
  if (legacy.length) {
138
148
  console.log('');
139
- warn(
140
- `Removing ${legacy.length} legacy (un-scoped) domain director${legacy.length > 1 ? 'ies' : 'y'}:`,
141
- );
142
- for (const d of legacy) {
143
- const dPath = path.join(DOMAINS_DIR, d);
144
- try {
145
- fs.rmSync(dPath, { recursive: true, force: true });
146
- log(` removed ~/.kdna/domains/${d}/`);
147
- } catch (e) {
148
- warn(` could not remove ~/.kdna/domains/${d}/ — ${e.message}`);
149
- console.log(` To remove manually: rm -rf ~/.kdna/domains/${d}/`);
150
- }
151
- }
149
+ warn(`Ignoring ${legacy.length} legacy domain director${legacy.length > 1 ? 'ies' : 'y'}.`);
150
+ console.log(' Runtime assets now live under ~/.kdna/packages/ as .kdna files.');
152
151
  console.log(' Re-install with: kdna install @aikdna/<name>');
153
152
  console.log('');
154
153
  }