@drawcall/market 0.1.13 → 0.1.14

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/dist/install.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Install resolved assets into a local project.
3
3
  *
4
- * 1. Downloads asset files into ./src/{assetName}/ via the oRPC client.
4
+ * 1. Downloads public asset files into ./public/... via the oRPC client.
5
5
  * 2. Merges npm dependencies into package.json.
6
6
  * 3. Runs the package manager to install npm deps.
7
7
  *
@@ -11,10 +11,11 @@
11
11
  import type { MarketClient } from './client.js';
12
12
  import type { ResolveResult } from './resolve.js';
13
13
  export interface InstallOptions {
14
- /** Project root directory (default: cwd) */
14
+ /** Directory to start project root discovery from (default: cwd) */
15
15
  cwd?: string;
16
16
  /** Log progress */
17
17
  onProgress?: (message: string) => void;
18
18
  }
19
19
  export declare function install(client: MarketClient, resolution: ResolveResult, opts?: InstallOptions): Promise<void>;
20
+ export declare function findInstallRoot(cwd?: string): Promise<string>;
20
21
  //# sourceMappingURL=install.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"install.d.ts","sourceRoot":"","sources":["../src/install.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAMH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAC/C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAA;AASjD,MAAM,WAAW,cAAc;IAC7B,4CAA4C;IAC5C,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,mBAAmB;IACnB,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;CACvC;AAED,wBAAsB,OAAO,CAC3B,MAAM,EAAE,YAAY,EACpB,UAAU,EAAE,aAAa,EACzB,IAAI,GAAE,cAAmB,GACxB,OAAO,CAAC,IAAI,CAAC,CASf"}
1
+ {"version":3,"file":"install.d.ts","sourceRoot":"","sources":["../src/install.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAMH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAC/C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAA;AASjD,MAAM,WAAW,cAAc;IAC7B,oEAAoE;IACpE,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,mBAAmB;IACnB,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;CACvC;AAED,wBAAsB,OAAO,CAC3B,MAAM,EAAE,YAAY,EACpB,UAAU,EAAE,aAAa,EACzB,IAAI,GAAE,cAAmB,GACxB,OAAO,CAAC,IAAI,CAAC,CAQf;AAED,wBAAsB,eAAe,CAAC,GAAG,GAAE,MAAsB,GAAG,OAAO,CAAC,MAAM,CAAC,CAgBlF"}
package/dist/install.js CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Install resolved assets into a local project.
3
3
  *
4
- * 1. Downloads asset files into ./src/{assetName}/ via the oRPC client.
4
+ * 1. Downloads public asset files into ./public/... via the oRPC client.
5
5
  * 2. Merges npm dependencies into package.json.
6
6
  * 3. Runs the package manager to install npm deps.
7
7
  *
@@ -13,42 +13,68 @@ import * as path from 'path';
13
13
  import { unzipSync } from 'fflate';
14
14
  import { detectPackageManager, installDependencies } from 'nypm';
15
15
  export async function install(client, resolution, opts = {}) {
16
- const cwd = opts.cwd ?? process.cwd();
17
16
  const log = opts.onProgress ?? (() => { });
18
- // Run asset file downloads and npm dep installation in parallel
17
+ const installRoot = await findInstallRoot(opts.cwd ?? process.cwd());
19
18
  await Promise.all([
20
- downloadAssets(client, resolution, cwd, log),
21
- installNpmDeps(resolution, cwd, log),
19
+ downloadAssets(client, resolution, installRoot, log),
20
+ installNpmDeps(resolution, installRoot, log),
22
21
  ]);
23
22
  }
24
- async function downloadAssets(client, resolution, cwd, log) {
23
+ export async function findInstallRoot(cwd = process.cwd()) {
24
+ const start = path.resolve(cwd);
25
+ let packageRoot = null;
26
+ let dir = start;
27
+ while (true) {
28
+ if (await isFile(path.join(dir, 'package.json'))) {
29
+ packageRoot = dir;
30
+ }
31
+ const parent = path.dirname(dir);
32
+ if (parent === dir)
33
+ break;
34
+ dir = parent;
35
+ }
36
+ return packageRoot ?? start;
37
+ }
38
+ async function downloadAssets(client, resolution, projectRoot, log) {
25
39
  for (const asset of resolution.assets) {
26
- const destDir = path.join(cwd, 'src', asset.name);
27
- await fs.mkdir(destDir, { recursive: true });
28
40
  log(`Downloading ${asset.name}@${asset.version}...`);
29
41
  const zip = await client.asset.downloadZip({ name: asset.name, version: asset.version });
30
42
  const files = unzipSync(new Uint8Array(await zip.arrayBuffer()));
43
+ let installedFiles = 0;
31
44
  for (const [relativePath, content] of Object.entries(files)) {
32
- const filePath = path.join(destDir, relativePath);
33
- if (!isInside(destDir, filePath)) {
45
+ const zipPath = relativePath.replace(/\\/g, '/');
46
+ if (zipPath.split('/').includes('..') ||
47
+ path.posix.isAbsolute(zipPath) ||
48
+ path.win32.isAbsolute(zipPath)) {
34
49
  throw new Error(`Zip contains an unsafe path: ${relativePath}`);
35
50
  }
51
+ const normalizedPath = path.posix.normalize(zipPath);
52
+ if (normalizedPath === '.' ||
53
+ normalizedPath === 'README.md' ||
54
+ normalizedPath.endsWith('/')) {
55
+ continue;
56
+ }
57
+ const filePath = path.join(projectRoot, normalizedPath);
36
58
  await fs.mkdir(path.dirname(filePath), { recursive: true });
37
59
  await fs.writeFile(filePath, content);
60
+ installedFiles += 1;
38
61
  }
39
- log(`Downloaded ${Object.keys(files).length} files to src/${asset.name}/`);
62
+ log(`Downloaded ${installedFiles} files.`);
40
63
  }
41
64
  }
42
- function isInside(root, target) {
43
- const relative = path.relative(root, target);
44
- return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
65
+ async function isFile(file) {
66
+ try {
67
+ return (await fs.stat(file)).isFile();
68
+ }
69
+ catch {
70
+ return false;
71
+ }
45
72
  }
46
- async function installNpmDeps(resolution, cwd, log) {
73
+ async function installNpmDeps(resolution, projectRoot, log) {
47
74
  const deps = resolution.npmDependencies;
48
75
  if (Object.keys(deps).length === 0)
49
76
  return;
50
- // Read existing package.json
51
- const pkgPath = path.join(cwd, 'package.json');
77
+ const pkgPath = path.join(projectRoot, 'package.json');
52
78
  let pkg;
53
79
  try {
54
80
  pkg = JSON.parse(await fs.readFile(pkgPath, 'utf-8'));
@@ -56,15 +82,13 @@ async function installNpmDeps(resolution, cwd, log) {
56
82
  catch {
57
83
  pkg = { name: 'my-project', private: true, dependencies: {} };
58
84
  }
59
- // Merge dependencies
60
85
  pkg.dependencies = { ...pkg.dependencies, ...deps };
61
86
  await fs.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
62
87
  log(`Installing npm dependencies: ${Object.keys(deps).join(', ')}`);
63
- // Detect package manager, fall back to npm
64
- const pm = await detectPackageManager(cwd).catch(() => null);
88
+ const pm = await detectPackageManager(projectRoot).catch(() => null);
65
89
  const pmName = pm?.name ?? 'npm';
66
90
  log(`Using ${pmName}...`);
67
- await installDependencies({ cwd, packageManager: { name: pmName, command: pmName } });
91
+ await installDependencies({ cwd: projectRoot, packageManager: { name: pmName, command: pmName } });
68
92
  log('npm dependencies installed.');
69
93
  }
70
94
  //# sourceMappingURL=install.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"install.js","sourceRoot":"","sources":["../src/install.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"install.js","sourceRoot":"","sources":["../src/install.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@drawcall/market",
3
- "version": "0.1.13",
3
+ "version": "0.1.14",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/drawcall-ai/market",
@@ -31,6 +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",
34
35
  "typecheck": "tsc --noEmit"
35
36
  },
36
37
  "dependencies": {
package/src/install.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Install resolved assets into a local project.
3
3
  *
4
- * 1. Downloads asset files into ./src/{assetName}/ via the oRPC client.
4
+ * 1. Downloads public asset files into ./public/... via the oRPC client.
5
5
  * 2. Merges npm dependencies into package.json.
6
6
  * 3. Runs the package manager to install npm deps.
7
7
  *
@@ -24,7 +24,7 @@ interface PackageJson {
24
24
  }
25
25
 
26
26
  export interface InstallOptions {
27
- /** Project root directory (default: cwd) */
27
+ /** Directory to start project root discovery from (default: cwd) */
28
28
  cwd?: string
29
29
  /** Log progress */
30
30
  onProgress?: (message: string) => void
@@ -35,59 +35,92 @@ export async function install(
35
35
  resolution: ResolveResult,
36
36
  opts: InstallOptions = {},
37
37
  ): Promise<void> {
38
- const cwd = opts.cwd ?? process.cwd()
39
38
  const log = opts.onProgress ?? (() => {})
39
+ const installRoot = await findInstallRoot(opts.cwd ?? process.cwd())
40
40
 
41
- // Run asset file downloads and npm dep installation in parallel
42
41
  await Promise.all([
43
- downloadAssets(client, resolution, cwd, log),
44
- installNpmDeps(resolution, cwd, log),
42
+ downloadAssets(client, resolution, installRoot, log),
43
+ installNpmDeps(resolution, installRoot, log),
45
44
  ])
46
45
  }
47
46
 
47
+ export async function findInstallRoot(cwd: string = process.cwd()): Promise<string> {
48
+ const start = path.resolve(cwd)
49
+ let packageRoot: string | null = null
50
+
51
+ let dir = start
52
+ while (true) {
53
+ if (await isFile(path.join(dir, 'package.json'))) {
54
+ packageRoot = dir
55
+ }
56
+
57
+ const parent = path.dirname(dir)
58
+ if (parent === dir) break
59
+ dir = parent
60
+ }
61
+
62
+ return packageRoot ?? start
63
+ }
64
+
48
65
  async function downloadAssets(
49
66
  client: MarketClient,
50
67
  resolution: ResolveResult,
51
- cwd: string,
68
+ projectRoot: string,
52
69
  log: (msg: string) => void,
53
70
  ): Promise<void> {
54
71
  for (const asset of resolution.assets) {
55
- const destDir = path.join(cwd, 'src', asset.name)
56
- await fs.mkdir(destDir, { recursive: true })
57
-
58
72
  log(`Downloading ${asset.name}@${asset.version}...`)
59
73
 
60
74
  const zip = await client.asset.downloadZip({ name: asset.name, version: asset.version })
61
75
  const files = unzipSync(new Uint8Array(await zip.arrayBuffer()))
76
+ let installedFiles = 0
62
77
 
63
78
  for (const [relativePath, content] of Object.entries(files) as [string, Uint8Array][]) {
64
- const filePath = path.join(destDir, relativePath)
65
- if (!isInside(destDir, filePath)) {
79
+ const zipPath = relativePath.replace(/\\/g, '/')
80
+ if (
81
+ zipPath.split('/').includes('..') ||
82
+ path.posix.isAbsolute(zipPath) ||
83
+ path.win32.isAbsolute(zipPath)
84
+ ) {
66
85
  throw new Error(`Zip contains an unsafe path: ${relativePath}`)
67
86
  }
87
+
88
+ const normalizedPath = path.posix.normalize(zipPath)
89
+ if (
90
+ normalizedPath === '.' ||
91
+ normalizedPath === 'README.md' ||
92
+ normalizedPath.endsWith('/')
93
+ ) {
94
+ continue
95
+ }
96
+
97
+ const filePath = path.join(projectRoot, normalizedPath)
68
98
  await fs.mkdir(path.dirname(filePath), { recursive: true })
69
99
  await fs.writeFile(filePath, content)
100
+ installedFiles += 1
70
101
  }
71
102
 
72
- log(`Downloaded ${Object.keys(files).length} files to src/${asset.name}/`)
103
+ log(`Downloaded ${installedFiles} files.`)
73
104
  }
74
105
  }
75
106
 
76
- function isInside(root: string, target: string): boolean {
77
- const relative = path.relative(root, target)
78
- return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative))
107
+ async function isFile(file: string): Promise<boolean> {
108
+ try {
109
+ return (await fs.stat(file)).isFile()
110
+ } catch {
111
+ return false
112
+ }
79
113
  }
80
114
 
81
115
  async function installNpmDeps(
82
116
  resolution: ResolveResult,
83
- cwd: string,
117
+ projectRoot: string,
84
118
  log: (msg: string) => void,
85
119
  ): Promise<void> {
86
120
  const deps = resolution.npmDependencies
87
121
  if (Object.keys(deps).length === 0) return
88
122
 
89
- // Read existing package.json
90
- const pkgPath = path.join(cwd, 'package.json')
123
+ const pkgPath = path.join(projectRoot, 'package.json')
91
124
  let pkg: PackageJson
92
125
  try {
93
126
  pkg = JSON.parse(await fs.readFile(pkgPath, 'utf-8')) as PackageJson
@@ -95,17 +128,15 @@ async function installNpmDeps(
95
128
  pkg = { name: 'my-project', private: true, dependencies: {} }
96
129
  }
97
130
 
98
- // Merge dependencies
99
131
  pkg.dependencies = { ...pkg.dependencies, ...deps }
100
132
  await fs.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + '\n')
101
133
 
102
134
  log(`Installing npm dependencies: ${Object.keys(deps).join(', ')}`)
103
135
 
104
- // Detect package manager, fall back to npm
105
- const pm = await detectPackageManager(cwd).catch(() => null)
136
+ const pm = await detectPackageManager(projectRoot).catch(() => null)
106
137
  const pmName = pm?.name ?? 'npm'
107
138
 
108
139
  log(`Using ${pmName}...`)
109
- await installDependencies({ cwd, packageManager: { name: pmName, command: pmName } })
140
+ await installDependencies({ cwd: projectRoot, packageManager: { name: pmName, command: pmName } })
110
141
  log('npm dependencies installed.')
111
142
  }
@@ -0,0 +1,114 @@
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
+
9
+ const textEncoder = new TextEncoder()
10
+
11
+ test('install writes zip files into the package root', async () => {
12
+ const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'market-install-'))
13
+ const appRoot = path.join(tempDir, 'app')
14
+ const cwd = path.join(appRoot, 'src', 'feature')
15
+ await fs.mkdir(path.join(appRoot, 'public'), { recursive: true })
16
+ await fs.mkdir(cwd, { recursive: true })
17
+ await fs.writeFile(path.join(appRoot, 'package.json'), '{}\n')
18
+ await fs.writeFile(path.join(appRoot, 'README.md'), 'project readme\n')
19
+
20
+ await install(
21
+ clientWithZip({
22
+ 'public/humanoid-animation/idle-loop.glb': textEncoder.encode('glb'),
23
+ 'src/generated/idle-loop.ts': textEncoder.encode('export const idleLoop = true\n'),
24
+ 'README.md': textEncoder.encode('asset readme'),
25
+ }),
26
+ {
27
+ assets: [
28
+ {
29
+ name: 'idle-loop',
30
+ version: '1.0.0',
31
+ npmDependencies: {},
32
+ assetDependencies: {},
33
+ },
34
+ ],
35
+ npmDependencies: {},
36
+ },
37
+ { cwd },
38
+ )
39
+
40
+ assert.equal(
41
+ await fs.readFile(path.join(appRoot, 'public', 'humanoid-animation', 'idle-loop.glb'), 'utf-8'),
42
+ 'glb',
43
+ )
44
+ assert.equal(
45
+ await fs.readFile(path.join(appRoot, 'src', 'generated', 'idle-loop.ts'), 'utf-8'),
46
+ 'export const idleLoop = true\n',
47
+ )
48
+ assert.equal(await fs.readFile(path.join(appRoot, 'README.md'), 'utf-8'), 'project readme\n')
49
+ assert.equal(await exists(path.join(cwd, 'src', 'idle-loop', 'public')), false)
50
+ })
51
+
52
+ test('install rejects zip paths that escape through parent segments', async () => {
53
+ const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'market-install-'))
54
+ await fs.writeFile(path.join(tempDir, 'package.json'), '{}\n')
55
+
56
+ await assert.rejects(
57
+ install(
58
+ clientWithZip({
59
+ 'public/../package.json': textEncoder.encode('nope\n'),
60
+ }),
61
+ {
62
+ assets: [
63
+ {
64
+ name: 'bad-path',
65
+ version: '1.0.0',
66
+ npmDependencies: {},
67
+ assetDependencies: {},
68
+ },
69
+ ],
70
+ npmDependencies: {},
71
+ },
72
+ { cwd: tempDir },
73
+ ),
74
+ /unsafe path/u,
75
+ )
76
+ })
77
+
78
+ test('findInstallRoot ignores public directories and uses the highest package.json', async () => {
79
+ const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'market-root-'))
80
+ const repoRoot = path.join(tempDir, 'repo')
81
+ const appRoot = path.join(repoRoot, 'packages', 'app')
82
+ const cwd = path.join(appRoot, 'src', 'routes')
83
+ await fs.mkdir(path.join(appRoot, 'public'), { recursive: true })
84
+ await fs.mkdir(cwd, { recursive: true })
85
+ await fs.writeFile(path.join(repoRoot, 'package.json'), '{}\n')
86
+ await fs.writeFile(path.join(appRoot, 'package.json'), '{}\n')
87
+
88
+ assert.equal(await findInstallRoot(cwd), repoRoot)
89
+ })
90
+
91
+ test('findInstallRoot falls back to cwd when no package.json exists', async () => {
92
+ const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'market-root-'))
93
+ const cwd = path.join(tempDir, 'repo', 'src')
94
+ await fs.mkdir(cwd, { recursive: true })
95
+
96
+ assert.equal(await findInstallRoot(cwd), cwd)
97
+ })
98
+
99
+ function clientWithZip(files: Record<string, Uint8Array>) {
100
+ return {
101
+ asset: {
102
+ downloadZip: async () => new Blob([zipSync(files)]),
103
+ },
104
+ } as never
105
+ }
106
+
107
+ async function exists(file: string): Promise<boolean> {
108
+ try {
109
+ await fs.stat(file)
110
+ return true
111
+ } catch {
112
+ return false
113
+ }
114
+ }