Skip to content

Commit d685fd1

Browse files
indexzeroclaude
andauthored
feat(cli): add packument show command with field projection (#22)
Add `packument show <name[@Version]>` command for displaying cached packuments with optional field selection via `--select`. Extend the projection module to support bracket notation in paths, enabling selectors like `time["4.17.21"]` for version keys with dots. - Parse name@version syntax for both scoped and unscoped packages - Support --select with full selector syntax including transforms - Handle bracket notation for keys with special characters - Unwrap single-field results for cleaner pipeline output --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 5961dec commit d685fd1

5 files changed

Lines changed: 493 additions & 7 deletions

File tree

cli/cli/src/cmd/packument/show.js

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
import { Cache, createStorageDriver, createPackumentKey, encodeOrigin } from '@_all_docs/cache';
2+
import { compileSelector } from '@_all_docs/view';
3+
4+
export const usage = `Usage: _all_docs packument show <name[@version]> [options]
5+
6+
Display a packument from cache or registry.
7+
8+
Options:
9+
--select <expr> Project specific fields using selector syntax
10+
--registry <url> Registry URL (default: npm)
11+
--origin <name> Origin name (alternative to --registry)
12+
--raw Output raw JSON without formatting
13+
14+
Selector Syntax:
15+
field Simple field access
16+
field.nested Nested field access
17+
field["key"] Bracket notation (for special chars or variables)
18+
field|transform Apply transform (keys, values, length, etc.)
19+
expr as alias Rename output field
20+
21+
Examples:
22+
_all_docs packument show lodash
23+
_all_docs packument show lodash --select 'versions|keys'
24+
_all_docs packument show lodash --select 'time["4.17.21"]'
25+
_all_docs packument show lodash@4.17.21 --select 'dist.integrity'
26+
_all_docs packument show lodash --select 'name, versions|keys|length as versionCount'
27+
`;
28+
29+
export const command = async (cli) => {
30+
if (cli.values.help) {
31+
console.log(usage);
32+
return;
33+
}
34+
35+
const spec = cli._[0];
36+
if (!spec) {
37+
console.error('Error: Package name required');
38+
console.error('Usage: _all_docs packument show <name[@version]>');
39+
process.exit(1);
40+
}
41+
42+
// Parse spec (handles scoped packages: @scope/name@version)
43+
const { name, version } = parseSpec(spec);
44+
45+
// Determine origin
46+
const registry = cli.values.registry || 'https://registry.npmjs.org';
47+
const origin = cli.values.origin || encodeOrigin(registry);
48+
49+
// Setup cache
50+
const driver = await createStorageDriver({ CACHE_DIR: cli.dir('packuments') });
51+
const cache = new Cache({ path: cli.dir('packuments'), driver });
52+
53+
// Create cache key and fetch
54+
const cacheKey = createPackumentKey(name, registry);
55+
56+
let packument;
57+
try {
58+
const entry = await cache.fetch(cacheKey);
59+
packument = entry?.body || entry;
60+
} catch (err) {
61+
console.error(`Error: Could not find cached packument for ${name}`);
62+
console.error(`Hint: Run '_all_docs packument fetch ${name}' first`);
63+
process.exit(1);
64+
}
65+
66+
if (!packument) {
67+
console.error(`Error: Package not found in cache: ${name}`);
68+
console.error(`Hint: Run '_all_docs packument fetch ${name}' first`);
69+
process.exit(1);
70+
}
71+
72+
// Determine what to output
73+
let data = packument;
74+
75+
// If version specified, narrow to version-specific data
76+
if (version) {
77+
if (!packument.versions?.[version]) {
78+
console.error(`Error: Version not found: ${name}@${version}`);
79+
console.error(`Available versions: ${Object.keys(packument.versions || {}).slice(0, 10).join(', ')}...`);
80+
process.exit(1);
81+
}
82+
// Include version data + time field for date lookups
83+
data = {
84+
...packument.versions[version],
85+
name: packument.name,
86+
time: packument.time
87+
};
88+
}
89+
90+
// Apply projection if --select specified
91+
if (cli.values.select) {
92+
try {
93+
const project = compileSelector(cli.values.select);
94+
data = project(data);
95+
96+
// If single field selected, unwrap the result
97+
const keys = Object.keys(data);
98+
if (keys.length === 1) {
99+
data = data[keys[0]];
100+
}
101+
} catch (err) {
102+
console.error(`Error: Invalid select expression: ${err.message}`);
103+
process.exit(1);
104+
}
105+
}
106+
107+
// Output
108+
if (cli.values.raw) {
109+
console.log(JSON.stringify(data));
110+
} else {
111+
console.log(JSON.stringify(data, null, 2));
112+
}
113+
};
114+
115+
/**
116+
* Parse a package spec into name and version
117+
* Handles: lodash, lodash@4.17.21, @scope/name, @scope/name@version
118+
* @param {string} spec - Package spec
119+
* @returns {{ name: string, version: string|null }}
120+
*/
121+
function parseSpec(spec) {
122+
// Handle scoped packages
123+
if (spec.startsWith('@')) {
124+
// @scope/name or @scope/name@version
125+
const slashIndex = spec.indexOf('/');
126+
if (slashIndex === -1) {
127+
return { name: spec, version: null };
128+
}
129+
130+
const afterSlash = spec.slice(slashIndex + 1);
131+
const atIndex = afterSlash.indexOf('@');
132+
133+
if (atIndex === -1) {
134+
// @scope/name (no version)
135+
return { name: spec, version: null };
136+
} else {
137+
// @scope/name@version
138+
return {
139+
name: spec.slice(0, slashIndex + 1 + atIndex),
140+
version: afterSlash.slice(atIndex + 1)
141+
};
142+
}
143+
}
144+
145+
// Unscoped package: name or name@version
146+
const atIndex = spec.indexOf('@');
147+
if (atIndex === -1) {
148+
return { name: spec, version: null };
149+
}
150+
151+
return {
152+
name: spec.slice(0, atIndex),
153+
version: spec.slice(atIndex + 1)
154+
};
155+
}
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
import { describe, it } from 'node:test';
2+
import { strict as assert } from 'node:assert';
3+
4+
/**
5+
* Test the parseSpec function logic for packument show command
6+
*/
7+
8+
/**
9+
* Parse a package spec into name and version
10+
* @param {string} spec - Package spec
11+
* @returns {{ name: string, version: string|null }}
12+
*/
13+
function parseSpec(spec) {
14+
// Handle scoped packages
15+
if (spec.startsWith('@')) {
16+
// @scope/name or @scope/name@version
17+
const slashIndex = spec.indexOf('/');
18+
if (slashIndex === -1) {
19+
return { name: spec, version: null };
20+
}
21+
22+
const afterSlash = spec.slice(slashIndex + 1);
23+
const atIndex = afterSlash.indexOf('@');
24+
25+
if (atIndex === -1) {
26+
// @scope/name (no version)
27+
return { name: spec, version: null };
28+
} else {
29+
// @scope/name@version
30+
return {
31+
name: spec.slice(0, slashIndex + 1 + atIndex),
32+
version: afterSlash.slice(atIndex + 1)
33+
};
34+
}
35+
}
36+
37+
// Unscoped package: name or name@version
38+
const atIndex = spec.indexOf('@');
39+
if (atIndex === -1) {
40+
return { name: spec, version: null };
41+
}
42+
43+
return {
44+
name: spec.slice(0, atIndex),
45+
version: spec.slice(atIndex + 1)
46+
};
47+
}
48+
49+
describe('parseSpec', () => {
50+
describe('unscoped packages', () => {
51+
it('parses simple name', () => {
52+
const result = parseSpec('lodash');
53+
assert.deepEqual(result, { name: 'lodash', version: null });
54+
});
55+
56+
it('parses name@version', () => {
57+
const result = parseSpec('lodash@4.17.21');
58+
assert.deepEqual(result, { name: 'lodash', version: '4.17.21' });
59+
});
60+
61+
it('parses name with dots', () => {
62+
const result = parseSpec('left-pad');
63+
assert.deepEqual(result, { name: 'left-pad', version: null });
64+
});
65+
66+
it('parses name with dots and version', () => {
67+
const result = parseSpec('left-pad@1.3.0');
68+
assert.deepEqual(result, { name: 'left-pad', version: '1.3.0' });
69+
});
70+
});
71+
72+
describe('scoped packages', () => {
73+
it('parses @scope/name', () => {
74+
const result = parseSpec('@babel/core');
75+
assert.deepEqual(result, { name: '@babel/core', version: null });
76+
});
77+
78+
it('parses @scope/name@version', () => {
79+
const result = parseSpec('@babel/core@7.23.0');
80+
assert.deepEqual(result, { name: '@babel/core', version: '7.23.0' });
81+
});
82+
83+
it('parses complex scoped package', () => {
84+
const result = parseSpec('@types/node@18.0.0');
85+
assert.deepEqual(result, { name: '@types/node', version: '18.0.0' });
86+
});
87+
88+
it('parses scoped package with no slash', () => {
89+
const result = parseSpec('@scope');
90+
assert.deepEqual(result, { name: '@scope', version: null });
91+
});
92+
});
93+
94+
describe('edge cases', () => {
95+
it('parses version with pre-release tag', () => {
96+
const result = parseSpec('react@18.0.0-alpha.1');
97+
assert.deepEqual(result, { name: 'react', version: '18.0.0-alpha.1' });
98+
});
99+
100+
it('parses scoped package with pre-release tag', () => {
101+
const result = parseSpec('@angular/core@16.0.0-rc.1');
102+
assert.deepEqual(result, { name: '@angular/core', version: '16.0.0-rc.1' });
103+
});
104+
});
105+
});
106+
107+
describe('packument show --select', () => {
108+
describe('single field selection', () => {
109+
it('unwraps single field result', () => {
110+
// When selecting a single field, the show command unwraps it
111+
const data = { name: 'lodash' };
112+
const keys = Object.keys(data);
113+
const result = keys.length === 1 ? data[keys[0]] : data;
114+
115+
assert.equal(result, 'lodash');
116+
});
117+
118+
it('keeps multi-field result as object', () => {
119+
const data = { name: 'lodash', version: '4.17.21' };
120+
const keys = Object.keys(data);
121+
const result = keys.length === 1 ? data[keys[0]] : data;
122+
123+
assert.deepEqual(result, { name: 'lodash', version: '4.17.21' });
124+
});
125+
});
126+
127+
describe('version narrowing', () => {
128+
it('merges version data with time field', () => {
129+
const packument = {
130+
name: 'lodash',
131+
time: { '4.17.21': '2021-02-20T15:42:16.891Z' },
132+
versions: {
133+
'4.17.21': { dist: { integrity: 'sha512-abc' } }
134+
}
135+
};
136+
137+
const version = '4.17.21';
138+
const data = {
139+
...packument.versions[version],
140+
name: packument.name,
141+
time: packument.time
142+
};
143+
144+
assert.equal(data.name, 'lodash');
145+
assert.deepEqual(data.dist, { integrity: 'sha512-abc' });
146+
assert.equal(data.time['4.17.21'], '2021-02-20T15:42:16.891Z');
147+
});
148+
});
149+
});

doc/cli-reference.md

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,64 @@ npx _all_docs packument fetch @babel/core
155155
- Cache file: `cache/packuments/v1:packument:npm:{hex(name)}.json`
156156
- Console: Package name, version count, cache status
157157

158+
### packument show
159+
160+
Display a cached packument with optional field selection.
161+
162+
```bash
163+
npx _all_docs packument show <name[@version]> [options]
164+
```
165+
166+
**Arguments:**
167+
- `<name[@version]>` - Package name, optionally with version
168+
169+
**Options:**
170+
- `--select <expr>` - Project specific fields using selector syntax
171+
- `--registry <url>` - Registry URL (default: npm)
172+
- `--raw` - Output raw JSON without formatting
173+
174+
**Selector Syntax:**
175+
- `field` - Simple field access
176+
- `field.nested` - Nested field access
177+
- `field["key"]` - Bracket notation (for keys with special chars)
178+
- `field|transform` - Apply transform (keys, values, length, etc.)
179+
- `expr as alias` - Rename output field
180+
181+
**Examples:**
182+
183+
```bash
184+
# Show full packument
185+
npx _all_docs packument show lodash
186+
187+
# Get version list
188+
npx _all_docs packument show lodash --select 'versions|keys'
189+
190+
# Get publish date for specific version
191+
npx _all_docs packument show lodash --select 'time["4.17.21"]'
192+
193+
# Get integrity hash for versioned packument
194+
npx _all_docs packument show lodash@4.17.21 --select 'dist.integrity'
195+
196+
# Get multiple fields
197+
npx _all_docs packument show lodash --select 'name, versions|keys|length as count'
198+
```
199+
200+
**Use Cases:**
201+
202+
```bash
203+
# Build verification - check integrity hash
204+
npx _all_docs packument show express@4.18.2 --select 'dist.integrity'
205+
206+
# Audit - check publish date
207+
npx _all_docs packument show left-pad --select 'time["1.1.1"]'
208+
209+
# Quick version count
210+
npx _all_docs packument show lodash --select 'versions|keys|length'
211+
212+
# Get tarball URL for download
213+
npx _all_docs packument show react@18.2.0 --select 'dist.tarball'
214+
```
215+
158216
### packument fetch-list
159217

160218
Fetch multiple packuments from a list.

0 commit comments

Comments
 (0)