chub-dev 0.2.0-beta.1 → 0.2.0-beta.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chub-dev",
3
- "version": "0.2.0-beta.1",
3
+ "version": "0.2.0-beta.3",
4
4
  "description": "CLI for Context Hub - search and retrieve LLM-optimized docs and skills",
5
5
  "type": "module",
6
6
  "bin": {
@@ -8,7 +8,8 @@
8
8
  },
9
9
  "files": [
10
10
  "bin/",
11
- "src/"
11
+ "src/",
12
+ "dist/"
12
13
  ],
13
14
  "engines": {
14
15
  "node": ">=18.0.0"
@@ -32,6 +33,7 @@
32
33
  },
33
34
  "homepage": "https://github.com/andrewyng/context-hub#readme",
34
35
  "scripts": {
36
+ "prepublish": "node bin/chub build ../content -o dist --base-url https://cdn.aichub.org/v1",
35
37
  "test": "vitest run",
36
38
  "test:watch": "vitest",
37
39
  "test:coverage": "vitest run --coverage"
@@ -7,17 +7,14 @@ import { output, error, info } from '../lib/output.js';
7
7
  import { trackEvent } from '../lib/analytics.js';
8
8
 
9
9
  /**
10
- * Core fetch logic shared by `get docs` and `get skills`.
11
- * @param {string} type - "doc" or "skill"
12
- * @param {string[]} ids - one or more entry ids
13
- * @param {object} opts - command options (lang, version, output, full)
14
- * @param {object} globalOpts - global options (json)
10
+ * Fetch one or more entries by ID. Auto-detects doc vs skill per entry.
15
11
  */
16
- async function fetchEntries(type, ids, opts, globalOpts) {
12
+ async function fetchEntries(ids, opts, globalOpts) {
17
13
  const results = [];
18
14
 
19
15
  for (const id of ids) {
20
- const result = getEntry(id, type);
16
+ // Search both docs and skills — auto-detect type
17
+ const result = getEntry(id);
21
18
 
22
19
  if (result.ambiguous) {
23
20
  error(
@@ -27,10 +24,11 @@ async function fetchEntries(type, ids, opts, globalOpts) {
27
24
  }
28
25
 
29
26
  if (!result.entry) {
30
- error(`Entry "${id}" not found in ${type}s.`, globalOpts);
27
+ error(`Entry "${id}" not found.`, globalOpts);
31
28
  }
32
29
 
33
30
  const entry = result.entry;
31
+ const type = entry.languages ? 'doc' : 'skill';
34
32
  const resolved = resolveDocPath(entry, opts.lang, opts.version);
35
33
 
36
34
  if (!resolved) {
@@ -52,10 +50,10 @@ async function fetchEntries(type, ids, opts, globalOpts) {
52
50
  try {
53
51
  if (opts.full && resolved.files.length > 0) {
54
52
  const allFiles = await fetchDocFull(resolved.source, resolved.path, resolved.files);
55
- results.push({ id: entry.id, files: allFiles, path: resolved.path });
53
+ results.push({ id: entry.id, type, files: allFiles, path: resolved.path });
56
54
  } else {
57
55
  const content = await fetchDoc(resolved.source, entryFile.filePath);
58
- results.push({ id: entry.id, content, path: entryFile.filePath });
56
+ results.push({ id: entry.id, type, content, path: entryFile.filePath });
59
57
  }
60
58
  } catch (err) {
61
59
  error(err.message, globalOpts);
@@ -64,7 +62,7 @@ async function fetchEntries(type, ids, opts, globalOpts) {
64
62
 
65
63
  // Track fetches
66
64
  for (const r of results) {
67
- trackEvent(type === 'doc' ? 'doc_fetched' : 'skill_fetched', {
65
+ trackEvent(r.type === 'doc' ? 'doc_fetched' : 'skill_fetched', {
68
66
  entry_id: r.id,
69
67
  full: !!opts.full,
70
68
  lang: opts.lang || undefined,
@@ -74,7 +72,6 @@ async function fetchEntries(type, ids, opts, globalOpts) {
74
72
  // Output
75
73
  if (opts.output) {
76
74
  if (opts.full) {
77
- // --full -o: write individual files preserving directory structure
78
75
  for (const r of results) {
79
76
  if (r.files) {
80
77
  const baseDir = ids.length > 1 ? join(opts.output, r.id) : opts.output;
@@ -98,6 +95,7 @@ async function fetchEntries(type, ids, opts, globalOpts) {
98
95
  mkdirSync(opts.output, { recursive: true });
99
96
  for (const r of results) {
100
97
  const outPath = join(opts.output, `${r.id}.md`);
98
+ mkdirSync(dirname(outPath), { recursive: true });
101
99
  writeFileSync(outPath, r.content);
102
100
  info(`Written to ${outPath}`);
103
101
  }
@@ -110,18 +108,16 @@ async function fetchEntries(type, ids, opts, globalOpts) {
110
108
  }
111
109
  }
112
110
  if (globalOpts.json) {
113
- console.log(JSON.stringify(results.map((r) => ({ id: r.id, path: opts.output }))));
111
+ console.log(JSON.stringify(results.map((r) => ({ id: r.id, type: r.type, path: opts.output }))));
114
112
  }
115
113
  } else {
116
- // stdout
117
114
  if (results.length === 1 && !results[0].files) {
118
115
  output(
119
- { id: results[0].id, content: results[0].content, path: results[0].path },
116
+ { id: results[0].id, type: results[0].type, content: results[0].content, path: results[0].path },
120
117
  (data) => process.stdout.write(data.content),
121
118
  globalOpts
122
119
  );
123
120
  } else {
124
- // Concatenate all content (--full to stdout, or multiple entries)
125
121
  const parts = results.flatMap((r) => {
126
122
  if (r.files) {
127
123
  return r.files.map((f) => `# FILE: ${f.name}\n\n${f.content}`);
@@ -130,7 +126,7 @@ async function fetchEntries(type, ids, opts, globalOpts) {
130
126
  });
131
127
  const combined = parts.join('\n\n---\n\n');
132
128
  output(
133
- results.map((r) => ({ id: r.id, path: r.path })),
129
+ results.map((r) => ({ id: r.id, type: r.type, path: r.path })),
134
130
  () => process.stdout.write(combined),
135
131
  globalOpts
136
132
  );
@@ -139,29 +135,15 @@ async function fetchEntries(type, ids, opts, globalOpts) {
139
135
  }
140
136
 
141
137
  export function registerGetCommand(program) {
142
- const get = program
143
- .command('get')
144
- .description('Retrieve docs or skills');
145
-
146
- get
147
- .command('docs <ids...>')
148
- .description('Fetch documentation content')
149
- .option('--lang <language>', 'Language variant')
150
- .option('--version <version>', 'Specific version')
151
- .option('-o, --output <path>', 'Write to file or directory')
152
- .option('--full', 'Fetch all files (not just entry point)')
153
- .action(async (ids, opts) => {
154
- const globalOpts = program.optsWithGlobals();
155
- await fetchEntries('doc', ids, opts, globalOpts);
156
- });
157
-
158
- get
159
- .command('skills <ids...>')
160
- .description('Fetch skill content')
138
+ program
139
+ .command('get <ids...>')
140
+ .description('Fetch docs or skills by ID (auto-detects type)')
141
+ .option('--lang <language>', 'Language variant (for docs)')
142
+ .option('--version <version>', 'Specific version (for docs)')
161
143
  .option('-o, --output <path>', 'Write to file or directory')
162
144
  .option('--full', 'Fetch all files (not just entry point)')
163
145
  .action(async (ids, opts) => {
164
146
  const globalOpts = program.optsWithGlobals();
165
- await fetchEntries('skill', ids, opts, globalOpts);
147
+ await fetchEntries(ids, opts, globalOpts);
166
148
  });
167
149
  }
package/src/index.js CHANGED
@@ -26,17 +26,16 @@ ${chalk.bold.underline('Getting Started')}
26
26
  ${chalk.dim('$')} chub search ${chalk.dim('# list everything available')}
27
27
  ${chalk.dim('$')} chub search "stripe" ${chalk.dim('# fuzzy search')}
28
28
  ${chalk.dim('$')} chub search stripe/payments ${chalk.dim('# exact id → full detail')}
29
- ${chalk.dim('$')} chub get docs stripe/payments ${chalk.dim('# print doc to terminal')}
30
- ${chalk.dim('$')} chub get docs stripe/payments -o doc.md ${chalk.dim('# save to file')}
31
- ${chalk.dim('$')} chub get docs stripe/payments --lang py ${chalk.dim('# specific language')}
32
- ${chalk.dim('$')} chub get skills pw/login-flows ${chalk.dim('# fetch a skill')}
33
- ${chalk.dim('$')} chub get docs openai/chat stripe/payments ${chalk.dim('# fetch multiple')}
29
+ ${chalk.dim('$')} chub get stripe/api ${chalk.dim('# print doc to terminal')}
30
+ ${chalk.dim('$')} chub get stripe/api -o doc.md ${chalk.dim('# save to file')}
31
+ ${chalk.dim('$')} chub get openai/chat --lang py ${chalk.dim('# specific language')}
32
+ ${chalk.dim('$')} chub get pw-community/login-flows ${chalk.dim('# fetch a skill')}
33
+ ${chalk.dim('$')} chub get openai/chat stripe/api ${chalk.dim('# fetch multiple')}
34
34
 
35
35
  ${chalk.bold.underline('Commands')}
36
36
 
37
37
  ${chalk.bold('search')} [query] Search docs and skills (no query = list all)
38
- ${chalk.bold('get docs')} <ids...> Fetch documentation content
39
- ${chalk.bold('get skills')} <ids...> Fetch skill content
38
+ ${chalk.bold('get')} <ids...> Fetch docs or skills by ID
40
39
  ${chalk.bold('update')} Refresh the cached registry
41
40
  ${chalk.bold('cache')} status|clear Manage the local cache
42
41
  ${chalk.bold('build')} <content-dir> Build registry from content directory
@@ -56,10 +55,10 @@ ${chalk.bold.underline('Agent Piping Patterns')}
56
55
 
57
56
  ${chalk.dim('# Search → pick → fetch → save')}
58
57
  ${chalk.dim('$')} ID=$(chub search "stripe" --json | jq -r '.results[0].id')
59
- ${chalk.dim('$')} chub get docs "$ID" --lang js -o .context/stripe.md
58
+ ${chalk.dim('$')} chub get "$ID" --lang js -o .context/stripe.md
60
59
 
61
- ${chalk.dim('# Fetch multiple docs at once')}
62
- ${chalk.dim('$')} chub get docs openai/chat stripe/payments -o .context/
60
+ ${chalk.dim('# Fetch multiple at once')}
61
+ ${chalk.dim('$')} chub get openai/chat stripe/api -o .context/
63
62
 
64
63
  ${chalk.bold.underline('Multi-Source Config')} ${chalk.dim('(~/.chub/config.yaml)')}
65
64
 
@@ -69,7 +68,7 @@ ${chalk.bold.underline('Multi-Source Config')} ${chalk.dim('(~/.chub/config.yaml
69
68
  ${chalk.dim(' - name: internal')}
70
69
  ${chalk.dim(' path: /path/to/local/docs')}
71
70
 
72
- ${chalk.dim('# On id collision, use source: prefix: chub get docs internal:openai/chat')}
71
+ ${chalk.dim('# On id collision, use source: prefix: chub get internal:openai/chat')}
73
72
  `);
74
73
  }
75
74
 
package/src/lib/cache.js CHANGED
@@ -1,9 +1,20 @@
1
1
  import { existsSync, mkdirSync, readFileSync, writeFileSync, rmSync, readdirSync, statSync } from 'node:fs';
2
- import { join } from 'node:path';
2
+ import { join, dirname } from 'node:path';
3
3
  import { pipeline } from 'node:stream/promises';
4
4
  import { createWriteStream } from 'node:fs';
5
+ import { fileURLToPath } from 'node:url';
5
6
  import { getChubDir, loadConfig } from './config.js';
6
7
 
8
+ const __dirname = dirname(fileURLToPath(import.meta.url));
9
+
10
+ /**
11
+ * Path to bundled content shipped with the npm package.
12
+ * Contains registry.json + doc files built from content/ at publish time.
13
+ */
14
+ function getBundledDir() {
15
+ return join(__dirname, '..', '..', 'dist');
16
+ }
17
+
7
18
  function getSourceDir(sourceName) {
8
19
  return join(getChubDir(), 'sources', sourceName);
9
20
  }
@@ -153,7 +164,13 @@ export async function fetchDoc(source, docPath) {
153
164
  return readFileSync(cachedPath, 'utf8');
154
165
  }
155
166
 
156
- // Fetch from CDN
167
+ // Check bundled content (shipped with npm package)
168
+ const bundledPath = join(getBundledDir(), docPath);
169
+ if (existsSync(bundledPath)) {
170
+ return readFileSync(bundledPath, 'utf8');
171
+ }
172
+
173
+ // Fetch from CDN (optional — only if source has a URL)
157
174
  const url = `${source.url}/${docPath}`;
158
175
  const controller = new AbortController();
159
176
  const timeout = setTimeout(() => controller.abort(), 30000);
@@ -303,6 +320,17 @@ export async function ensureRegistry() {
303
320
  return;
304
321
  }
305
322
 
306
- // No registries at all — must download remote ones
323
+ // No registries at all — try bundled content first, then network
324
+ const bundledRegistry = join(getBundledDir(), 'registry.json');
325
+ if (existsSync(bundledRegistry)) {
326
+ // Seed cache from bundled content (ships with npm package)
327
+ const defaultDir = getSourceDir('default');
328
+ mkdirSync(defaultDir, { recursive: true });
329
+ writeFileSync(getSourceRegistryPath('default'), readFileSync(bundledRegistry, 'utf8'));
330
+ writeMeta('default', { lastUpdated: 0, bundledSeed: true }); // lastUpdated=0 → stale, so chub update will refresh
331
+ return;
332
+ }
333
+
334
+ // No bundled content either — must download from remote
307
335
  await fetchAllRegistries(true);
308
336
  }