@aikdna/kdna-cli 0.16.10 → 0.18.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,60 @@ 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) issues.push('registry.trust.timestamp.expires_at must be an ISO timestamp');
73
+ if (snapshotExpires && snapshotExpires <= now) {
74
+ issues.push(`registry snapshot expired at ${trust.snapshot.expires_at}`);
75
+ }
76
+ if (timestampExpires && timestampExpires <= now) {
77
+ issues.push(`registry timestamp expired at ${trust.timestamp.expires_at}`);
78
+ }
79
+
80
+ return issues;
81
+ }
82
+
83
+ function registryRevocations(registry) {
84
+ return registry?.trust?.revocations || [];
85
+ }
86
+
87
+ function isEntryRevoked(registry, entry) {
88
+ const revocations = registryRevocations(registry);
89
+ return revocations.find((rev) => {
90
+ if (rev.name && rev.name !== entry.name) return false;
91
+ if (rev.version && rev.version !== entry.version) return false;
92
+ if (rev.asset_digest && rev.asset_digest !== entry.asset_digest) return false;
93
+ return rev.name || rev.asset_digest;
94
+ }) || null;
95
+ }
96
+
42
97
  // ─── Name parsing ───────────────────────────────────────────────────────
43
98
 
44
99
  /**
@@ -151,7 +206,15 @@ class RegistryResolver {
151
206
  _loadRegistryForScope(scopeName) {
152
207
  if (this._registries.has(scopeName)) return this._registries.get(scopeName);
153
208
  const source = this._sourceForScope(scopeName);
154
- const data = source.load({ allowNetwork: this.allowNetwork, refresh: this.refresh });
209
+ let data = source.load({ allowNetwork: this.allowNetwork, refresh: this.refresh });
210
+ let trustIssues = data ? registryTrustIssues(data) : [];
211
+ if (trustIssues.length && this.allowNetwork && !this.refresh) {
212
+ data = source.load({ allowNetwork: true, refresh: true });
213
+ trustIssues = data ? registryTrustIssues(data) : [];
214
+ }
215
+ if (trustIssues.length) {
216
+ throw new Error(`Registry trust check failed:\n${trustIssues.map((i) => `- ${i}`).join('\n')}`);
217
+ }
155
218
  this._registries.set(scopeName, data);
156
219
  return data;
157
220
  }
@@ -186,12 +249,6 @@ class RegistryResolver {
186
249
  );
187
250
  }
188
251
 
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
252
  const scope = registry.scopes?.[parsed.scope];
196
253
  if (!scope) {
197
254
  throw new Error(`Scope ${parsed.scope} not registered in registry.`);
@@ -215,6 +272,13 @@ class RegistryResolver {
215
272
  throw new Error(`${entry.name}@${entry.version} has been yanked${when}.${reason}${replace}`);
216
273
  }
217
274
 
275
+ const revocation = isEntryRevoked(registry, entry);
276
+ if (revocation) {
277
+ const reason = revocation.reason ? `\nReason: ${revocation.reason}` : '';
278
+ const when = revocation.revoked_at ? ` (revoked ${revocation.revoked_at.slice(0, 10)})` : '';
279
+ throw new Error(`${entry.name}@${entry.version} has been revoked${when}.${reason}`);
280
+ }
281
+
218
282
  return { parsed, scope, entry, registry };
219
283
  }
220
284
 
@@ -250,6 +314,9 @@ function fetchRegistry() {
250
314
  module.exports = {
251
315
  RegistryResolver,
252
316
  parseName,
317
+ REQUIRED_SCHEMA_VERSION,
318
+ registryTrustIssues,
319
+ isEntryRevoked,
253
320
  loadRegistry,
254
321
  fetchRegistry,
255
322
  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
  }