@drawcall/market 0.1.35 → 0.1.37

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 (63) hide show
  1. package/README.md +6 -3
  2. package/dist/asset-implementation.d.ts +16 -7
  3. package/dist/asset-implementation.d.ts.map +1 -1
  4. package/dist/asset-implementation.js +20 -1
  5. package/dist/asset-implementation.js.map +1 -1
  6. package/dist/cli.js +14 -2
  7. package/dist/cli.js.map +1 -1
  8. package/dist/commands/install.d.ts +3 -4
  9. package/dist/commands/install.d.ts.map +1 -1
  10. package/dist/commands/install.js +57 -11
  11. package/dist/commands/install.js.map +1 -1
  12. package/dist/commands/list.d.ts +17 -0
  13. package/dist/commands/list.d.ts.map +1 -0
  14. package/dist/commands/list.js +26 -0
  15. package/dist/commands/list.js.map +1 -0
  16. package/dist/commands/upload.d.ts.map +1 -1
  17. package/dist/commands/upload.js +64 -2
  18. package/dist/commands/upload.js.map +1 -1
  19. package/dist/contract.d.ts +9 -1
  20. package/dist/contract.d.ts.map +1 -1
  21. package/dist/contract.js +1 -1
  22. package/dist/contract.js.map +1 -1
  23. package/dist/index.d.ts +5 -2
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +2 -0
  26. package/dist/index.js.map +1 -1
  27. package/dist/install.d.ts +14 -0
  28. package/dist/install.d.ts.map +1 -1
  29. package/dist/install.js +133 -23
  30. package/dist/install.js.map +1 -1
  31. package/dist/market-lock.d.ts +17 -0
  32. package/dist/market-lock.d.ts.map +1 -0
  33. package/dist/market-lock.js +44 -0
  34. package/dist/market-lock.js.map +1 -0
  35. package/dist/output.d.ts +4 -2
  36. package/dist/output.d.ts.map +1 -1
  37. package/dist/output.js +28 -7
  38. package/dist/output.js.map +1 -1
  39. package/dist/package-json.d.ts +11 -0
  40. package/dist/package-json.d.ts.map +1 -0
  41. package/dist/package-json.js +23 -0
  42. package/dist/package-json.js.map +1 -0
  43. package/dist/skill.d.ts +1 -1
  44. package/dist/skill.d.ts.map +1 -1
  45. package/dist/skill.js +20 -9
  46. package/dist/skill.js.map +1 -1
  47. package/package.json +2 -2
  48. package/src/asset-implementation.ts +40 -6
  49. package/src/cli.ts +24 -6
  50. package/src/commands/install.ts +76 -16
  51. package/src/commands/list.ts +47 -0
  52. package/src/commands/upload.ts +84 -2
  53. package/src/contract.ts +10 -1
  54. package/src/index.ts +15 -0
  55. package/src/install.ts +197 -35
  56. package/src/market-lock.ts +65 -0
  57. package/src/output.ts +36 -8
  58. package/src/package-json.ts +37 -0
  59. package/src/skill.ts +20 -9
  60. package/tests/install-command.test.ts +23 -1
  61. package/tests/install-layout.test.ts +117 -0
  62. package/tests/list-command.test.ts +76 -0
  63. package/tests/output.test.ts +69 -3
package/dist/skill.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export declare const marketSkill = "---\nname: market\ndescription: Find, preview, install, generate, and publish Drawcall Market assets from a coding agent.\n---\n\n# Drawcall Market\n\nUse the `market` CLI. Keep commands short and read the summary lines.\n\n## Quick Start\n\n```sh\nmarket search \"wooden chair\" --type model --limit 3\nmarket install wooden-chair --cwd \"$PWD\"\nmarket preview wooden-chair --out /tmp/wooden-chair.png\n```\n\n## Workflow\n\n1. Search first unless the user already gave an exact asset name. `search` requires `--type`; use `model` unless the user names another supported type: `humanoid-model`, `texture`, `humanoid-animation`, `template`, `sound-effect`, `background-music`, `environment`, or `flipbook`.\n2. Use `--limit 1` for lookup, `--limit 3` for choice. Search caps at 5 and prints full descriptions.\n3. `install` takes one or more exact asset names (optionally `name@range`); it does not search or generate. Find names with `search` first. No `--type` is needed \u2014 asset names are unique.\n4. `preview <name>` saves the preview image; no `--type` is needed. Not every type has previews (e.g. `humanoid-animation`, `template`, `sound-effect`, `background-music`); the CLI reports when one is unavailable.\n5. Use `--unapproved` only when the user asks for unapproved/private/admin assets. Do not install unapproved assets without explicit acceptance.\n6. `generate --type <type> \"<prompt>\"` creates a new asset; it requires login and a type that supports generation. No asset type currently supports generation.\n7. Upload only when publishing is requested: `market upload <name> <zip> \"<description>\" --type <type>`. Declare dependencies with repeatable flags: `--npm name@range`, `--asset name@range`, `--skill label=source`. A skill source is a `skills add` argument: a whole repo (`owner/repo` or a git URL), a single skill via the full URL form `https://github.com/owner/repo/tree/<branch>/<subpath>` (the `tree/<branch>/<subpath>` shorthand needs the full URL, not `owner/repo`), or a local path to a skill directory inside the zip. Example: `market upload my-scene scene.zip \"A scene\" --type model --npm three@^0.178.0 --skill web-design=https://github.com/vercel-labs/agent-skills/tree/main/skills/web-design-guidelines`.\n8. Installed `environment` assets contain `public/environment/<name>.hdr` for Three.js IBL lighting and `public/environment/<name>-background.webp` for the visible equirectangular background. Use `market preview` to fetch the preview image separately.\n9. Installed `flipbook` assets contain `public/flipbook/<name>.ktx2`. Render them with `@drawcall/flipbook`'s `Flipbook` class and Three.js `KTX2Loader` for Basis-compressed files; `market preview` fetches the middle frame from the flipbook.\n\n## Humanoid animations\n\n`humanoid-animation` assets are single-clip GLBs \u2014 one motion per asset (one idle, one walk-forward, one jump, one attack) on a normalized skeleton that retargets onto any humanoid, whatever its role. A behaving character is therefore a *set* of clips, not one asset: from what the character actually does, budget the clips it needs \u2014 an idle, its locomotion (walk/run, often split by direction: fwd/bwd/left/right), and one clip per distinct action and reaction it performs \u2014 then search for each separately.\n\nSearch one motion per query, named by the motion, because results rank by keyword overlap: a query naming several motions at once is dominated by whichever word matches the most assets and buries the others, so real clips look like a gap when they exist. Names describe the motion, not the character \u2014 so search the motion (`\"walk forward\"`, `\"reload\"`, `\"jump\"`), not the role (`\"player run\"`, `\"boss attack\"`). If a motion finds nothing, retry with synonyms (run/jog/sprint, attack/swing/strike).\n\n## Output\n\nCommands print concise, line-oriented summaries:\n\n```text\nResults: 2/8 query=\"wooden chair\" type=model approval=approved\n- wooden-chair@1.0.0 | model | approved | Low-poly wooden chair\nInstalled:\n- wooden-chair@1.0.0 (asset)\n description: Low-poly wooden chair\n files:\n public/model\n \u2514\u2500 wooden-chair.glb\n- three@^0.178.0 (npm)\n- web-design \u2190 https://github.com/vercel-labs/agent-skills/tree/main/skills/web-design-guidelines (skill)\nSaved preview for wooden-chair@1.0.0: /tmp/wooden-chair.png\n```\n\nAssets may also declare `skill` dependencies, installed for you via the `skills` CLI (`skills add`) during `install`. Sources are either a GitHub/git ref or a local path to a skill directory shipped inside the asset. This requires `npx` to be available.\n\nIf search returns no results, try one broader noun phrase. If a command returns `Error: Not logged in...`, ask before running `market login`.\n";
1
+ export declare const marketSkill = "---\nname: market\ndescription: Find, preview, install, generate, and publish Drawcall Market assets from a coding agent.\n---\n\n# Drawcall Market\n\nUse the `market` CLI. Keep commands short and read the summary lines.\n\n## Quick Start\n\n```sh\nmarket search \"wooden chair\" --type model --limit 3\nmarket install wooden-chair --cwd \"$PWD\"\nmarket list --cwd \"$PWD\"\nmarket preview wooden-chair --out /tmp/wooden-chair.png\n```\n\n## Workflow\n\n1. In an existing repo, run `list --cwd \"$PWD\"` first to see installed local assets from `.drawcall/market-lock.json`. Use the listed names with `preview <name>` when you want preview images.\n2. Search first unless the user already gave an exact asset name. `search` requires `--type`; use `model` unless the user names another supported type: `humanoid-model`, `texture`, `humanoid-animation`, `template`, `sound-effect`, `background-music`, `environment`, or `flipbook`.\n3. Use `--limit 1` for lookup, `--limit 3` for choice. Search caps at 5 and prints full descriptions.\n4. `install` takes zero or more exact asset names (optionally `name@range`). With names, it installs those assets; with no names, it installs `assetDependencies` from the nearest `package.json`. It does not search or generate. Find names with `search` first. No `--type` is needed \u2014 asset names are unique. Use `--force` only when the user agrees to overwrite changed local files.\n5. `preview <name>` saves the preview image; no `--type` is needed. Not every type has previews (e.g. `humanoid-animation`, `template`, `sound-effect`, `background-music`); the CLI reports when one is unavailable.\n6. Use `--unapproved` only when the user asks for unapproved/private/admin assets. Do not install unapproved assets without explicit acceptance.\n7. `generate --type <type> \"<prompt>\"` creates a new asset; it requires login and a type that supports generation. No asset type currently supports generation.\n8. Upload only when publishing is requested: `market upload <name> <zip> \"<description>\" --type <type>`. Declare dependencies with repeatable flags: `--npm name@range`, `--asset name@range`, `--skill label=source`. Template uploads also read root `package.json.assetDependencies`; `--asset` flags are additive and must not conflict. Template upload omits installed dependency files whose hashes still match `.drawcall/market-lock.json`, so edited local files stay in the template. A skill source is a `skills add` argument: a whole repo (`owner/repo` or a git URL), a single skill via the full URL form `https://github.com/owner/repo/tree/<branch>/<subpath>` (the `tree/<branch>/<subpath>` shorthand needs the full URL, not `owner/repo`), or a local path to a skill directory inside the zip. Example: `market upload my-scene scene.zip \"A scene\" --type model --npm three@^0.178.0 --skill web-design=https://github.com/vercel-labs/agent-skills/tree/main/skills/web-design-guidelines`.\n9. Installed `environment` assets contain `public/environment/<name>.hdr` for Three.js IBL lighting and `public/environment/<name>-background.webp` for the visible equirectangular background. Use `market preview` to fetch the preview image separately.\n10. Installed `flipbook` assets contain `public/flipbook/<name>.ktx2`. Render them with `@drawcall/flipbook`'s `Flipbook` class and Three.js `KTX2Loader` for Basis-compressed files; `market preview` fetches the middle frame from the flipbook.\n\n## Humanoid animations\n\n`humanoid-animation` assets are single-clip GLBs \u2014 one motion per asset (one idle, one walk-forward, one jump, one attack) on a normalized skeleton that retargets onto any humanoid, whatever its role. A behaving character is therefore a *set* of clips, not one asset: from what the character actually does, budget the clips it needs \u2014 an idle, its locomotion (walk/run, often split by direction: fwd/bwd/left/right), and one clip per distinct action and reaction it performs \u2014 then search for each separately.\n\nSearch one motion per query, named by the motion, because results rank by keyword overlap: a query naming several motions at once is dominated by whichever word matches the most assets and buries the others, so real clips look like a gap when they exist. Names describe the motion, not the character \u2014 so search the motion (`\"walk forward\"`, `\"reload\"`, `\"jump\"`), not the role (`\"player run\"`, `\"boss attack\"`). If a motion finds nothing, retry with synonyms (run/jog/sprint, attack/swing/strike).\n\n## Output\n\nCommands print concise, line-oriented summaries:\n\n```text\nResults: 2/8 query=\"wooden chair\" type=model approval=approved\n- wooden-chair@1.0.0 | model | approved | Low-poly wooden chair\nInstalled:\n- wooden-chair@1.0.0 (asset)\n description: Low-poly wooden chair\n files:\n public/model\n \u2514\u2500 wooden-chair.glb\n- three@^0.178.0 (npm)\n- web-design \u2190 https://github.com/vercel-labs/agent-skills/tree/main/skills/web-design-guidelines (skill)\nInstalled assets: 1\n- wooden-chair@1.0.0 (model)\n files:\n public/model\n \u2514\u2500 wooden-chair.glb\nSaved preview for wooden-chair@1.0.0: /tmp/wooden-chair.png\n```\n\nAssets may also declare `skill` dependencies, installed for you via the `skills` CLI (`skills add`) during `install`. Sources are either a GitHub/git ref or a local path to a skill directory shipped inside the asset. This requires `npx` to be available.\n\nInstalled non-template assets are saved to `package.json.assetDependencies`; templates are scaffolds and are not saved as project asset dependencies. Exact installed versions and file hashes are recorded in `.drawcall/market-lock.json`.\n\n`list` is offline: it reads `.drawcall/market-lock.json` from the nearest package root and prints exact installed names, versions, types, and installed file paths.\n\nIf search returns no results, try one broader noun phrase. If a command returns `Error: Not logged in...`, ask before running `market login`.\n";
2
2
  //# sourceMappingURL=skill.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"skill.d.ts","sourceRoot":"","sources":["../src/skill.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,WAAW,0qJAwDvB,CAAA"}
1
+ {"version":3,"file":"skill.d.ts","sourceRoot":"","sources":["../src/skill.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,WAAW,81LAmEvB,CAAA"}
package/dist/skill.js CHANGED
@@ -12,20 +12,22 @@ Use the \`market\` CLI. Keep commands short and read the summary lines.
12
12
  \`\`\`sh
13
13
  market search "wooden chair" --type model --limit 3
14
14
  market install wooden-chair --cwd "$PWD"
15
+ market list --cwd "$PWD"
15
16
  market preview wooden-chair --out /tmp/wooden-chair.png
16
17
  \`\`\`
17
18
 
18
19
  ## Workflow
19
20
 
20
- 1. Search first unless the user already gave an exact asset name. \`search\` requires \`--type\`; use \`model\` unless the user names another supported type: \`humanoid-model\`, \`texture\`, \`humanoid-animation\`, \`template\`, \`sound-effect\`, \`background-music\`, \`environment\`, or \`flipbook\`.
21
- 2. Use \`--limit 1\` for lookup, \`--limit 3\` for choice. Search caps at 5 and prints full descriptions.
22
- 3. \`install\` takes one or more exact asset names (optionally \`name@range\`); it does not search or generate. Find names with \`search\` first. No \`--type\` is needed — asset names are unique.
23
- 4. \`preview <name>\` saves the preview image; no \`--type\` is needed. Not every type has previews (e.g. \`humanoid-animation\`, \`template\`, \`sound-effect\`, \`background-music\`); the CLI reports when one is unavailable.
24
- 5. Use \`--unapproved\` only when the user asks for unapproved/private/admin assets. Do not install unapproved assets without explicit acceptance.
25
- 6. \`generate --type <type> "<prompt>"\` creates a new asset; it requires login and a type that supports generation. No asset type currently supports generation.
26
- 7. Upload only when publishing is requested: \`market upload <name> <zip> "<description>" --type <type>\`. Declare dependencies with repeatable flags: \`--npm name@range\`, \`--asset name@range\`, \`--skill label=source\`. A skill source is a \`skills add\` argument: a whole repo (\`owner/repo\` or a git URL), a single skill via the full URL form \`https://github.com/owner/repo/tree/<branch>/<subpath>\` (the \`tree/<branch>/<subpath>\` shorthand needs the full URL, not \`owner/repo\`), or a local path to a skill directory inside the zip. Example: \`market upload my-scene scene.zip "A scene" --type model --npm three@^0.178.0 --skill web-design=https://github.com/vercel-labs/agent-skills/tree/main/skills/web-design-guidelines\`.
27
- 8. Installed \`environment\` assets contain \`public/environment/<name>.hdr\` for Three.js IBL lighting and \`public/environment/<name>-background.webp\` for the visible equirectangular background. Use \`market preview\` to fetch the preview image separately.
28
- 9. Installed \`flipbook\` assets contain \`public/flipbook/<name>.ktx2\`. Render them with \`@drawcall/flipbook\`'s \`Flipbook\` class and Three.js \`KTX2Loader\` for Basis-compressed files; \`market preview\` fetches the middle frame from the flipbook.
21
+ 1. In an existing repo, run \`list --cwd "$PWD"\` first to see installed local assets from \`.drawcall/market-lock.json\`. Use the listed names with \`preview <name>\` when you want preview images.
22
+ 2. Search first unless the user already gave an exact asset name. \`search\` requires \`--type\`; use \`model\` unless the user names another supported type: \`humanoid-model\`, \`texture\`, \`humanoid-animation\`, \`template\`, \`sound-effect\`, \`background-music\`, \`environment\`, or \`flipbook\`.
23
+ 3. Use \`--limit 1\` for lookup, \`--limit 3\` for choice. Search caps at 5 and prints full descriptions.
24
+ 4. \`install\` takes zero or more exact asset names (optionally \`name@range\`). With names, it installs those assets; with no names, it installs \`assetDependencies\` from the nearest \`package.json\`. It does not search or generate. Find names with \`search\` first. No \`--type\` is needed — asset names are unique. Use \`--force\` only when the user agrees to overwrite changed local files.
25
+ 5. \`preview <name>\` saves the preview image; no \`--type\` is needed. Not every type has previews (e.g. \`humanoid-animation\`, \`template\`, \`sound-effect\`, \`background-music\`); the CLI reports when one is unavailable.
26
+ 6. Use \`--unapproved\` only when the user asks for unapproved/private/admin assets. Do not install unapproved assets without explicit acceptance.
27
+ 7. \`generate --type <type> "<prompt>"\` creates a new asset; it requires login and a type that supports generation. No asset type currently supports generation.
28
+ 8. Upload only when publishing is requested: \`market upload <name> <zip> "<description>" --type <type>\`. Declare dependencies with repeatable flags: \`--npm name@range\`, \`--asset name@range\`, \`--skill label=source\`. Template uploads also read root \`package.json.assetDependencies\`; \`--asset\` flags are additive and must not conflict. Template upload omits installed dependency files whose hashes still match \`.drawcall/market-lock.json\`, so edited local files stay in the template. A skill source is a \`skills add\` argument: a whole repo (\`owner/repo\` or a git URL), a single skill via the full URL form \`https://github.com/owner/repo/tree/<branch>/<subpath>\` (the \`tree/<branch>/<subpath>\` shorthand needs the full URL, not \`owner/repo\`), or a local path to a skill directory inside the zip. Example: \`market upload my-scene scene.zip "A scene" --type model --npm three@^0.178.0 --skill web-design=https://github.com/vercel-labs/agent-skills/tree/main/skills/web-design-guidelines\`.
29
+ 9. Installed \`environment\` assets contain \`public/environment/<name>.hdr\` for Three.js IBL lighting and \`public/environment/<name>-background.webp\` for the visible equirectangular background. Use \`market preview\` to fetch the preview image separately.
30
+ 10. Installed \`flipbook\` assets contain \`public/flipbook/<name>.ktx2\`. Render them with \`@drawcall/flipbook\`'s \`Flipbook\` class and Three.js \`KTX2Loader\` for Basis-compressed files; \`market preview\` fetches the middle frame from the flipbook.
29
31
 
30
32
  ## Humanoid animations
31
33
 
@@ -48,11 +50,20 @@ Installed:
48
50
  └─ wooden-chair.glb
49
51
  - three@^0.178.0 (npm)
50
52
  - web-design ← https://github.com/vercel-labs/agent-skills/tree/main/skills/web-design-guidelines (skill)
53
+ Installed assets: 1
54
+ - wooden-chair@1.0.0 (model)
55
+ files:
56
+ public/model
57
+ └─ wooden-chair.glb
51
58
  Saved preview for wooden-chair@1.0.0: /tmp/wooden-chair.png
52
59
  \`\`\`
53
60
 
54
61
  Assets may also declare \`skill\` dependencies, installed for you via the \`skills\` CLI (\`skills add\`) during \`install\`. Sources are either a GitHub/git ref or a local path to a skill directory shipped inside the asset. This requires \`npx\` to be available.
55
62
 
63
+ Installed non-template assets are saved to \`package.json.assetDependencies\`; templates are scaffolds and are not saved as project asset dependencies. Exact installed versions and file hashes are recorded in \`.drawcall/market-lock.json\`.
64
+
65
+ \`list\` is offline: it reads \`.drawcall/market-lock.json\` from the nearest package root and prints exact installed names, versions, types, and installed file paths.
66
+
56
67
  If search returns no results, try one broader noun phrase. If a command returns \`Error: Not logged in...\`, ask before running \`market login\`.
57
68
  `;
58
69
  //# sourceMappingURL=skill.js.map
package/dist/skill.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"skill.js","sourceRoot":"","sources":["../src/skill.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,WAAW,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAwD1B,CAAA"}
1
+ {"version":3,"file":"skill.js","sourceRoot":"","sources":["../src/skill.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,WAAW,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAmE1B,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@drawcall/market",
3
- "version": "0.1.35",
3
+ "version": "0.1.37",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/drawcall-ai/market",
@@ -31,7 +31,7 @@
31
31
  "scripts": {
32
32
  "build": "tsc",
33
33
  "dev": "tsx src/cli.ts",
34
- "test:install-layout": "tsx --test tests/install-layout.test.ts tests/install-command.test.ts",
34
+ "test:install-layout": "tsx --test tests/install-layout.test.ts tests/install-command.test.ts tests/list-command.test.ts",
35
35
  "typecheck": "tsc --noEmit"
36
36
  },
37
37
  "dependencies": {
@@ -1,4 +1,5 @@
1
1
  import type {
2
+ AssetInstallMetadata,
2
3
  AssetSearchResult,
3
4
  AssetVersion,
4
5
  GenerateAssetResult,
@@ -6,6 +7,12 @@ import type {
6
7
  } from './contract.js'
7
8
  import type { AssetType } from './schemas.js'
8
9
 
10
+ export const ASSET_RELEVANCE_SCORE: unique symbol = Symbol('assetRelevanceScore')
11
+
12
+ export interface RankedAssetSearchResult extends AssetSearchResult {
13
+ [ASSET_RELEVANCE_SCORE]?: number
14
+ }
15
+
9
16
  export interface AssetSearchInput {
10
17
  page: number
11
18
  limit: number
@@ -45,12 +52,14 @@ export interface AssetGenerateInput {
45
52
 
46
53
  export interface AssetProvider {
47
54
  type: AssetType
48
- /**
49
- * Shown to the user after an asset of this type is installed. Use it for
50
- * type-specific guidance (where files landed, how to use them). Surfaced to
51
- * the CLI through the `asset.postInstallMessages` contract procedure.
52
- */
53
- postInstallMessage?: string
55
+ /** Shown to the user after an asset of this type is installed. */
56
+ installMessage?: string
57
+ /** Whether a top-level install should save this asset to package.json assetDependencies. */
58
+ saveOnInstall?: boolean
59
+ /** Whether upload should read package.json assetDependencies from the source zip. */
60
+ readAssetDependenciesFromPackageJson?: boolean
61
+ /** Whether upload should omit unchanged installed dependency files from the source zip. */
62
+ omitUnchangedInstalledFilesOnUpload?: boolean
54
63
  search(input: AssetSearchInput): Promise<PaginatedList<AssetSearchResult>>
55
64
  exact(input: AssetExactInput): Promise<AssetSearchResult | null>
56
65
  uploadZip(input: AssetUploadZipInput, owner: { id: string; role: string }): Promise<AssetVersion>
@@ -59,3 +68,28 @@ export interface AssetProvider {
59
68
  }
60
69
 
61
70
  export type AssetImplementation = AssetProvider
71
+
72
+ export function assetRelevanceScore(asset: AssetSearchResult): number | undefined {
73
+ return (asset as RankedAssetSearchResult)[ASSET_RELEVANCE_SCORE]
74
+ }
75
+
76
+ export function installMetadataForProvider(provider: AssetProvider): AssetInstallMetadata {
77
+ return {
78
+ installMessage: provider.installMessage,
79
+ saveOnInstall: provider.saveOnInstall ?? true,
80
+ readAssetDependenciesFromPackageJson: provider.readAssetDependenciesFromPackageJson ?? false,
81
+ omitUnchangedInstalledFilesOnUpload: provider.omitUnchangedInstalledFilesOnUpload ?? false,
82
+ }
83
+ }
84
+
85
+ export function withAssetRelevanceScore(
86
+ asset: AssetSearchResult,
87
+ score: number,
88
+ ): RankedAssetSearchResult {
89
+ const ranked = { ...asset } as RankedAssetSearchResult
90
+ Object.defineProperty(ranked, ASSET_RELEVANCE_SCORE, {
91
+ value: score,
92
+ enumerable: false,
93
+ })
94
+ return ranked
95
+ }
package/src/cli.ts CHANGED
@@ -9,6 +9,7 @@ import { ASSET_TYPES, type AssetType } from './schemas.js'
9
9
  import { installCommand } from './commands/install.js'
10
10
  import { searchCommand } from './commands/search.js'
11
11
  import { generateCommand } from './commands/generate.js'
12
+ import { listCommand } from './commands/list.js'
12
13
  import { previewCommand } from './commands/preview.js'
13
14
  import { uploadCommand } from './commands/upload.js'
14
15
  import { logout } from './commands/logout.js'
@@ -75,15 +76,32 @@ program
75
76
 
76
77
  program
77
78
  .command('install')
78
- .description('Install assets by name')
79
- .argument('<assets...>', 'Asset names, optionally with @range')
79
+ .description('Install assets by name, or package.json assetDependencies when no names are given')
80
+ .argument('[assets...]', 'Asset names, optionally with @range')
80
81
  .addOption(apiOption)
81
82
  .option('--unapproved', 'Include unapproved versions', false)
83
+ .option('--force', 'Overwrite existing files', false)
82
84
  .option('--cwd <dir>', 'Project directory')
83
- .action(async (args: string[], opts: { api?: string; unapproved: boolean; cwd?: string }) => {
84
- await installCommand(args, {
85
- baseUrl: opts.api,
86
- unapproved: opts.unapproved,
85
+ .action(
86
+ async (
87
+ args: string[],
88
+ opts: { api?: string; unapproved: boolean; force: boolean; cwd?: string },
89
+ ) => {
90
+ await installCommand(args, {
91
+ baseUrl: opts.api,
92
+ unapproved: opts.unapproved,
93
+ force: opts.force,
94
+ cwd: opts.cwd,
95
+ })
96
+ },
97
+ )
98
+
99
+ program
100
+ .command('list')
101
+ .description('List locally installed assets')
102
+ .option('--cwd <dir>', 'Project directory')
103
+ .action(async (opts: { cwd?: string }) => {
104
+ await listCommand({
87
105
  cwd: opts.cwd,
88
106
  })
89
107
  })
@@ -1,6 +1,9 @@
1
+ import * as fs from 'fs/promises'
2
+ import * as path from 'path'
1
3
  import ora from 'ora'
2
4
  import { resolve } from '../resolve.js'
3
- import { install as runInstall } from '../install.js'
5
+ import { findInstallRoot, install as runInstall, type InstallRootRequest } from '../install.js'
6
+ import { packageJsonAssetDependencies } from '../package-json.js'
4
7
  import { assetNameSchema } from '../schemas.js'
5
8
  import { getCliClient } from '../cli-client.js'
6
9
  import { installResult } from '../output.js'
@@ -10,42 +13,41 @@ export interface InstallCommandOptions {
10
13
  unapproved?: boolean
11
14
  cwd?: string
12
15
  baseUrl?: string
16
+ force?: boolean
13
17
  }
14
18
 
15
- interface AssetRequest {
16
- name: string
17
- range: string
18
- }
19
+ type AssetRequest = InstallRootRequest
19
20
 
20
21
  export async function installCommand(args: string[], opts: InstallCommandOptions): Promise<void> {
21
22
  const { client } = await getCliClient({ baseUrl: opts.baseUrl })
22
23
  const includeUnapproved = opts.unapproved ?? false
24
+ const metadata = await client.asset.installMetadata()
23
25
 
24
26
  const spinner = ora({
25
27
  isEnabled: Boolean(process.stderr.isTTY),
26
28
  isSilent: !process.stderr.isTTY,
27
29
  }).start()
28
30
  try {
29
- const requests: AssetRequest[] = []
30
- for (const arg of args) {
31
- spinner.text = `Resolving "${truncate(arg, 40)}"`
32
- requests.push(await resolveArg(client, arg, includeUnapproved))
33
- }
31
+ const requests =
32
+ args.length > 0
33
+ ? await resolveArgs(client, args, includeUnapproved, spinner)
34
+ : await packageJsonRequests(opts.cwd)
34
35
 
35
36
  spinner.text = 'Resolving dependency tree'
36
37
  const resolution = await resolve(client.asset, requests, { includeUnapproved })
37
38
 
38
39
  const result = await runInstall(client, resolution, {
39
40
  cwd: opts.cwd,
41
+ force: opts.force ?? false,
42
+ rootRequests: requests,
43
+ installMetadata: metadata,
40
44
  onProgress: (msg) => {
41
45
  spinner.text = msg
42
46
  },
43
47
  })
44
48
 
45
- const messages = await client.asset.postInstallMessages()
46
-
47
49
  spinner.stop()
48
- console.log(installResult(result, messages))
50
+ console.log(installResult(result, metadata))
49
51
  } catch (err) {
50
52
  spinner.stop()
51
53
  throw err
@@ -72,7 +74,14 @@ export async function resolveArg(
72
74
  }
73
75
 
74
76
  const existing = await client.asset.exact({ name: parsed.name, includeUnapproved })
75
- if (existing) return parsed
77
+ if (existing) {
78
+ return {
79
+ name: parsed.name,
80
+ range: parsed.range,
81
+ saveRange: parsed.explicitRange ? parsed.range : `^${existing.latestVersion}`,
82
+ save: true,
83
+ }
84
+ }
76
85
 
77
86
  if (!includeUnapproved) {
78
87
  const hidden = await client.asset.exact({ name: parsed.name, includeUnapproved: true })
@@ -89,11 +98,62 @@ export async function resolveArg(
89
98
  )
90
99
  }
91
100
 
92
- function parseNameAndRange(arg: string): AssetRequest | null {
101
+ async function resolveArgs(
102
+ client: MarketClient,
103
+ args: string[],
104
+ includeUnapproved: boolean,
105
+ spinner: { text: string },
106
+ ): Promise<AssetRequest[]> {
107
+ const requests: AssetRequest[] = []
108
+ for (const arg of args) {
109
+ spinner.text = `Resolving "${truncate(arg, 40)}"`
110
+ requests.push(await resolveArg(client, arg, includeUnapproved))
111
+ }
112
+ return requests
113
+ }
114
+
115
+ async function packageJsonRequests(cwd: string | undefined): Promise<AssetRequest[]> {
116
+ const installRoot = await findInstallRoot(cwd ?? process.cwd())
117
+ const pkgPath = path.join(installRoot, 'package.json')
118
+ const dependencies = await readPackageJsonAssetDependencies(pkgPath)
119
+ const requests = Object.entries(dependencies).map(([name, range]) => ({
120
+ name,
121
+ range,
122
+ saveRange: range,
123
+ save: false,
124
+ }))
125
+
126
+ if (requests.length === 0) {
127
+ throw new Error(
128
+ 'No assets specified and package.json has no assetDependencies. Pass asset names or add assetDependencies.',
129
+ )
130
+ }
131
+
132
+ return requests
133
+ }
134
+
135
+ async function readPackageJsonAssetDependencies(pkgPath: string): Promise<Record<string, string>> {
136
+ try {
137
+ return packageJsonAssetDependencies(await fs.readFile(pkgPath, 'utf-8'), pkgPath)
138
+ } catch (error) {
139
+ if (isMissingFile(error)) return {}
140
+ throw error
141
+ }
142
+ }
143
+
144
+ function isMissingFile(error: unknown): boolean {
145
+ return error instanceof Error && 'code' in error && error.code === 'ENOENT'
146
+ }
147
+
148
+ function parseNameAndRange(arg: string):
149
+ | (Pick<AssetRequest, 'name' | 'range'> & {
150
+ explicitRange: boolean
151
+ })
152
+ | null {
93
153
  const atIdx = arg.indexOf('@')
94
154
  const name = atIdx >= 0 ? arg.slice(0, atIdx) : arg
95
155
  const range = atIdx >= 0 ? arg.slice(atIdx + 1) : '*'
96
- return assetNameSchema.safeParse(name).success ? { name, range } : null
156
+ return assetNameSchema.safeParse(name).success ? { name, range, explicitRange: atIdx >= 0 } : null
97
157
  }
98
158
 
99
159
  function truncate(s: string, max: number): string {
@@ -0,0 +1,47 @@
1
+ import * as path from 'path'
2
+ import { findInstallRoot } from '../install.js'
3
+ import { MARKET_LOCK_PATH, readMarketLock } from '../market-lock.js'
4
+ import { listResult } from '../output.js'
5
+
6
+ export interface ListCommandOptions {
7
+ cwd?: string
8
+ }
9
+
10
+ export interface InstalledAssetListing {
11
+ name: string
12
+ version: string
13
+ type: string
14
+ files: string[]
15
+ }
16
+
17
+ export interface ListInstalledAssetsResult {
18
+ projectRoot: string
19
+ lockPath: string
20
+ assets: InstalledAssetListing[]
21
+ }
22
+
23
+ export async function listCommand(opts: ListCommandOptions): Promise<void> {
24
+ const result = await listInstalledAssets(opts.cwd)
25
+ console.log(listResult(result))
26
+ }
27
+
28
+ export async function listInstalledAssets(
29
+ cwd: string | undefined,
30
+ ): Promise<ListInstalledAssetsResult> {
31
+ const projectRoot = await findInstallRoot(cwd ?? process.cwd())
32
+ const lock = await readMarketLock(projectRoot)
33
+ const assets = Object.entries(lock.assets)
34
+ .map(([name, asset]) => ({
35
+ name,
36
+ version: asset.version,
37
+ type: asset.type,
38
+ files: Object.keys(asset.files).sort((a, b) => a.localeCompare(b)),
39
+ }))
40
+ .sort((a, b) => a.name.localeCompare(b.name))
41
+
42
+ return {
43
+ projectRoot,
44
+ lockPath: path.join(projectRoot, MARKET_LOCK_PATH),
45
+ assets,
46
+ }
47
+ }
@@ -1,8 +1,12 @@
1
1
  import * as fs from 'fs/promises'
2
2
  import * as path from 'path'
3
+ import { unzipSync, zipSync } from 'fflate'
3
4
  import ora from 'ora'
4
5
  import semver from 'semver'
5
6
  import { getCliClient } from '../cli-client.js'
7
+ import { findInstallRoot } from '../install.js'
8
+ import { readMarketLock, sha256 } from '../market-lock.js'
9
+ import { packageJsonAssetDependenciesFromFiles } from '../package-json.js'
6
10
  import { MAX_ASSET_DESCRIPTION_LENGTH, MAX_UPLOAD_ZIP_SIZE_BYTES } from '../schemas.js'
7
11
  import { unchangedUploadResult, uploadResult } from '../output.js'
8
12
  import {
@@ -43,7 +47,7 @@ export async function uploadCommand(
43
47
  const parsedDescription = parseUploadDescription(description)
44
48
  // Parse dep flags before any network work so a malformed spec fails fast.
45
49
  const npmDependencies = parseVersionedDeps(opts.npm ?? [], 'npm')
46
- const assetDependencies = parseVersionedDeps(opts.asset ?? [], 'asset')
50
+ const explicitAssetDependencies = parseVersionedDeps(opts.asset ?? [], 'asset')
47
51
  const skillDependencies = parseSkillDeps(opts.skill ?? [])
48
52
  const cwd = opts.cwd ?? process.cwd()
49
53
  const type = opts.type
@@ -52,8 +56,21 @@ export async function uploadCommand(
52
56
  if (zipStat.size >= MAX_UPLOAD_ZIP_SIZE_BYTES) {
53
57
  throw new Error('Upload zip must be smaller than 1 GB')
54
58
  }
55
- const zip = new Uint8Array(await fs.readFile(zipFile))
59
+ const sourceZip = new Uint8Array(await fs.readFile(zipFile))
56
60
  const { client } = await getCliClient({ baseUrl: opts.baseUrl, requireAuth: true })
61
+ const installMetadata = await client.asset.installMetadata()
62
+ const typeMetadata = installMetadata[type]
63
+ const sourceFiles = unzipSync(sourceZip)
64
+ const packageJsonAssetDependencies = typeMetadata?.readAssetDependenciesFromPackageJson
65
+ ? packageJsonAssetDependenciesFromFiles(sourceFiles)
66
+ : {}
67
+ const assetDependencies = mergeAssetDependencies(
68
+ packageJsonAssetDependencies,
69
+ explicitAssetDependencies,
70
+ )
71
+ const zip = typeMetadata?.omitUnchangedInstalledFilesOnUpload
72
+ ? await omitUnchangedInstalledFiles(sourceZip, sourceFiles, assetDependencies, cwd)
73
+ : sourceZip
57
74
  const profile = await client.user.getProfile()
58
75
  if (!profile) {
59
76
  throw new Error('Not logged in. Run `market login` first.')
@@ -168,6 +185,71 @@ export function parseSkillDeps(specs: string[]): Record<string, string> {
168
185
  return out
169
186
  }
170
187
 
188
+ function mergeAssetDependencies(
189
+ fromPackageJson: Record<string, string>,
190
+ explicit: Record<string, string>,
191
+ ): Record<string, string> {
192
+ const merged = { ...fromPackageJson }
193
+ for (const [name, range] of Object.entries(explicit)) {
194
+ if (name in merged && merged[name] !== range) {
195
+ throw new Error(
196
+ `Conflicting asset dependency "${name}": package.json has ${merged[name]}, --asset has ${range}`,
197
+ )
198
+ }
199
+ merged[name] = range
200
+ }
201
+ return merged
202
+ }
203
+
204
+ async function omitUnchangedInstalledFiles(
205
+ sourceZip: Uint8Array,
206
+ sourceFiles: Record<string, Uint8Array>,
207
+ assetDependencies: Record<string, string>,
208
+ cwd: string,
209
+ ): Promise<Uint8Array> {
210
+ const dependencyNames = new Set(Object.keys(assetDependencies))
211
+ if (dependencyNames.size === 0) return sourceZip
212
+
213
+ const installRoot = await findInstallRoot(cwd)
214
+ const lock = await readMarketLock(installRoot)
215
+ const hashesByPath = new Map<string, string>()
216
+
217
+ for (const [name, asset] of Object.entries(lock.assets)) {
218
+ if (!dependencyNames.has(name)) continue
219
+ for (const [file, metadata] of Object.entries(asset.files)) {
220
+ hashesByPath.set(file, metadata.sha256)
221
+ }
222
+ }
223
+
224
+ if (hashesByPath.size === 0) return sourceZip
225
+
226
+ let omitted = false
227
+ const filtered: Record<string, Uint8Array> = {}
228
+ for (const [file, content] of Object.entries(sourceFiles)) {
229
+ const normalizedPath = normalizedZipPath(file)
230
+ const lockedHash = normalizedPath ? hashesByPath.get(normalizedPath) : undefined
231
+ if (lockedHash && lockedHash === sha256(content)) {
232
+ omitted = true
233
+ continue
234
+ }
235
+ filtered[file] = content
236
+ }
237
+
238
+ return omitted ? zipSync(filtered) : sourceZip
239
+ }
240
+
241
+ function normalizedZipPath(file: string): string | null {
242
+ const zipPath = file.replace(/\\/g, '/')
243
+ if (
244
+ zipPath.split('/').includes('..') ||
245
+ path.posix.isAbsolute(zipPath) ||
246
+ path.win32.isAbsolute(zipPath)
247
+ ) {
248
+ return null
249
+ }
250
+ return path.posix.normalize(zipPath)
251
+ }
252
+
171
253
  /** Compare a stored dependency JSON string against a freshly parsed record. */
172
254
  function depsEqual(storedJson: string, next: Record<string, string>): boolean {
173
255
  let stored: Record<string, string>
package/src/contract.ts CHANGED
@@ -19,6 +19,7 @@ export interface AssetVersion {
19
19
  assetDependencies: string
20
20
  skillDependencies: string
21
21
  sourceKey: string
22
+ sourceSizeBytes: number | null
22
23
  createdAt: Date
23
24
  }
24
25
 
@@ -45,6 +46,7 @@ export interface AssetSearchResult {
45
46
  npmDependencies: string
46
47
  assetDependencies: string
47
48
  skillDependencies: string
49
+ sourceSizeBytes: number | null
48
50
  /** Public URL of the preview image, or null for types without one. */
49
51
  previewUrl: string | null
50
52
  }
@@ -74,6 +76,13 @@ export interface GenerateAssetResult {
74
76
  version: string
75
77
  }
76
78
 
79
+ export interface AssetInstallMetadata {
80
+ installMessage?: string
81
+ saveOnInstall: boolean
82
+ readAssetDependenciesFromPackageJson: boolean
83
+ omitUnchangedInstalledFilesOnUpload: boolean
84
+ }
85
+
77
86
  export const contract = {
78
87
  asset: {
79
88
  search: oc.input(listAssetsSchema).output(z.custom<PaginatedList<AssetSearchResult>>()),
@@ -95,7 +104,7 @@ export const contract = {
95
104
 
96
105
  generate: oc.input(generateAssetSchema).output(z.custom<GenerateAssetResult>()),
97
106
 
98
- postInstallMessages: oc.output(z.custom<Record<string, string>>()),
107
+ installMetadata: oc.output(z.custom<Record<string, AssetInstallMetadata>>()),
99
108
  },
100
109
 
101
110
  user: {
package/src/index.ts CHANGED
@@ -9,6 +9,7 @@ export type { AppContract } from './contract.js'
9
9
  // Contract output types
10
10
  export type {
11
11
  Asset,
12
+ AssetInstallMetadata,
12
13
  AssetVersion,
13
14
  AssetSearchResult,
14
15
  PaginatedList,
@@ -22,9 +23,16 @@ export type {
22
23
  AssetGenerateInput,
23
24
  AssetImplementation,
24
25
  AssetProvider,
26
+ RankedAssetSearchResult,
25
27
  AssetSearchInput,
26
28
  AssetUploadZipInput,
27
29
  } from './asset-implementation.js'
30
+ export {
31
+ ASSET_RELEVANCE_SCORE,
32
+ assetRelevanceScore,
33
+ installMetadataForProvider,
34
+ withAssetRelevanceScore,
35
+ } from './asset-implementation.js'
28
36
 
29
37
  // Resolve
30
38
  export { resolve, ResolutionError } from './resolve.js'
@@ -55,3 +63,10 @@ export type { AssetType } from './schemas.js'
55
63
 
56
64
  // Constants
57
65
  export { MAX_FILE_SIZE, ALLOWED_EXTENSIONS, ASSET_TYPE_LABELS } from './constants.js'
66
+
67
+ export {
68
+ packageJsonAssetDependencies,
69
+ packageJsonAssetDependenciesFromFiles,
70
+ parsePackageJson,
71
+ } from './package-json.js'
72
+ export type { PackageJson } from './package-json.js'