@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.
- package/README.md +6 -3
- package/dist/asset-implementation.d.ts +16 -7
- package/dist/asset-implementation.d.ts.map +1 -1
- package/dist/asset-implementation.js +20 -1
- package/dist/asset-implementation.js.map +1 -1
- package/dist/cli.js +14 -2
- package/dist/cli.js.map +1 -1
- package/dist/commands/install.d.ts +3 -4
- package/dist/commands/install.d.ts.map +1 -1
- package/dist/commands/install.js +57 -11
- package/dist/commands/install.js.map +1 -1
- package/dist/commands/list.d.ts +17 -0
- package/dist/commands/list.d.ts.map +1 -0
- package/dist/commands/list.js +26 -0
- package/dist/commands/list.js.map +1 -0
- package/dist/commands/upload.d.ts.map +1 -1
- package/dist/commands/upload.js +64 -2
- package/dist/commands/upload.js.map +1 -1
- package/dist/contract.d.ts +9 -1
- package/dist/contract.d.ts.map +1 -1
- package/dist/contract.js +1 -1
- package/dist/contract.js.map +1 -1
- package/dist/index.d.ts +5 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/install.d.ts +14 -0
- package/dist/install.d.ts.map +1 -1
- package/dist/install.js +133 -23
- package/dist/install.js.map +1 -1
- package/dist/market-lock.d.ts +17 -0
- package/dist/market-lock.d.ts.map +1 -0
- package/dist/market-lock.js +44 -0
- package/dist/market-lock.js.map +1 -0
- package/dist/output.d.ts +4 -2
- package/dist/output.d.ts.map +1 -1
- package/dist/output.js +28 -7
- package/dist/output.js.map +1 -1
- package/dist/package-json.d.ts +11 -0
- package/dist/package-json.d.ts.map +1 -0
- package/dist/package-json.js +23 -0
- package/dist/package-json.js.map +1 -0
- package/dist/skill.d.ts +1 -1
- package/dist/skill.d.ts.map +1 -1
- package/dist/skill.js +20 -9
- package/dist/skill.js.map +1 -1
- package/package.json +2 -2
- package/src/asset-implementation.ts +40 -6
- package/src/cli.ts +24 -6
- package/src/commands/install.ts +76 -16
- package/src/commands/list.ts +47 -0
- package/src/commands/upload.ts +84 -2
- package/src/contract.ts +10 -1
- package/src/index.ts +15 -0
- package/src/install.ts +197 -35
- package/src/market-lock.ts +65 -0
- package/src/output.ts +36 -8
- package/src/package-json.ts +37 -0
- package/src/skill.ts +20 -9
- package/tests/install-command.test.ts +23 -1
- package/tests/install-layout.test.ts +117 -0
- package/tests/list-command.test.ts +76 -0
- 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`.\
|
|
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
|
package/dist/skill.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"skill.d.ts","sourceRoot":"","sources":["../src/skill.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,WAAW,
|
|
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.
|
|
21
|
-
2.
|
|
22
|
-
3.
|
|
23
|
-
4. \`
|
|
24
|
-
5.
|
|
25
|
-
6.
|
|
26
|
-
7.
|
|
27
|
-
8.
|
|
28
|
-
9. Installed \`
|
|
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
|
|
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.
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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('
|
|
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(
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
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
|
})
|
package/src/commands/install.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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,
|
|
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)
|
|
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
|
|
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
|
+
}
|
package/src/commands/upload.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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'
|