@raster-app/cli 0.3.2 → 0.4.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.
Files changed (3) hide show
  1. package/README.md +4 -3
  2. package/dist/index.js +125 -28
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -22,7 +22,7 @@ npx -p @raster-app/cli raster whoami
22
22
  raster auth login
23
23
  ```
24
24
 
25
- `raster auth login` signs you in over OAuth (no API key needed). When a browser is available — an interactive session with a display — it opens a **browser authorization-code flow** with a local loopback redirect; otherwise, over SSH or on a headless machine, it falls back to the **device authorization grant**, printing a short code and a URL you approve in a browser on any device. Pass `--browser` or `--device` to force either.
25
+ `raster auth login` signs you in over OAuth (no API key needed). When a browser is available, it opens a **browser authorization-code flow** with a local loopback redirect; otherwise, over SSH or on a headless machine, it falls back to the **device authorization grant**, printing a short code and a URL you approve in a browser on any device. Pass `--browser` or `--device` to force either.
26
26
 
27
27
  OAuth tokens (access + refresh) are stored in `~/.config/raster/config.json` with owner-only permissions. The CLI refreshes the access token automatically before it expires and rotates the refresh token; when the saved login can no longer be refreshed, it asks you to run `raster auth login` again.
28
28
 
@@ -90,13 +90,14 @@ Show the organizations, plan, and libraries the current credential can access. A
90
90
  | Command | Description | Example |
91
91
  | --- | --- | --- |
92
92
  | `raster assets ls` | List assets; filter with `--tag` (up to 5), page with `--page`/`--page-size`. | `raster assets ls --library brand --tag sunset` |
93
- | `raster assets get <assetId>` | Show one asset's metadata. | `raster assets get asset_123` |
93
+ | `raster assets get <assetId>` | Show one asset's metadata — including its variants, and duration plus stream details for video. | `raster assets get asset_123` |
94
94
  | `raster assets search <query>` | Search across the organization (`--library` scopes to one). | `raster assets search "golden hour"` |
95
95
  | `raster assets download <assetId>` | Download the file (`-o` path, `--force` to overwrite). | `raster assets download asset_123 -o photo.png` |
96
- | `raster assets upload <files...>` | Upload local files (batched at 20 per request). | `raster assets upload ./photos/*.png` |
96
+ | `raster assets upload <files...>` | Upload local files (batched at 20 per request). Pass `--parent <assetId>` to attach a single image as a variant of an existing asset. | `raster assets upload ./photos/*.png` |
97
97
  | `raster assets rm <assetIds...>` | Move assets to trash (`--yes` skips the prompt). | `raster assets rm asset_123 asset_456` |
98
98
  | `raster assets describe <assetId> --text <text>` | Set an asset's description. | `raster assets describe asset_123 --text "Hero shot"` |
99
99
  | `raster assets transfer <assetIds...> --to <libraryId>` | Move assets to another library. | `raster assets transfer asset_123 --to archive` |
100
+ | `raster assets promote <assetId> --variant <variantId>` | Make a variant the asset's default — the pixels change at the same URL, and the old default is kept as a variant. | `raster assets promote asset_123 --variant asset_456` |
100
101
 
101
102
  ### `tags`
102
103
 
package/dist/index.js CHANGED
@@ -16337,11 +16337,11 @@ config(en_default());
16337
16337
  // package.json
16338
16338
  var package_default = {
16339
16339
  name: "@raster-app/cli",
16340
- version: "0.3.2",
16340
+ version: "0.4.0",
16341
16341
  description: "Command-line client for the Raster API",
16342
16342
  license: "Apache-2.0",
16343
16343
  type: "module",
16344
- homepage: "https://docs.raster.app/api/cli",
16344
+ homepage: "https://raster.app/docs/api/cli",
16345
16345
  engines: {
16346
16346
  node: ">=20"
16347
16347
  },
@@ -16372,7 +16372,7 @@ var package_default = {
16372
16372
  };
16373
16373
 
16374
16374
  // src/api/version.ts
16375
- var API_VERSION = "2026-05-20";
16375
+ var API_VERSION = "2026-07-08";
16376
16376
  var DEFAULT_BASE_URL = "https://api.raster.app";
16377
16377
 
16378
16378
  // src/commands/assets.ts
@@ -16981,6 +16981,10 @@ import { join } from "node:path";
16981
16981
  var MAX_UPLOAD_FILES = 20;
16982
16982
  var MAX_TAGS_PER_QUERY = 5;
16983
16983
  var MAX_DELETE_IDS = 100;
16984
+ var MAX_TRANSFER_IDS = 100;
16985
+ var MAX_TAG_IDS = 100;
16986
+ var MAX_TAGS_PER_CALL = 20;
16987
+ var MAX_DESCRIPTION_LENGTH = 2000;
16984
16988
  var VISIBLE_KEY_PREFIX_LENGTH = 6;
16985
16989
  function maskApiKey(key) {
16986
16990
  return `${key.slice(0, VISIBLE_KEY_PREFIX_LENGTH)}…`;
@@ -16998,15 +17002,25 @@ function isErrnoException(error51) {
16998
17002
  var CONTENT_TYPES_BY_EXTENSION = {
16999
17003
  avif: "image/avif",
17000
17004
  bmp: "image/bmp",
17005
+ fig: "application/x-figma",
17001
17006
  gif: "image/gif",
17002
17007
  heic: "image/heic",
17008
+ heif: "image/heif",
17009
+ ico: "image/x-icon",
17003
17010
  jpeg: "image/jpeg",
17004
17011
  jpg: "image/jpeg",
17005
17012
  png: "image/png",
17006
17013
  svg: "image/svg+xml",
17007
17014
  tif: "image/tiff",
17008
17015
  tiff: "image/tiff",
17009
- webp: "image/webp"
17016
+ webp: "image/webp",
17017
+ avi: "video/x-msvideo",
17018
+ m4v: "video/mp4",
17019
+ mov: "video/quicktime",
17020
+ mp4: "video/mp4",
17021
+ mpeg: "video/mpeg",
17022
+ mpg: "video/mpeg",
17023
+ wmv: "video/x-ms-wmv"
17010
17024
  };
17011
17025
  var FALLBACK_CONTENT_TYPE = "application/octet-stream";
17012
17026
  function contentTypeForFilename(filename) {
@@ -17560,14 +17574,21 @@ async function createCommandContext(globals, overrides = {}) {
17560
17574
  };
17561
17575
  const baseUrl = validateBaseUrl(env.RASTER_API_BASE_URL ?? DEFAULT_BASE_URL);
17562
17576
  const baseFetch = overrides.fetch ?? globalThis.fetch;
17563
- const credential = await resolveActiveCredential({
17577
+ const { credential, sessionExpired } = await resolveActiveCredential({
17564
17578
  flagKey: globals.apiKey,
17565
17579
  env,
17566
17580
  config: config2,
17567
17581
  fetch: instrumentFetch(baseFetch, output, null),
17568
17582
  now: now()
17569
17583
  });
17584
+ if (sessionExpired) {
17585
+ note(output, "Your saved login could not be refreshed and has been cleared.");
17586
+ if (config2.apiKey) {
17587
+ note(output, "The API key stored in config is not used automatically. Pass --api-key, or set RASTER_API_KEY, to use it.");
17588
+ }
17589
+ }
17570
17590
  const apiKey = credential?.kind === "apiKey" ? { key: credential.key, source: credential.source } : null;
17591
+ const notAuthenticated = () => new AuthSetupError(authErrorMessage(sessionExpired));
17571
17592
  const bearer = credentialBearer(credential);
17572
17593
  const instrumentedFetch = instrumentFetch(baseFetch, output, bearer ? maskApiKey(bearer) : null);
17573
17594
  const clientFactory = (clientOverrides) => createApiClient({
@@ -17578,9 +17599,8 @@ async function createCommandContext(globals, overrides = {}) {
17578
17599
  let viewerPromise = null;
17579
17600
  const loadViewer = () => {
17580
17601
  if (!viewerPromise) {
17581
- if (!credential) {
17582
- throw new AuthSetupError("Not authenticated. Run `raster auth login` or set RASTER_API_KEY.");
17583
- }
17602
+ if (!credential)
17603
+ throw notAuthenticated();
17584
17604
  viewerPromise = unwrapResult(() => clientFactory().GET("/me", { params: { header: VERSION_HEADER } }));
17585
17605
  }
17586
17606
  return viewerPromise;
@@ -17629,6 +17649,7 @@ async function createCommandContext(globals, overrides = {}) {
17629
17649
  isInteractive: overrides.isInteractive ?? process.stdin.isTTY === true,
17630
17650
  credential,
17631
17651
  apiKey,
17652
+ sessionExpired,
17632
17653
  bearerToken: bearer,
17633
17654
  fetch: instrumentedFetch,
17634
17655
  clientFactory,
@@ -17663,7 +17684,7 @@ function credentialBearer(credential) {
17663
17684
  async function resolveActiveCredential(options) {
17664
17685
  const credential = resolveCredential({ flagKey: options.flagKey, env: options.env, config: options.config });
17665
17686
  if (credential?.kind !== "oauth")
17666
- return credential;
17687
+ return { credential, sessionExpired: false };
17667
17688
  const session = await ensureFreshOAuthSession({
17668
17689
  tokens: credential.tokens,
17669
17690
  env: options.env,
@@ -17672,13 +17693,9 @@ async function resolveActiveCredential(options) {
17672
17693
  now: options.now
17673
17694
  });
17674
17695
  if (session.status === "reauth-required") {
17675
- return resolveCredential({
17676
- flagKey: options.flagKey,
17677
- env: options.env,
17678
- config: { ...options.config, oauth: undefined }
17679
- });
17696
+ return { credential: null, sessionExpired: true };
17680
17697
  }
17681
- return { kind: "oauth", tokens: session.tokens };
17698
+ return { credential: { kind: "oauth", tokens: session.tokens }, sessionExpired: false };
17682
17699
  }
17683
17700
  function instrumentFetch(baseFetch, output, maskedKey) {
17684
17701
  return async (input, init) => {
@@ -17696,15 +17713,18 @@ function instrumentFetch(baseFetch, output, maskedKey) {
17696
17713
  }
17697
17714
  };
17698
17715
  }
17716
+ function authErrorMessage(sessionExpired) {
17717
+ return sessionExpired ? "Your saved login expired. Run `raster auth login` to sign in again." : "Not authenticated. Run `raster auth login` or set RASTER_API_KEY.";
17718
+ }
17699
17719
  function requireAuth(context) {
17700
17720
  if (context.credential)
17701
17721
  return context.credential;
17702
- throw new AuthSetupError("Not authenticated. Run `raster auth login` or set RASTER_API_KEY.");
17722
+ throw new AuthSetupError(authErrorMessage(context.sessionExpired));
17703
17723
  }
17704
17724
  function requireBearer(context) {
17705
17725
  if (context.bearerToken)
17706
17726
  return context.bearerToken;
17707
- throw new AuthSetupError("Not authenticated. Run `raster auth login` or set RASTER_API_KEY.");
17727
+ throw new AuthSetupError(authErrorMessage(context.sessionExpired));
17708
17728
  }
17709
17729
  async function executeCommand(context, action) {
17710
17730
  try {
@@ -17786,7 +17806,10 @@ async function getAssetCommand(context, assetId) {
17786
17806
  printJson(context, asset);
17787
17807
  return;
17788
17808
  }
17789
- renderRecord(context, [
17809
+ renderRecord(context, assetRecordFields(asset));
17810
+ }
17811
+ function assetRecordFields(asset) {
17812
+ const fields = [
17790
17813
  { label: "id", value: asset.id },
17791
17814
  { label: "name", value: asset.name ?? "" },
17792
17815
  { label: "type", value: asset.contentType ?? "" },
@@ -17795,7 +17818,39 @@ async function getAssetCommand(context, assetId) {
17795
17818
  { label: "tags", value: (asset.tags ?? []).filter((tag) => tag !== null).join(",") },
17796
17819
  { label: "description", value: asset.description ?? "" },
17797
17820
  { label: "url", value: asset.url ?? "" }
17798
- ]);
17821
+ ];
17822
+ const push = (label, value) => {
17823
+ if (value)
17824
+ fields.push({ label, value });
17825
+ };
17826
+ push("app url", asset.appUrl);
17827
+ push("duration", formatDuration(asset.duration));
17828
+ push("parent", asset.parentId);
17829
+ const variants = (asset.variants ?? []).filter((variant) => variant !== null);
17830
+ if (variants.length > 0) {
17831
+ fields.push({ label: "variants", value: variants.map((variant) => variant.id).join(",") });
17832
+ }
17833
+ push("approval", asset.approval?.state);
17834
+ const capture = asset.metadata?.capture;
17835
+ push("captured", capture?.recordedAt);
17836
+ push("device", [capture?.make, capture?.model].filter(Boolean).join(" "));
17837
+ const video = asset.metadata?.video;
17838
+ if (video) {
17839
+ push("codec", video.codec);
17840
+ push("frame rate", video.frameRate ? `${video.frameRate} fps` : "");
17841
+ push("audio", video.hasAudio ? [video.audioCodec, video.audioLayout].filter(Boolean).join(" ") || "yes" : "none");
17842
+ }
17843
+ return fields;
17844
+ }
17845
+ function formatDuration(seconds) {
17846
+ if (typeof seconds !== "number" || !Number.isFinite(seconds) || seconds < 0)
17847
+ return "";
17848
+ const whole = Math.round(seconds);
17849
+ const hours = Math.floor(whole / 3600);
17850
+ const minutes = Math.floor(whole % 3600 / 60);
17851
+ const remainder = whole % 60;
17852
+ const padded = (value) => String(value).padStart(2, "0");
17853
+ return hours > 0 ? `${hours}:${padded(minutes)}:${padded(remainder)}` : `${minutes}:${padded(remainder)}`;
17799
17854
  }
17800
17855
  async function fetchAsset(context, assetId) {
17801
17856
  const organizationId = await context.resolveOrg();
@@ -17862,9 +17917,15 @@ async function downloadAssetCommand(context, assetId, options) {
17862
17917
  var uploadedAssetsSchema = exports_external.object({
17863
17918
  assets: exports_external.array(exports_external.looseObject({ id: exports_external.string().optional(), name: exports_external.string().nullish() }))
17864
17919
  });
17865
- async function uploadAssetsCommand(context, files) {
17920
+ var uploadOptionsSchema = exports_external.object({
17921
+ parent: exports_external.string().min(1).optional()
17922
+ });
17923
+ async function uploadAssetsCommand(context, files, options = {}) {
17866
17924
  if (files.length === 0)
17867
17925
  throw new UsageError("Provide at least one file to upload.");
17926
+ if (options.parent && files.length !== 1) {
17927
+ throw new ValidationError("--parent uploads a single file. Pass exactly one path.");
17928
+ }
17868
17929
  const bearer = requireBearer(context);
17869
17930
  const organizationId = await context.resolveOrg();
17870
17931
  const libraryId = await context.resolveLibrary();
@@ -17882,6 +17943,8 @@ async function uploadAssetsCommand(context, files) {
17882
17943
  }
17883
17944
  formData.append("files", new Blob([bytes], { type: contentTypeForFilename(filePath) }), basename(filePath));
17884
17945
  }
17946
+ if (options.parent)
17947
+ formData.append("parentId", options.parent);
17885
17948
  if (batches.length > 1) {
17886
17949
  note(context, `Uploading batch ${batchIndex + 1}/${batches.length} (${batch.length} files)…`);
17887
17950
  }
@@ -17897,7 +17960,11 @@ async function uploadAssetsCommand(context, files) {
17897
17960
  printJson(context, payloads.length === 1 ? payloads[0] : payloads);
17898
17961
  return;
17899
17962
  }
17900
- note(context, `Uploaded ${uploadedNames.length} asset(s).`);
17963
+ if (options.parent) {
17964
+ note(context, `Uploaded ${uploadedNames.length} variant of ${options.parent}.`);
17965
+ } else {
17966
+ note(context, `Uploaded ${uploadedNames.length} asset(s).`);
17967
+ }
17901
17968
  for (const name of uploadedNames) {
17902
17969
  context.stdout(`${name}
17903
17970
  `);
@@ -17934,7 +18001,7 @@ async function removeAssetsCommand(context, assetIds, options) {
17934
18001
  note(context, `Moved ${assetIds.length} asset(s) to trash.`);
17935
18002
  }
17936
18003
  var describeOptionsSchema = exports_external.object({
17937
- text: exports_external.string().min(1)
18004
+ text: exports_external.string().min(1).max(MAX_DESCRIPTION_LENGTH)
17938
18005
  });
17939
18006
  async function describeAssetCommand(context, assetId, options) {
17940
18007
  requireAuth(context);
@@ -17960,6 +18027,9 @@ var transferOptionsSchema = exports_external.object({
17960
18027
  async function transferAssetsCommand(context, assetIds, options) {
17961
18028
  if (assetIds.length === 0)
17962
18029
  throw new UsageError("Provide at least one asset id.");
18030
+ if (assetIds.length > MAX_TRANSFER_IDS) {
18031
+ throw new ValidationError(`At most ${MAX_TRANSFER_IDS} asset ids are supported per call.`);
18032
+ }
17963
18033
  requireAuth(context);
17964
18034
  const organizationId = await context.resolveOrg();
17965
18035
  const libraryId = await context.resolveLibrary();
@@ -17974,15 +18044,34 @@ async function transferAssetsCommand(context, assetIds, options) {
17974
18044
  }
17975
18045
  note(context, `Transferred ${assetIds.length} asset(s) to library ${options.to}.`);
17976
18046
  }
18047
+ var promoteOptionsSchema = exports_external.object({
18048
+ variant: exports_external.string().min(1)
18049
+ });
18050
+ async function promoteVariantCommand(context, assetId, options) {
18051
+ requireAuth(context);
18052
+ const organizationId = await context.resolveOrg();
18053
+ const libraryId = await context.resolveLibrary();
18054
+ const client = context.clientFactory();
18055
+ const result = await unwrapResult(() => client.POST("/organizations/{organizationId}/libraries/{libraryId}/assets/{assetId}/promote", {
18056
+ params: { path: { organizationId, libraryId, assetId }, header: VERSION_HEADER },
18057
+ body: { variantId: options.variant }
18058
+ }));
18059
+ if (context.json) {
18060
+ printJson(context, result);
18061
+ return;
18062
+ }
18063
+ note(context, `Promoted variant ${result.promotedVariantId}; ${result.demotedVariantId} is kept as a variant. ` + "The swap completes a moment later — re-fetch the asset to see it.");
18064
+ }
17977
18065
  function registerAssetCommands(program2, runAction) {
17978
18066
  const assets = program2.command("assets").description("Work with assets in a library");
17979
18067
  assets.command("ls").description("List assets in the library").option("--page <number>", "Page number (1-based)").option("--page-size <number>", "Results per page").option("--tag <tag...>", "Filter by tag (repeatable, up to 5)").action((options, command) => runAction(command, (context) => listAssetsCommand(context, parseOptions(listOptionsSchema, options))));
17980
18068
  assets.command("get").description("Show a single asset").argument("<assetId>", "Asset id").action((assetId, _options, command) => runAction(command, (context) => getAssetCommand(context, assetId)));
17981
18069
  assets.command("search").description("Search assets across the organization (pass --library to scope to one)").argument("<query>", "Search query").option("--page <number>", "Page number (1-based)").option("--page-size <number>", "Results per page").action((query, options, command) => runAction(command, (context) => searchAssetsCommand(context, query, parseOptions(searchOptionsSchema, options))));
17982
18070
  assets.command("download").description("Download an asset's file").argument("<assetId>", "Asset id").option("-o, --output <path>", "Target file path").option("--force", "Overwrite the target file if it exists").action((assetId, options, command) => runAction(command, (context) => downloadAssetCommand(context, assetId, parseOptions(downloadOptionsSchema, options))));
17983
- assets.command("upload").description("Upload local files as assets").argument("<files...>", "Local file paths").action((files, _options, command) => runAction(command, (context) => uploadAssetsCommand(context, files)));
18071
+ assets.command("upload").description("Upload local files as assets").argument("<files...>", "Local file paths").option("--parent <assetId>", "Attach a single image as a variant of this asset").action((files, options, command) => runAction(command, (context) => uploadAssetsCommand(context, files, parseOptions(uploadOptionsSchema, options))));
17984
18072
  assets.command("rm").description("Move assets to trash (recoverable)").argument("<assetIds...>", "Asset ids").option("--yes", "Skip the confirmation prompt").action((assetIds, options, command) => runAction(command, (context) => removeAssetsCommand(context, assetIds, parseOptions(removeOptionsSchema, options))));
17985
18073
  assets.command("describe").description("Set an asset's description").argument("<assetId>", "Asset id").requiredOption("--text <description>", "Description text").action((assetId, options, command) => runAction(command, (context) => describeAssetCommand(context, assetId, parseOptions(describeOptionsSchema, options))));
18074
+ assets.command("promote").description("Make one of an asset's variants its default (same URL, new pixels)").argument("<assetId>", "Asset id").requiredOption("--variant <variantId>", "Variant to promote").action((assetId, options, command) => runAction(command, (context) => promoteVariantCommand(context, assetId, parseOptions(promoteOptionsSchema, options))));
17986
18075
  assets.command("transfer").description("Move assets to another library").argument("<assetIds...>", "Asset ids to move").requiredOption("--to <libraryId>", "Target library id").action((assetIds, options, command) => runAction(command, (context) => transferAssetsCommand(context, assetIds, parseOptions(transferOptionsSchema, options))));
17987
18076
  }
17988
18077
 
@@ -18376,10 +18465,10 @@ async function logoutCommand(context) {
18376
18465
  async function statusCommand(context) {
18377
18466
  if (!context.credential) {
18378
18467
  if (context.json) {
18379
- printJson(context, { authenticated: false });
18468
+ printJson(context, { authenticated: false, sessionExpired: context.sessionExpired });
18380
18469
  return;
18381
18470
  }
18382
- note(context, "Not authenticated. Run `raster auth login`.");
18471
+ note(context, context.sessionExpired ? "Your saved login expired. Run `raster auth login` to sign in again." : "Not authenticated. Run `raster auth login`.");
18383
18472
  return;
18384
18473
  }
18385
18474
  if (context.credential.kind === "oauth") {
@@ -18598,9 +18687,18 @@ async function listTagsCommand(context, options) {
18598
18687
  var mutateOptionsSchema = exports_external.object({
18599
18688
  tag: exports_external.array(exports_external.string().min(1)).min(1)
18600
18689
  });
18601
- async function tagAssetsCommand(context, assetIds, options) {
18690
+ function assertWithinTagCeilings(assetIds, tags) {
18602
18691
  if (assetIds.length === 0)
18603
18692
  throw new UsageError("Provide at least one asset id.");
18693
+ if (assetIds.length > MAX_TAG_IDS) {
18694
+ throw new ValidationError(`At most ${MAX_TAG_IDS} asset ids are supported per call.`);
18695
+ }
18696
+ if (tags.length > MAX_TAGS_PER_CALL) {
18697
+ throw new ValidationError(`At most ${MAX_TAGS_PER_CALL} tags are supported per call.`);
18698
+ }
18699
+ }
18700
+ async function tagAssetsCommand(context, assetIds, options) {
18701
+ assertWithinTagCeilings(assetIds, options.tag);
18604
18702
  requireAuth(context);
18605
18703
  const organizationId = await context.resolveOrg();
18606
18704
  const libraryId = await context.resolveLibrary();
@@ -18616,8 +18714,7 @@ async function tagAssetsCommand(context, assetIds, options) {
18616
18714
  note(context, `Tagged ${assetIds.length} asset(s) with ${options.tag.join(", ")}.`);
18617
18715
  }
18618
18716
  async function untagAssetsCommand(context, assetIds, options) {
18619
- if (assetIds.length === 0)
18620
- throw new UsageError("Provide at least one asset id.");
18717
+ assertWithinTagCeilings(assetIds, options.tag);
18621
18718
  requireAuth(context);
18622
18719
  const organizationId = await context.resolveOrg();
18623
18720
  const libraryId = await context.resolveLibrary();
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@raster-app/cli",
3
- "version": "0.3.2",
3
+ "version": "0.4.0",
4
4
  "description": "Command-line client for the Raster API",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
7
- "homepage": "https://docs.raster.app/api/cli",
7
+ "homepage": "https://raster.app/docs/api/cli",
8
8
  "engines": {
9
9
  "node": ">=20"
10
10
  },