Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
155 changes: 155 additions & 0 deletions cli/cli/src/cmd/packument/show.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import { Cache, createStorageDriver, createPackumentKey, encodeOrigin } from '@_all_docs/cache';
import { compileSelector } from '@_all_docs/view';

export const usage = `Usage: _all_docs packument show <name[@version]> [options]

Display a packument from cache or registry.

Options:
--select <expr> Project specific fields using selector syntax
--registry <url> Registry URL (default: npm)
--origin <name> Origin name (alternative to --registry)
--raw Output raw JSON without formatting

Selector Syntax:
field Simple field access
field.nested Nested field access
field["key"] Bracket notation (for special chars or variables)
field|transform Apply transform (keys, values, length, etc.)
expr as alias Rename output field

Examples:
_all_docs packument show lodash
_all_docs packument show lodash --select 'versions|keys'
_all_docs packument show lodash --select 'time["4.17.21"]'
_all_docs packument show lodash@4.17.21 --select 'dist.integrity'
_all_docs packument show lodash --select 'name, versions|keys|length as versionCount'
`;

export const command = async (cli) => {
if (cli.values.help) {
console.log(usage);
return;
}

const spec = cli._[0];
if (!spec) {
console.error('Error: Package name required');
console.error('Usage: _all_docs packument show <name[@version]>');
process.exit(1);
}

// Parse spec (handles scoped packages: @scope/name@version)
const { name, version } = parseSpec(spec);

// Determine origin
const registry = cli.values.registry || 'https://registry.npmjs.org';
const origin = cli.values.origin || encodeOrigin(registry);

// Setup cache
const driver = await createStorageDriver({ CACHE_DIR: cli.dir('packuments') });
const cache = new Cache({ path: cli.dir('packuments'), driver });

// Create cache key and fetch
const cacheKey = createPackumentKey(name, registry);

let packument;
try {
const entry = await cache.fetch(cacheKey);
packument = entry?.body || entry;
} catch (err) {
console.error(`Error: Could not find cached packument for ${name}`);
console.error(`Hint: Run '_all_docs packument fetch ${name}' first`);
process.exit(1);
}

if (!packument) {
console.error(`Error: Package not found in cache: ${name}`);
console.error(`Hint: Run '_all_docs packument fetch ${name}' first`);
process.exit(1);
}

// Determine what to output
let data = packument;

// If version specified, narrow to version-specific data
if (version) {
if (!packument.versions?.[version]) {
console.error(`Error: Version not found: ${name}@${version}`);
console.error(`Available versions: ${Object.keys(packument.versions || {}).slice(0, 10).join(', ')}...`);
process.exit(1);
}
// Include version data + time field for date lookups
data = {
...packument.versions[version],
name: packument.name,
time: packument.time
};
}

// Apply projection if --select specified
if (cli.values.select) {
try {
const project = compileSelector(cli.values.select);
data = project(data);

// If single field selected, unwrap the result
const keys = Object.keys(data);
if (keys.length === 1) {
data = data[keys[0]];
}
} catch (err) {
console.error(`Error: Invalid select expression: ${err.message}`);
process.exit(1);
}
}

// Output
if (cli.values.raw) {
console.log(JSON.stringify(data));
} else {
console.log(JSON.stringify(data, null, 2));
}
};

/**
* Parse a package spec into name and version
* Handles: lodash, lodash@4.17.21, @scope/name, @scope/name@version
* @param {string} spec - Package spec
* @returns {{ name: string, version: string|null }}
*/
function parseSpec(spec) {
// Handle scoped packages
if (spec.startsWith('@')) {
// @scope/name or @scope/name@version
const slashIndex = spec.indexOf('/');
if (slashIndex === -1) {
return { name: spec, version: null };
}

const afterSlash = spec.slice(slashIndex + 1);
const atIndex = afterSlash.indexOf('@');

if (atIndex === -1) {
// @scope/name (no version)
return { name: spec, version: null };
} else {
// @scope/name@version
return {
name: spec.slice(0, slashIndex + 1 + atIndex),
version: afterSlash.slice(atIndex + 1)
};
}
}

// Unscoped package: name or name@version
const atIndex = spec.indexOf('@');
if (atIndex === -1) {
return { name: spec, version: null };
}

return {
name: spec.slice(0, atIndex),
version: spec.slice(atIndex + 1)
};
}
149 changes: 149 additions & 0 deletions cli/cli/src/cmd/packument/show.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import { describe, it } from 'node:test';
import { strict as assert } from 'node:assert';

/**
* Test the parseSpec function logic for packument show command
*/

/**
* Parse a package spec into name and version
* @param {string} spec - Package spec
* @returns {{ name: string, version: string|null }}
*/
function parseSpec(spec) {
// Handle scoped packages
if (spec.startsWith('@')) {
// @scope/name or @scope/name@version
const slashIndex = spec.indexOf('/');
if (slashIndex === -1) {
return { name: spec, version: null };
}

const afterSlash = spec.slice(slashIndex + 1);
const atIndex = afterSlash.indexOf('@');

if (atIndex === -1) {
// @scope/name (no version)
return { name: spec, version: null };
} else {
// @scope/name@version
return {
name: spec.slice(0, slashIndex + 1 + atIndex),
version: afterSlash.slice(atIndex + 1)
};
}
}

// Unscoped package: name or name@version
const atIndex = spec.indexOf('@');
if (atIndex === -1) {
return { name: spec, version: null };
}

return {
name: spec.slice(0, atIndex),
version: spec.slice(atIndex + 1)
};
}

describe('parseSpec', () => {
describe('unscoped packages', () => {
it('parses simple name', () => {
const result = parseSpec('lodash');
assert.deepEqual(result, { name: 'lodash', version: null });
});

it('parses name@version', () => {
const result = parseSpec('lodash@4.17.21');
assert.deepEqual(result, { name: 'lodash', version: '4.17.21' });
});

it('parses name with dots', () => {
const result = parseSpec('left-pad');
assert.deepEqual(result, { name: 'left-pad', version: null });
});

it('parses name with dots and version', () => {
const result = parseSpec('left-pad@1.3.0');
assert.deepEqual(result, { name: 'left-pad', version: '1.3.0' });
});
});

describe('scoped packages', () => {
it('parses @scope/name', () => {
const result = parseSpec('@babel/core');
assert.deepEqual(result, { name: '@babel/core', version: null });
});

it('parses @scope/name@version', () => {
const result = parseSpec('@babel/core@7.23.0');
assert.deepEqual(result, { name: '@babel/core', version: '7.23.0' });
});

it('parses complex scoped package', () => {
const result = parseSpec('@types/node@18.0.0');
assert.deepEqual(result, { name: '@types/node', version: '18.0.0' });
});

it('parses scoped package with no slash', () => {
const result = parseSpec('@scope');
assert.deepEqual(result, { name: '@scope', version: null });
});
});

describe('edge cases', () => {
it('parses version with pre-release tag', () => {
const result = parseSpec('react@18.0.0-alpha.1');
assert.deepEqual(result, { name: 'react', version: '18.0.0-alpha.1' });
});

it('parses scoped package with pre-release tag', () => {
const result = parseSpec('@angular/core@16.0.0-rc.1');
assert.deepEqual(result, { name: '@angular/core', version: '16.0.0-rc.1' });
});
});
});

describe('packument show --select', () => {
describe('single field selection', () => {
it('unwraps single field result', () => {
// When selecting a single field, the show command unwraps it
const data = { name: 'lodash' };
const keys = Object.keys(data);
const result = keys.length === 1 ? data[keys[0]] : data;

assert.equal(result, 'lodash');
});

it('keeps multi-field result as object', () => {
const data = { name: 'lodash', version: '4.17.21' };
const keys = Object.keys(data);
const result = keys.length === 1 ? data[keys[0]] : data;

assert.deepEqual(result, { name: 'lodash', version: '4.17.21' });
});
});

describe('version narrowing', () => {
it('merges version data with time field', () => {
const packument = {
name: 'lodash',
time: { '4.17.21': '2021-02-20T15:42:16.891Z' },
versions: {
'4.17.21': { dist: { integrity: 'sha512-abc' } }
}
};

const version = '4.17.21';
const data = {
...packument.versions[version],
name: packument.name,
time: packument.time
};

assert.equal(data.name, 'lodash');
assert.deepEqual(data.dist, { integrity: 'sha512-abc' });
assert.equal(data.time['4.17.21'], '2021-02-20T15:42:16.891Z');
});
});
});
58 changes: 58 additions & 0 deletions doc/cli-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,64 @@ npx _all_docs packument fetch @babel/core
- Cache file: `cache/packuments/v1:packument:npm:{hex(name)}.json`
- Console: Package name, version count, cache status

### packument show

Display a cached packument with optional field selection.

```bash
npx _all_docs packument show <name[@version]> [options]
```

**Arguments:**
- `<name[@version]>` - Package name, optionally with version

**Options:**
- `--select <expr>` - Project specific fields using selector syntax
- `--registry <url>` - Registry URL (default: npm)
- `--raw` - Output raw JSON without formatting

**Selector Syntax:**
- `field` - Simple field access
- `field.nested` - Nested field access
- `field["key"]` - Bracket notation (for keys with special chars)
- `field|transform` - Apply transform (keys, values, length, etc.)
- `expr as alias` - Rename output field

**Examples:**

```bash
# Show full packument
npx _all_docs packument show lodash

# Get version list
npx _all_docs packument show lodash --select 'versions|keys'

# Get publish date for specific version
npx _all_docs packument show lodash --select 'time["4.17.21"]'

# Get integrity hash for versioned packument
npx _all_docs packument show lodash@4.17.21 --select 'dist.integrity'

# Get multiple fields
npx _all_docs packument show lodash --select 'name, versions|keys|length as count'
```

**Use Cases:**

```bash
# Build verification - check integrity hash
npx _all_docs packument show express@4.18.2 --select 'dist.integrity'

# Audit - check publish date
npx _all_docs packument show left-pad --select 'time["1.1.1"]'

# Quick version count
npx _all_docs packument show lodash --select 'versions|keys|length'

# Get tarball URL for download
npx _all_docs packument show react@18.2.0 --select 'dist.tarball'
```

### packument fetch-list

Fetch multiple packuments from a list.
Expand Down
Loading