@drawcall/market 0.1.36 → 0.1.38
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 +4 -1
- package/dist/asset-implementation.d.ts +6 -0
- package/dist/asset-implementation.d.ts.map +1 -1
- package/dist/asset-implementation.js +12 -0
- package/dist/asset-implementation.js.map +1 -1
- package/dist/cli.js +10 -0
- package/dist/cli.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/contract.d.ts +2 -0
- package/dist/contract.d.ts.map +1 -1
- package/dist/contract.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/output.d.ts +2 -0
- package/dist/output.d.ts.map +1 -1
- package/dist/output.js +23 -5
- package/dist/output.js.map +1 -1
- package/dist/skill.d.ts +1 -1
- package/dist/skill.d.ts.map +1 -1
- package/dist/skill.js +18 -9
- package/dist/skill.js.map +1 -1
- package/package.json +7 -2
- package/skills/market/SKILL.md +69 -0
- package/src/asset-implementation.ts +22 -0
- package/src/cli.ts +11 -0
- package/src/commands/list.ts +47 -0
- package/src/contract.ts +2 -0
- package/src/index.ts +7 -1
- package/src/output.ts +29 -5
- package/src/skill.ts +18 -9
- package/tests/install-command.test.ts +0 -67
- package/tests/install-layout.test.ts +0 -307
- package/tests/output.test.ts +0 -186
- package/tests/resolve-skills.test.ts +0 -87
- package/tests/upload-deps.test.ts +0 -63
- package/tsconfig.json +0 -9
|
@@ -7,6 +7,12 @@ import type {
|
|
|
7
7
|
} from './contract.js'
|
|
8
8
|
import type { AssetType } from './schemas.js'
|
|
9
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
|
+
|
|
10
16
|
export interface AssetSearchInput {
|
|
11
17
|
page: number
|
|
12
18
|
limit: number
|
|
@@ -63,6 +69,10 @@ export interface AssetProvider {
|
|
|
63
69
|
|
|
64
70
|
export type AssetImplementation = AssetProvider
|
|
65
71
|
|
|
72
|
+
export function assetRelevanceScore(asset: AssetSearchResult): number | undefined {
|
|
73
|
+
return (asset as RankedAssetSearchResult)[ASSET_RELEVANCE_SCORE]
|
|
74
|
+
}
|
|
75
|
+
|
|
66
76
|
export function installMetadataForProvider(provider: AssetProvider): AssetInstallMetadata {
|
|
67
77
|
return {
|
|
68
78
|
installMessage: provider.installMessage,
|
|
@@ -71,3 +81,15 @@ export function installMetadataForProvider(provider: AssetProvider): AssetInstal
|
|
|
71
81
|
omitUnchangedInstalledFilesOnUpload: provider.omitUnchangedInstalledFilesOnUpload ?? false,
|
|
72
82
|
}
|
|
73
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'
|
|
@@ -95,6 +96,16 @@ program
|
|
|
95
96
|
},
|
|
96
97
|
)
|
|
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({
|
|
105
|
+
cwd: opts.cwd,
|
|
106
|
+
})
|
|
107
|
+
})
|
|
108
|
+
|
|
98
109
|
program
|
|
99
110
|
.command('search')
|
|
100
111
|
.description('Find assets')
|
|
@@ -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/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
|
}
|
package/src/index.ts
CHANGED
|
@@ -23,10 +23,16 @@ export type {
|
|
|
23
23
|
AssetGenerateInput,
|
|
24
24
|
AssetImplementation,
|
|
25
25
|
AssetProvider,
|
|
26
|
+
RankedAssetSearchResult,
|
|
26
27
|
AssetSearchInput,
|
|
27
28
|
AssetUploadZipInput,
|
|
28
29
|
} from './asset-implementation.js'
|
|
29
|
-
export {
|
|
30
|
+
export {
|
|
31
|
+
ASSET_RELEVANCE_SCORE,
|
|
32
|
+
assetRelevanceScore,
|
|
33
|
+
installMetadataForProvider,
|
|
34
|
+
withAssetRelevanceScore,
|
|
35
|
+
} from './asset-implementation.js'
|
|
30
36
|
|
|
31
37
|
// Resolve
|
|
32
38
|
export { resolve, ResolutionError } from './resolve.js'
|
package/src/output.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AssetInstallMetadata, AssetSearchResult } from './contract.js'
|
|
2
|
+
import type { ListInstalledAssetsResult } from './commands/list.js'
|
|
2
3
|
import type { InstallResult } from './install.js'
|
|
3
4
|
|
|
4
5
|
export function assetVersionRef(name: string, version?: string): string {
|
|
@@ -40,14 +41,11 @@ export function installResult(
|
|
|
40
41
|
const lines = ['Installed:']
|
|
41
42
|
|
|
42
43
|
for (const asset of result.assets) {
|
|
43
|
-
|
|
44
|
-
lines.push(`- ${assetVersionRef(asset.name, asset.version)} (${asset.type})`)
|
|
44
|
+
lines.push(assetLine(asset))
|
|
45
45
|
if (asset.description) {
|
|
46
46
|
lines.push(...block(' description: ', asset.description))
|
|
47
47
|
}
|
|
48
|
-
|
|
49
|
-
lines.push(' files:', ...indent(fileTree(files), ' '))
|
|
50
|
-
}
|
|
48
|
+
lines.push(...assetFileLines(asset.files))
|
|
51
49
|
const message = installMetadata[asset.type]?.installMessage
|
|
52
50
|
if (message) {
|
|
53
51
|
lines.push(...block(' note: ', message))
|
|
@@ -78,6 +76,32 @@ export function installResult(
|
|
|
78
76
|
return lines.join('\n')
|
|
79
77
|
}
|
|
80
78
|
|
|
79
|
+
export function listResult(result: ListInstalledAssetsResult): string {
|
|
80
|
+
if (result.assets.length === 0) {
|
|
81
|
+
return 'No installed assets found.'
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const lines = [`Installed assets: ${result.assets.length}`]
|
|
85
|
+
for (const asset of result.assets) {
|
|
86
|
+
lines.push(assetLine(asset), ...assetFileLines(asset.files))
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return lines.join('\n')
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function assetLine(asset: { name: string; version: string; type: string }): string {
|
|
93
|
+
return `- ${assetVersionRef(asset.name, asset.version)} (${asset.type})`
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function assetFileLines(paths: string[]): string[] {
|
|
97
|
+
const lines: string[] = []
|
|
98
|
+
const files = unique(paths)
|
|
99
|
+
if (files.length > 0) {
|
|
100
|
+
lines.push(' files:', ...indent(fileTree(files), ' '))
|
|
101
|
+
}
|
|
102
|
+
return lines
|
|
103
|
+
}
|
|
104
|
+
|
|
81
105
|
function block(label: string, message: string, width = 68): string[] {
|
|
82
106
|
const hang = ' '.repeat(label.length)
|
|
83
107
|
const words = normalizeInline(message).split(' ').filter(Boolean)
|
package/src/skill.ts
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,6 +50,11 @@ 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
|
|
|
@@ -55,5 +62,7 @@ Assets may also declare \`skill\` dependencies, installed for you via the \`skil
|
|
|
55
62
|
|
|
56
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\`.
|
|
57
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
|
+
|
|
58
67
|
If search returns no results, try one broader noun phrase. If a command returns \`Error: Not logged in...\`, ask before running \`market login\`.
|
|
59
68
|
`
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
import assert from 'node:assert/strict'
|
|
2
|
-
import test from 'node:test'
|
|
3
|
-
import { resolveArg } from '../src/commands/install.js'
|
|
4
|
-
|
|
5
|
-
test('resolveArg reports unapproved explicit assets clearly', async () => {
|
|
6
|
-
const client = {
|
|
7
|
-
asset: {
|
|
8
|
-
exact: async ({ includeUnapproved }: { name: string; includeUnapproved: boolean }) =>
|
|
9
|
-
includeUnapproved
|
|
10
|
-
? {
|
|
11
|
-
name: 'qwantani-moon-noon-puresky',
|
|
12
|
-
latestVersion: '1.0.2',
|
|
13
|
-
approved: false,
|
|
14
|
-
}
|
|
15
|
-
: null,
|
|
16
|
-
},
|
|
17
|
-
} as never
|
|
18
|
-
|
|
19
|
-
await assert.rejects(
|
|
20
|
-
resolveArg(client, 'qwantani-moon-noon-puresky@1.0.2', false),
|
|
21
|
-
/has no approved versions/u,
|
|
22
|
-
)
|
|
23
|
-
})
|
|
24
|
-
|
|
25
|
-
test('resolveArg keeps explicit versioned refs exact when the asset exists', async () => {
|
|
26
|
-
const client = {
|
|
27
|
-
asset: {
|
|
28
|
-
exact: async () => ({ name: 'fresh-sky', latestVersion: '1.2.3', approved: true }),
|
|
29
|
-
},
|
|
30
|
-
} as never
|
|
31
|
-
|
|
32
|
-
const request = await resolveArg(client, 'fresh-sky@1.2.3', false)
|
|
33
|
-
|
|
34
|
-
assert.deepEqual(request, {
|
|
35
|
-
name: 'fresh-sky',
|
|
36
|
-
range: '1.2.3',
|
|
37
|
-
saveRange: '1.2.3',
|
|
38
|
-
save: true,
|
|
39
|
-
})
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
test('resolveArg saves an implicit range from the selected latest version', async () => {
|
|
43
|
-
const client = {
|
|
44
|
-
asset: {
|
|
45
|
-
exact: async () => ({ name: 'fresh-sky', latestVersion: '1.2.3', approved: true }),
|
|
46
|
-
},
|
|
47
|
-
} as never
|
|
48
|
-
|
|
49
|
-
const request = await resolveArg(client, 'fresh-sky', false)
|
|
50
|
-
|
|
51
|
-
assert.deepEqual(request, {
|
|
52
|
-
name: 'fresh-sky',
|
|
53
|
-
range: '*',
|
|
54
|
-
saveRange: '^1.2.3',
|
|
55
|
-
save: true,
|
|
56
|
-
})
|
|
57
|
-
})
|
|
58
|
-
|
|
59
|
-
test('resolveArg errors when the asset does not exist instead of deferring', async () => {
|
|
60
|
-
const client = {
|
|
61
|
-
asset: {
|
|
62
|
-
exact: async () => null,
|
|
63
|
-
},
|
|
64
|
-
} as never
|
|
65
|
-
|
|
66
|
-
await assert.rejects(resolveArg(client, 'does-not-exist@1.2.3', false), /not found/u)
|
|
67
|
-
})
|
|
@@ -1,307 +0,0 @@
|
|
|
1
|
-
import assert from 'node:assert/strict'
|
|
2
|
-
import * as fs from 'node:fs/promises'
|
|
3
|
-
import * as os from 'node:os'
|
|
4
|
-
import * as path from 'node:path'
|
|
5
|
-
import test from 'node:test'
|
|
6
|
-
import { zipSync } from 'fflate'
|
|
7
|
-
import { findInstallRoot, install } from '../src/install.js'
|
|
8
|
-
import { MARKET_LOCK_PATH } from '../src/market-lock.js'
|
|
9
|
-
|
|
10
|
-
const textEncoder = new TextEncoder()
|
|
11
|
-
|
|
12
|
-
test('install writes zip files into the package root', async () => {
|
|
13
|
-
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'market-install-'))
|
|
14
|
-
const appRoot = path.join(tempDir, 'app')
|
|
15
|
-
const cwd = path.join(appRoot, 'src', 'feature')
|
|
16
|
-
await fs.mkdir(path.join(appRoot, 'public'), { recursive: true })
|
|
17
|
-
await fs.mkdir(cwd, { recursive: true })
|
|
18
|
-
await fs.writeFile(path.join(appRoot, 'package.json'), '{}\n')
|
|
19
|
-
await fs.writeFile(path.join(appRoot, 'README.md'), 'project readme\n')
|
|
20
|
-
|
|
21
|
-
const result = await install(
|
|
22
|
-
clientWithZip({
|
|
23
|
-
'public/humanoid-animation/idle-loop.glb': textEncoder.encode('glb'),
|
|
24
|
-
'src/generated/idle-loop.ts': textEncoder.encode('export const idleLoop = true\n'),
|
|
25
|
-
'README.md': textEncoder.encode('asset readme'),
|
|
26
|
-
}),
|
|
27
|
-
{
|
|
28
|
-
assets: [
|
|
29
|
-
{
|
|
30
|
-
name: 'idle-loop',
|
|
31
|
-
type: 'humanoid-animation',
|
|
32
|
-
version: '1.0.0',
|
|
33
|
-
description: 'Idle loop animation',
|
|
34
|
-
npmDependencies: {},
|
|
35
|
-
assetDependencies: {},
|
|
36
|
-
skillDependencies: {},
|
|
37
|
-
},
|
|
38
|
-
],
|
|
39
|
-
npmDependencies: {},
|
|
40
|
-
skillDependencies: {},
|
|
41
|
-
},
|
|
42
|
-
{ cwd },
|
|
43
|
-
)
|
|
44
|
-
|
|
45
|
-
assert.deepEqual(result, {
|
|
46
|
-
assets: [
|
|
47
|
-
{
|
|
48
|
-
name: 'idle-loop',
|
|
49
|
-
type: 'humanoid-animation',
|
|
50
|
-
version: '1.0.0',
|
|
51
|
-
description: 'Idle loop animation',
|
|
52
|
-
files: ['public/humanoid-animation/idle-loop.glb', 'src/generated/idle-loop.ts'],
|
|
53
|
-
},
|
|
54
|
-
],
|
|
55
|
-
npmDependencies: {},
|
|
56
|
-
skillDependencies: {},
|
|
57
|
-
warnings: [],
|
|
58
|
-
})
|
|
59
|
-
assert.equal(
|
|
60
|
-
await fs.readFile(path.join(appRoot, 'public', 'humanoid-animation', 'idle-loop.glb'), 'utf-8'),
|
|
61
|
-
'glb',
|
|
62
|
-
)
|
|
63
|
-
assert.equal(
|
|
64
|
-
await fs.readFile(path.join(appRoot, 'src', 'generated', 'idle-loop.ts'), 'utf-8'),
|
|
65
|
-
'export const idleLoop = true\n',
|
|
66
|
-
)
|
|
67
|
-
assert.equal(await fs.readFile(path.join(appRoot, 'README.md'), 'utf-8'), 'project readme\n')
|
|
68
|
-
assert.equal(await exists(path.join(cwd, 'src', 'idle-loop', 'public')), false)
|
|
69
|
-
|
|
70
|
-
const lock = JSON.parse(await fs.readFile(path.join(appRoot, MARKET_LOCK_PATH), 'utf-8')) as {
|
|
71
|
-
assets: Record<string, { version: string; files: Record<string, { sha256: string }> }>
|
|
72
|
-
}
|
|
73
|
-
assert.equal(lock.assets['idle-loop'].version, '1.0.0')
|
|
74
|
-
assert.equal(
|
|
75
|
-
typeof lock.assets['idle-loop'].files['public/humanoid-animation/idle-loop.glb'].sha256,
|
|
76
|
-
'string',
|
|
77
|
-
)
|
|
78
|
-
})
|
|
79
|
-
|
|
80
|
-
test('install rejects zip paths that escape through parent segments', async () => {
|
|
81
|
-
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'market-install-'))
|
|
82
|
-
await fs.writeFile(path.join(tempDir, 'package.json'), '{}\n')
|
|
83
|
-
|
|
84
|
-
await assert.rejects(
|
|
85
|
-
install(
|
|
86
|
-
clientWithZip({
|
|
87
|
-
'public/../package.json': textEncoder.encode('nope\n'),
|
|
88
|
-
}),
|
|
89
|
-
{
|
|
90
|
-
assets: [
|
|
91
|
-
{
|
|
92
|
-
name: 'bad-path',
|
|
93
|
-
type: 'template',
|
|
94
|
-
version: '1.0.0',
|
|
95
|
-
description: null,
|
|
96
|
-
npmDependencies: {},
|
|
97
|
-
assetDependencies: {},
|
|
98
|
-
skillDependencies: {},
|
|
99
|
-
},
|
|
100
|
-
],
|
|
101
|
-
npmDependencies: {},
|
|
102
|
-
skillDependencies: {},
|
|
103
|
-
},
|
|
104
|
-
{ cwd: tempDir },
|
|
105
|
-
),
|
|
106
|
-
/unsafe path/u,
|
|
107
|
-
)
|
|
108
|
-
})
|
|
109
|
-
|
|
110
|
-
test('install runs the skills runner per dependency, resolving local paths against the root', async () => {
|
|
111
|
-
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'market-skills-'))
|
|
112
|
-
const appRoot = path.join(tempDir, 'app')
|
|
113
|
-
const cwd = path.join(appRoot, 'src')
|
|
114
|
-
await fs.mkdir(cwd, { recursive: true })
|
|
115
|
-
await fs.writeFile(path.join(appRoot, 'package.json'), '{}\n')
|
|
116
|
-
|
|
117
|
-
const calls: Array<{ source: string; cwd: string }> = []
|
|
118
|
-
|
|
119
|
-
const result = await install(
|
|
120
|
-
clientWithZip({
|
|
121
|
-
'public/skills/local-skill/SKILL.md': textEncoder.encode('---\nname: local-skill\n---\n'),
|
|
122
|
-
}),
|
|
123
|
-
{
|
|
124
|
-
assets: [
|
|
125
|
-
{
|
|
126
|
-
name: 'with-skills',
|
|
127
|
-
type: 'template',
|
|
128
|
-
version: '1.0.0',
|
|
129
|
-
description: null,
|
|
130
|
-
npmDependencies: {},
|
|
131
|
-
assetDependencies: {},
|
|
132
|
-
skillDependencies: {},
|
|
133
|
-
},
|
|
134
|
-
],
|
|
135
|
-
npmDependencies: {},
|
|
136
|
-
skillDependencies: {
|
|
137
|
-
'web-design-guidelines': 'vercel-labs/agent-skills/tree/main/skills/web-design-guidelines',
|
|
138
|
-
'local-skill': './public/skills/local-skill',
|
|
139
|
-
},
|
|
140
|
-
},
|
|
141
|
-
{
|
|
142
|
-
cwd,
|
|
143
|
-
runSkillAdd: async (source, runnerCwd) => {
|
|
144
|
-
calls.push({ source, cwd: runnerCwd })
|
|
145
|
-
},
|
|
146
|
-
},
|
|
147
|
-
)
|
|
148
|
-
|
|
149
|
-
// Every runner call uses the install root (the dir holding package.json).
|
|
150
|
-
assert.deepEqual(new Set(calls.map((c) => c.cwd)), new Set([appRoot]))
|
|
151
|
-
|
|
152
|
-
const bySource = new Map(calls.map((c) => [c.source, c]))
|
|
153
|
-
// Remote refs are passed through untouched.
|
|
154
|
-
assert.ok(bySource.has('vercel-labs/agent-skills/tree/main/skills/web-design-guidelines'))
|
|
155
|
-
// Local paths are resolved against the install root, not the cwd.
|
|
156
|
-
assert.ok(bySource.has(path.join(appRoot, 'public', 'skills', 'local-skill')))
|
|
157
|
-
assert.equal(calls.length, 2)
|
|
158
|
-
|
|
159
|
-
assert.deepEqual(result.skillDependencies, {
|
|
160
|
-
'web-design-guidelines': 'vercel-labs/agent-skills/tree/main/skills/web-design-guidelines',
|
|
161
|
-
'local-skill': './public/skills/local-skill',
|
|
162
|
-
})
|
|
163
|
-
assert.deepEqual(result.warnings, [])
|
|
164
|
-
})
|
|
165
|
-
|
|
166
|
-
test('install saves top-level assets according to provider policy', async () => {
|
|
167
|
-
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'market-save-policy-'))
|
|
168
|
-
await fs.writeFile(path.join(tempDir, 'package.json'), '{}\n')
|
|
169
|
-
|
|
170
|
-
await install(
|
|
171
|
-
clientWithZip({
|
|
172
|
-
'public/model/chair.glb': textEncoder.encode('chair'),
|
|
173
|
-
'src/template.ts': textEncoder.encode('export {}\n'),
|
|
174
|
-
}),
|
|
175
|
-
{
|
|
176
|
-
assets: [
|
|
177
|
-
{
|
|
178
|
-
name: 'chair',
|
|
179
|
-
type: 'model',
|
|
180
|
-
version: '1.2.0',
|
|
181
|
-
description: null,
|
|
182
|
-
npmDependencies: {},
|
|
183
|
-
assetDependencies: {},
|
|
184
|
-
skillDependencies: {},
|
|
185
|
-
},
|
|
186
|
-
{
|
|
187
|
-
name: 'starter-template',
|
|
188
|
-
type: 'template',
|
|
189
|
-
version: '2.0.0',
|
|
190
|
-
description: null,
|
|
191
|
-
npmDependencies: {},
|
|
192
|
-
assetDependencies: {},
|
|
193
|
-
skillDependencies: {},
|
|
194
|
-
},
|
|
195
|
-
],
|
|
196
|
-
npmDependencies: {},
|
|
197
|
-
skillDependencies: {},
|
|
198
|
-
},
|
|
199
|
-
{
|
|
200
|
-
cwd: tempDir,
|
|
201
|
-
rootRequests: [
|
|
202
|
-
{ name: 'chair', range: '*', saveRange: '^1.2.0', save: true },
|
|
203
|
-
{ name: 'starter-template', range: '*', saveRange: '^2.0.0', save: true },
|
|
204
|
-
],
|
|
205
|
-
installMetadata: {
|
|
206
|
-
template: {
|
|
207
|
-
saveOnInstall: false,
|
|
208
|
-
readAssetDependenciesFromPackageJson: true,
|
|
209
|
-
omitUnchangedInstalledFilesOnUpload: true,
|
|
210
|
-
},
|
|
211
|
-
},
|
|
212
|
-
},
|
|
213
|
-
)
|
|
214
|
-
|
|
215
|
-
const pkg = JSON.parse(await fs.readFile(path.join(tempDir, 'package.json'), 'utf-8')) as {
|
|
216
|
-
assetDependencies: Record<string, string>
|
|
217
|
-
}
|
|
218
|
-
assert.deepEqual(pkg.assetDependencies, { chair: '^1.2.0' })
|
|
219
|
-
})
|
|
220
|
-
|
|
221
|
-
test('install skips existing changed files unless force is set', async () => {
|
|
222
|
-
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'market-conflict-'))
|
|
223
|
-
await fs.mkdir(path.join(tempDir, 'public', 'model'), { recursive: true })
|
|
224
|
-
await fs.writeFile(path.join(tempDir, 'package.json'), '{}\n')
|
|
225
|
-
await fs.writeFile(path.join(tempDir, 'public', 'model', 'chair.glb'), 'local edit')
|
|
226
|
-
|
|
227
|
-
const resolution = {
|
|
228
|
-
assets: [
|
|
229
|
-
{
|
|
230
|
-
name: 'chair',
|
|
231
|
-
type: 'model',
|
|
232
|
-
version: '1.0.0',
|
|
233
|
-
description: null,
|
|
234
|
-
npmDependencies: {},
|
|
235
|
-
assetDependencies: {},
|
|
236
|
-
skillDependencies: {},
|
|
237
|
-
},
|
|
238
|
-
],
|
|
239
|
-
npmDependencies: {},
|
|
240
|
-
skillDependencies: {},
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
const skipped = await install(
|
|
244
|
-
clientWithZip({
|
|
245
|
-
'public/model/chair.glb': textEncoder.encode('market bytes'),
|
|
246
|
-
}),
|
|
247
|
-
resolution,
|
|
248
|
-
{ cwd: tempDir },
|
|
249
|
-
)
|
|
250
|
-
|
|
251
|
-
assert.equal(await fs.readFile(path.join(tempDir, 'public', 'model', 'chair.glb'), 'utf-8'), 'local edit')
|
|
252
|
-
assert.match(skipped.warnings[0], /Skipped public\/model\/chair\.glb/u)
|
|
253
|
-
assert.deepEqual(skipped.assets[0].files, [])
|
|
254
|
-
|
|
255
|
-
const forced = await install(
|
|
256
|
-
clientWithZip({
|
|
257
|
-
'public/model/chair.glb': textEncoder.encode('market bytes'),
|
|
258
|
-
}),
|
|
259
|
-
resolution,
|
|
260
|
-
{ cwd: tempDir, force: true },
|
|
261
|
-
)
|
|
262
|
-
|
|
263
|
-
assert.equal(
|
|
264
|
-
await fs.readFile(path.join(tempDir, 'public', 'model', 'chair.glb'), 'utf-8'),
|
|
265
|
-
'market bytes',
|
|
266
|
-
)
|
|
267
|
-
assert.deepEqual(forced.warnings, [])
|
|
268
|
-
assert.deepEqual(forced.assets[0].files, ['public/model/chair.glb'])
|
|
269
|
-
})
|
|
270
|
-
|
|
271
|
-
test('findInstallRoot uses the nearest package.json, not the monorepo root', async () => {
|
|
272
|
-
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'market-root-'))
|
|
273
|
-
const repoRoot = path.join(tempDir, 'repo')
|
|
274
|
-
const appRoot = path.join(repoRoot, 'packages', 'app')
|
|
275
|
-
const cwd = path.join(appRoot, 'src', 'routes')
|
|
276
|
-
await fs.mkdir(path.join(appRoot, 'public'), { recursive: true })
|
|
277
|
-
await fs.mkdir(cwd, { recursive: true })
|
|
278
|
-
await fs.writeFile(path.join(repoRoot, 'package.json'), '{}\n')
|
|
279
|
-
await fs.writeFile(path.join(appRoot, 'package.json'), '{}\n')
|
|
280
|
-
|
|
281
|
-
assert.equal(await findInstallRoot(cwd), appRoot)
|
|
282
|
-
})
|
|
283
|
-
|
|
284
|
-
test('findInstallRoot falls back to cwd when no package.json exists', async () => {
|
|
285
|
-
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'market-root-'))
|
|
286
|
-
const cwd = path.join(tempDir, 'repo', 'src')
|
|
287
|
-
await fs.mkdir(cwd, { recursive: true })
|
|
288
|
-
|
|
289
|
-
assert.equal(await findInstallRoot(cwd), cwd)
|
|
290
|
-
})
|
|
291
|
-
|
|
292
|
-
function clientWithZip(files: Record<string, Uint8Array>) {
|
|
293
|
-
return {
|
|
294
|
-
asset: {
|
|
295
|
-
downloadZip: async () => new Blob([zipSync(files)]),
|
|
296
|
-
},
|
|
297
|
-
} as never
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
async function exists(file: string): Promise<boolean> {
|
|
301
|
-
try {
|
|
302
|
-
await fs.stat(file)
|
|
303
|
-
return true
|
|
304
|
-
} catch {
|
|
305
|
-
return false
|
|
306
|
-
}
|
|
307
|
-
}
|