Skip to content

Commit aa455f7

Browse files
committed
Adding assets:list: List assets with optional search filter via Management API
1 parent 830af07 commit aa455f7

8 files changed

Lines changed: 221 additions & 0 deletions

File tree

.claude/commands/blokctl-api.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ Every CLI command is backed by a reusable Action class with no CLI dependencies:
4747
| `Story\StoriesTagsAssignAction` | Assign tags to stories | `->tagged`, `->errors` |
4848
| `Story\StoryVersionsAction` | List story versions | `->versions`, `->storyId`, `->count()` |
4949
| `Story\StoriesWorkflowAssignAction` | Assign stage to unstaged stories (preflight+execute) | `->countWithoutStage`, `->workflowStages` |
50+
| `Asset\AssetsListAction` | List/search assets (MAPI only) | `->assets`, `->count()` |
5051
| `Asset\AssetsUnreferencedAction` | Find unreferenced assets | `->unreferencedAssets`, `->totalAssets`, `->referencedCount`, `->storiesAnalyzed` |
5152
| `Workflow\WorkflowsListAction` | List workflows+stages | `->workflows`, `->count()` |
5253
| `Workflow\WorkflowStageShowAction` | Show stage details | `->stage`, `->workflowName` |

.claude/commands/blokctl-cli.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ Use this skill when the user wants to run blokctl commands to manage a Storyblok
4949

5050
### Assets
5151

52+
- **`assets:list`** — List assets. Options: `--search`, `--page` (`-p`), `--per-page` (max 1000). Management API only, no preview token needed.
5253
- **`assets:unreferenced`** — Detect orphaned assets not referenced in any story. Fetches all assets via Management API (1000/page), scans all stories via CDN API (higher rate limits), then diffs.
5354

5455
### Workflows

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
All notable changes to `blokctl` will be documented in this file.
44

5+
## 0.6.1 - 2026-04-03
6+
- Adding `assets:list`: List assets with optional search filter via Management API
7+
58
## 0.6.0 - 2026-04-03
69
- Upgrading `symfony/console` from ^7.0 to ^8.0
710
- Upgrading `phpunit/phpunit` from ^12.0 to ^13.0

README.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -683,6 +683,22 @@ Displays stage details: name, ID, workflow, position, color, publish permissions
683683

684684
### Assets
685685

686+
#### `assets:list` — List assets
687+
688+
```bash
689+
php bin/blokctl assets:list -S 290817118944379
690+
php bin/blokctl assets:list -S 290817118944379 --search=hero
691+
php bin/blokctl assets:list -S 290817118944379 --per-page=100 --page=2
692+
```
693+
694+
| Option | Short | Description |
695+
|---|---|---|
696+
| `--search` | | Search assets by filename |
697+
| `--page` | `-p` | Page number (default: 1) |
698+
| `--per-page` | | Results per page (max 1000, default: 25) |
699+
700+
Lists assets via the Management API. Each asset displays its filename, ID, content type, file size, and creation date. No preview token needed.
701+
686702
#### `assets:unreferenced` — List assets not referenced in any story
687703

688704
```bash
@@ -1277,6 +1293,24 @@ $result->workflowId; // int
12771293

12781294
### Assets
12791295

1296+
#### List assets
1297+
1298+
```php
1299+
use Blokctl\Action\Asset\AssetsListAction;
1300+
1301+
$result = (new AssetsListAction($client))->execute(
1302+
spaceId: $spaceId,
1303+
search: 'hero',
1304+
page: 1,
1305+
perPage: 25,
1306+
);
1307+
1308+
$result->assets; // Assets collection
1309+
$result->count(); // int
1310+
```
1311+
1312+
Uses the Management API only. No preview token needed.
1313+
12801314
#### Find unreferenced assets
12811315

12821316
```php

bin/blokctl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
<?php
33
require __DIR__ . "/../vendor/autoload.php";
44

5+
use Blokctl\Command\AssetsListCommand;
56
use Blokctl\Command\AssetsUnreferencedCommand;
67
use Blokctl\Command\AppProvisionInstallCommand;
78
use Blokctl\Command\AppProvisionListCommand;
@@ -37,6 +38,7 @@ $dotenv->safeLoad();
3738

3839
$application = new Application("blokctl", "1.0.0-dev");
3940

41+
$application->addCommand(new AssetsListCommand());
4042
$application->addCommand(new AssetsUnreferencedCommand());
4143
$application->addCommand(new AppProvisionInstallCommand());
4244
$application->addCommand(new AppProvisionListCommand());
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Blokctl\Action\Asset;
6+
7+
use Storyblok\ManagementApi\Endpoints\AssetApi;
8+
use Storyblok\ManagementApi\ManagementApiClient;
9+
use Storyblok\ManagementApi\QueryParameters\AssetsParams;
10+
use Storyblok\ManagementApi\QueryParameters\PaginationParams;
11+
12+
final readonly class AssetsListAction
13+
{
14+
public function __construct(
15+
private ManagementApiClient $client,
16+
) {}
17+
18+
public function execute(
19+
string $spaceId,
20+
?string $search = null,
21+
int $page = 1,
22+
int $perPage = 25,
23+
): AssetsListResult {
24+
$params = new AssetsParams(
25+
search: $search,
26+
);
27+
28+
$pagination = new PaginationParams(
29+
page: $page,
30+
perPage: $perPage,
31+
);
32+
33+
$assets = (new AssetApi($this->client, $spaceId))
34+
->page($params, $pagination)->data();
35+
36+
return new AssetsListResult(
37+
assets: $assets,
38+
);
39+
}
40+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Blokctl\Action\Asset;
6+
7+
use Storyblok\ManagementApi\Data\Assets;
8+
9+
final readonly class AssetsListResult
10+
{
11+
public function __construct(
12+
public Assets $assets,
13+
) {}
14+
15+
public function count(): int
16+
{
17+
return $this->assets->count();
18+
}
19+
}

src/Command/AssetsListCommand.php

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Blokctl\Command;
6+
7+
use Blokctl\Action\Asset\AssetsListAction;
8+
use Blokctl\Render;
9+
use Storyblok\ManagementApi\Data\Asset;
10+
use Symfony\Component\Console\Attribute\AsCommand;
11+
use Symfony\Component\Console\Input\InputInterface;
12+
use Symfony\Component\Console\Input\InputOption;
13+
use Symfony\Component\Console\Output\OutputInterface;
14+
15+
#[AsCommand(
16+
name: 'assets:list',
17+
description: 'List assets with optional search filter',
18+
)]
19+
class AssetsListCommand extends AbstractCommand
20+
{
21+
#[\Override]
22+
protected function configure(): void
23+
{
24+
parent::configure();
25+
$this
26+
->addOption(
27+
'search',
28+
null,
29+
InputOption::VALUE_REQUIRED,
30+
'Search assets by filename',
31+
)
32+
->addOption(
33+
'page',
34+
'p',
35+
InputOption::VALUE_REQUIRED,
36+
'Page number',
37+
'1',
38+
)
39+
->addOption(
40+
'per-page',
41+
null,
42+
InputOption::VALUE_REQUIRED,
43+
'Results per page (max 1000)',
44+
'25',
45+
);
46+
}
47+
48+
protected function execute(
49+
InputInterface $input,
50+
OutputInterface $output,
51+
): int {
52+
/** @var string|null $search */
53+
$search = $input->getOption('search');
54+
/** @var string $pageOption */
55+
$pageOption = $input->getOption('page');
56+
/** @var string $perPageOption */
57+
$perPageOption = $input->getOption('per-page');
58+
59+
try {
60+
$result = (new AssetsListAction($this->client))->execute(
61+
spaceId: $this->spaceId,
62+
search: $search,
63+
page: (int) $pageOption,
64+
perPage: (int) $perPageOption,
65+
);
66+
} catch (\RuntimeException $runtimeException) {
67+
Render::error($runtimeException->getMessage());
68+
return self::FAILURE;
69+
}
70+
71+
if ($result->count() === 0) {
72+
Render::log('No assets found');
73+
return self::SUCCESS;
74+
}
75+
76+
Render::titleSection(
77+
'Assets (page ' . $pageOption .
78+
', showing ' . $result->count() . ')',
79+
);
80+
81+
/** @var Asset $asset */
82+
foreach ($result->assets as $asset) {
83+
$details = [];
84+
$details[] = 'id: ' . $asset->id();
85+
$contentType = $asset->contentType();
86+
if ($contentType !== '') {
87+
$details[] = $contentType;
88+
}
89+
90+
$size = $asset->contentLength();
91+
if ($size !== null && $size > 0) {
92+
$details[] = $this->formatBytes($size);
93+
}
94+
95+
$createdAt = $asset->createdAt();
96+
if ($createdAt !== null && $createdAt !== '') {
97+
$details[] = $createdAt;
98+
}
99+
100+
Render::labelValue(
101+
$asset->filename(),
102+
implode(' | ', $details),
103+
);
104+
}
105+
106+
return self::SUCCESS;
107+
}
108+
109+
private function formatBytes(int $bytes): string
110+
{
111+
if ($bytes < 1024) {
112+
return $bytes . ' B';
113+
}
114+
115+
if ($bytes < 1048576) {
116+
return round($bytes / 1024, 1) . ' KB';
117+
}
118+
119+
return round($bytes / 1048576, 1) . ' MB';
120+
}
121+
}

0 commit comments

Comments
 (0)