Skip to content

Commit 35c6503

Browse files
committed
Merge branch 'license-review' of github.com:corespan/aistudio-app into license-review
2 parents f9189d3 + 2a804d9 commit 35c6503

12 files changed

Lines changed: 174 additions & 69 deletions

File tree

‎Benchmarking/.env.example‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,7 @@
1-
VITE_API_URL = "https://corespan.ddnsgeek.com:8443"
2-
BROWSER=chrome
1+
# Backend API origin. Required — the dev server throws on startup if unset.
2+
# Point this at wherever your aistudio-server instance is reachable.
3+
VITE_API_URL="http://localhost:8002"
4+
5+
# Optional: browser to open when the dev server starts (chrome, firefox,
6+
# safari, or a path to a binary). Leave unset to use the system default.
7+
BROWSER=chrome

‎Benchmarking/CLAUDE.md‎

Lines changed: 34 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -23,16 +23,19 @@ client, UI primitives, chart wrappers — lives locally under `src/shared/`.
2323

2424
---
2525

26-
## Shared Packages
26+
## Shared Code
2727

28-
This app consumes the workspace packages via `workspace:*` and path aliases
29-
(wired in `vite.config.ts` + `tsconfig*.json`):
28+
There are no workspace packages and no private registry — every dependency in
29+
`package.json` is public on npm, so a clean `pnpm install` is all a fresh
30+
checkout needs.
3031

31-
- `@corespan/ui` — shared components (peer deps only — this app provides Mantine/React/etc.)
32-
- `@corespan/api` — zero-dep HTTP client (`createApiClient`)
33-
- `@corespan/utils` — zero-dep helpers
32+
Shared code lives under `src/shared/` and is imported through the single `@`
33+
alias (`@` → `src`, wired in `vite.config.ts` + `tsconfig*.json`):
3434

35-
Never import across workspace boundaries with relative paths — always use the `@corespan/` alias.
35+
- `@/shared/api` — HTTP client (`baseClient`), `API_ORIGIN`, TanStack `queryClient`
36+
- `@/shared/ui` — local primitives: `CoreChart`, `CoreTable`, `CoreForm`, `CoreIcon`
37+
38+
Prefer the `@` alias over deep relative paths (`../../../shared/...`).
3639

3740
---
3841

@@ -41,14 +44,24 @@ Never import across workspace boundaries with relative paths — always use the
4144
```
4245
src/
4346
├── app/
44-
│ └── App.tsx # Mantine + QueryClient providers
47+
│ ├── App.tsx # Mantine + QueryClient providers
48+
│ ├── constants.ts
49+
│ └── layout/ # AppLayout, AppFooter, PageShell
4550
├── features/
46-
│ └── benchmarks/
47-
│ └── Benchmarks.tsx # Starter page
51+
│ ├── about/ # AboutUs page + components
52+
│ └── benchmarks/ # The main feature
53+
│ ├── Benchmarks.tsx
54+
│ ├── components/ # Tables, charts, modals, log streams
55+
│ ├── data/ # Query keys, services
56+
│ ├── lib/ # Pure helpers (maskIp, toBenchmarkRows, ...)
57+
│ └── store/ # Zustand stores (useJupyterRunStore, ...)
4858
├── shared/
49-
│ └── api/
50-
│ ├── baseClient.ts # Single createApiClient instance — never create a second
51-
│ └── queryClient.ts # TanStack QueryClient config
59+
│ ├── api/
60+
│ │ ├── baseClient.ts # Single createApiClient instance — never create a second
61+
│ │ ├── config.ts # API_ORIGIN, resolved from VITE_API_URL
62+
│ │ ├── core/ # Client, middleware, error types
63+
│ │ └── queryClient.ts # TanStack QueryClient config
64+
│ └── ui/ # CoreChart, CoreTable, CoreForm, CoreIcon
5265
├── index.css
5366
└── main.tsx # React 19 StrictMode entry point
5467
```
@@ -64,6 +77,11 @@ import it — never instantiate a second client.
6477

6578
## Development Proxy
6679

67-
The Vite config has no backend proxy yet. When the app needs to call the backend,
68-
add a proxy block mirroring `apps/composer/vite.config.ts` and document the required
69-
`.env` vars here (see `.env.example`).
80+
`vite.config.ts` proxies two paths to `VITE_API_URL`:
81+
82+
- `/api` — the versioned API namespace
83+
- `/health` — the backend's health probe, which sits at the root rather than
84+
under `/api/v1` and so needs its own entry
85+
86+
`VITE_API_URL` is **required** — the dev server throws on startup without it.
87+
Copy `.env.example` to `.env` and point it at your backend.

‎Benchmarking/public/icons.svg‎

Lines changed: 0 additions & 24 deletions
This file was deleted.

‎Benchmarking/public/third-party-licences.txt‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1653,6 +1653,8 @@ Licence: (MIT OR CC0-1.0)
16531653
Author: Sindre Sorhus
16541654
Source: https://github.com/sindresorhus/type-fest#readme
16551655

1656+
── license-cc0 ──
1657+
16561658
Creative Commons Legal Code
16571659

16581660
CC0 1.0 Universal
@@ -1775,6 +1777,18 @@ Source: https://github.com/sindresorhus/type-fest#readme
17751777
party to this document and has no duty or obligation with respect to
17761778
this CC0 or use of the Work.
17771779

1780+
── license-mit ──
1781+
1782+
MIT License
1783+
1784+
Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (https://sindresorhus.com)
1785+
1786+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
1787+
1788+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
1789+
1790+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
1791+
17781792
------------------------------------------------------------------------
17791793

17801794
use-callback-ref@1.3.3

‎Benchmarking/scripts/check-compliance.mjs‎

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
* Usage: pnpm compliance
1010
*/
1111

12-
import { execFileSync } from 'node:child_process'
12+
import { execFileSync, execSync } from 'node:child_process'
1313
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs'
1414
import { dirname, join, resolve } from 'node:path'
1515
import { fileURLToPath } from 'node:url'
@@ -242,11 +242,16 @@ section('Build output')
242242
}
243243
} else {
244244
try {
245-
execFileSync('pnpm', ['build'], {
245+
const buildOptions = {
246246
cwd: APP,
247247
stdio: 'inherit',
248248
env: { ...process.env, VITE_API_URL: process.env.VITE_API_URL || 'https://example.invalid' },
249-
})
249+
}
250+
// `pnpm` is a .cmd on Windows and Node refuses to spawn one without a
251+
// shell — see the longer note on runPnpm in
252+
// generate-third-party-notices.mjs.
253+
if (process.platform === 'win32') execSync('pnpm build', buildOptions)
254+
else execFileSync('pnpm', ['build'], buildOptions)
250255
} catch {
251256
fail('build failed')
252257
}

‎Benchmarking/scripts/generate-third-party-notices.mjs‎

Lines changed: 74 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@
5050
* node scripts/generate-third-party-notices.mjs --check # CI: fail if stale
5151
*/
5252

53-
import { execFileSync } from 'node:child_process'
53+
import { execFileSync, execSync } from 'node:child_process'
5454
import { existsSync, readFileSync, readdirSync, writeFileSync } from 'node:fs'
5555
import { dirname, join, resolve } from 'node:path'
5656
import { fileURLToPath } from 'node:url'
@@ -77,6 +77,27 @@ const CHECK = process.argv.includes('--check')
7777
*/
7878
const normalise = (text) => text.replace(/\r\n?/g, '\n').replace(/[ \t]+$/gm, '')
7979

80+
/**
81+
* Run pnpm and capture its output, on Windows as well as POSIX.
82+
*
83+
* `pnpm` on PATH is a shim: an extensionless script on POSIX, `pnpm.cmd` on
84+
* Windows. Since Node 20.12 (the CVE-2024-27980 fix) spawning a `.cmd` without
85+
* a shell is refused, so `execFileSync('pnpm', ...)` failed here with ENOENT —
86+
* which the catch below reported as "run `pnpm install` first", sending anyone
87+
* who hit it after an install that had worked fine.
88+
*
89+
* Windows therefore needs a shell. Passing an args array *and* `shell: true`
90+
* concatenates them unescaped and warns (DEP0190), so the command is built as
91+
* one string instead. Every argument here is a literal — nothing is
92+
* interpolated from the environment or the lockfile — so there is nothing for
93+
* cmd.exe to misparse. POSIX keeps the no-shell path, which is what CI runs.
94+
*/
95+
function runPnpm(args, options) {
96+
return process.platform === 'win32'
97+
? execSync(`pnpm ${args.join(' ')}`, options)
98+
: execFileSync('pnpm', args, options)
99+
}
100+
80101
/** Filenames that hold a licence text, in order of preference. */
81102
const LICENCE_FILENAMES = [
82103
'LICENSE',
@@ -107,7 +128,7 @@ function readProductionPackages() {
107128
// licence text can be read rather than guessed from an SPDX string.
108129
let raw
109130
try {
110-
raw = execFileSync('pnpm', ['licenses', 'list', '--prod', '--json'], {
131+
raw = runPnpm(['licenses', 'list', '--prod', '--json'], {
111132
cwd: ROOT,
112133
encoding: 'utf8',
113134
maxBuffer: 64 * 1024 * 1024,
@@ -141,26 +162,64 @@ function readProductionPackages() {
141162
return packages
142163
}
143164

165+
/**
166+
* Read the licence text a package ships on disk.
167+
*
168+
* Every filename decision below goes through one `readdirSync` listing rather
169+
* than `existsSync` probes. That is deliberate, and it is the fix for a bug
170+
* that only appeared once this script was first run on Windows:
171+
*
172+
* NTFS is case-insensitive, so `existsSync('.../LICENSE-MIT')` returns true
173+
* for a file actually named `license-mit`. `type-fest` is dual `MIT OR
174+
* CC0-1.0` and ships both `license-mit` and `license-cc0`. On Linux the
175+
* preference list missed both and the fallback picked CC0; on Windows
176+
* `LICENSE-MIT` matched case-insensitively and won. Same commit, same
177+
* lockfile, 120 lines of difference — and `--check` called the committed
178+
* artifact stale on a clean checkout.
179+
*
180+
* Matching against the real listing makes the preference list mean what it
181+
* says on both platforms. `readdirSync` order is filesystem-defined, so the
182+
* fallback sorts before choosing; without that the output would still depend
183+
* on the machine that produced it.
184+
*/
144185
function readLicenceText(pkgPath) {
145186
if (!existsSync(pkgPath)) return null
146187

147-
for (const filename of LICENCE_FILENAMES) {
148-
const candidate = join(pkgPath, filename)
149-
if (existsSync(candidate)) {
150-
try {
151-
return normalise(readFileSync(candidate, 'utf8')).trim()
152-
} catch {
153-
/* fall through to the next candidate */
154-
}
188+
let entries
189+
try {
190+
entries = readdirSync(pkgPath)
191+
} catch {
192+
return null // unreadable directory — reported as missing below
193+
}
194+
195+
const read = (filename) => {
196+
try {
197+
return normalise(readFileSync(join(pkgPath, filename), 'utf8')).trim()
198+
} catch {
199+
return null
155200
}
156201
}
157202

203+
for (const filename of LICENCE_FILENAMES) {
204+
if (!entries.includes(filename)) continue
205+
const text = read(filename)
206+
if (text) return text
207+
}
208+
158209
// Some packages name the file unpredictably (LICENSE-APACHE, COPYING, ...).
159-
try {
160-
const match = readdirSync(pkgPath).find((f) => /^(licen[cs]e|copying)/i.test(f))
161-
if (match) return normalise(readFileSync(join(pkgPath, match), 'utf8')).trim()
162-
} catch {
163-
/* unreadable directory — reported as missing below */
210+
// A package that ships more than one is telling us something — usually that
211+
// it is dual-licensed — so reproduce all of them, labelled, rather than
212+
// picking one and discarding the choice the manifest advertises.
213+
const matches = entries.filter((f) => /^(licen[cs]e|copying)/i.test(f)).sort()
214+
215+
if (matches.length === 1) return read(matches[0])
216+
217+
if (matches.length > 1) {
218+
const texts = matches
219+
.map((f) => ({ f, text: read(f) }))
220+
.filter(({ text }) => text)
221+
.map(({ f, text }) => `── ${f} ──\n\n${text}`)
222+
if (texts.length) return texts.join('\n\n')
164223
}
165224

166225
return null

‎Benchmarking/src/features/benchmarks/components/StartBenchmarkModal.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,7 @@ export const StartBenchmarkModal = ({ opened, onClose }: StartBenchmarkModalProp
276276
<Paper withBorder radius="md" p="lg" shadow="xs">
277277
<Autocomplete
278278
label="Node IP"
279-
placeholder="Type or paste a machine IP (e.g. 10.6.12.22)"
279+
placeholder="Type or paste a machine IP (e.g. 10.0.0.12)"
280280
description={
281281
nodes.data && nodes.data.length > 0
282282
? 'Previously used machines appear as suggestions'

‎Benchmarking/src/features/benchmarks/data/selectors/toSelectOptions.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export const toNamedOptions = (raw: unknown): ComboboxData => toOptions(raw)
3636

3737
/**
3838
* Normalize /nodes into Machine IP options. Value is the node's IP; label adds
39-
* the hostname/name when present (e.g. "10.6.82.45 (bench-01)").
39+
* the hostname/name when present (e.g. "192.0.2.45 (bench-01)").
4040
*/
4141
export const toNodeOptions = (raw: unknown): ComboboxData =>
4242
unwrap(raw)

‎Benchmarking/src/features/benchmarks/lib/maskIp.ts‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
// Masks the host-identifying half of every IPv4 address found in a string —
22
// keeps the first two octets (useful for spotting which subnet/rack a machine
3-
// is on) and replaces the last two with a bullet, e.g. "10.6.82.45" becomes
4-
// "10.6.•.•". Regex-based so it works equally on a bare IP ("10.6.82.45"), a
3+
// is on) and replaces the last two with a bullet, e.g. "192.0.2.45" becomes
4+
// "192.0.•.•". Regex-based so it works equally on a bare IP ("192.0.2.45"), a
55
// comma-joined list of them (`machineIp` — see `toBenchmarkRows.ts`), or an
66
// IP embedded in a longer string like a notebook URL
7-
// ("http://10.6.82.45:8888/lab" -> "http://10.6.•.•:8888/lab"). Strings with
7+
// ("http://192.0.2.45:8888/lab" -> "http://192.0.•.•:8888/lab"). Strings with
88
// no IPv4-shaped substring (hostnames, "—", "") pass through unchanged.
9+
//
10+
// Addresses in these comments are from 192.0.2.0/24, the RFC 5737
11+
// documentation range, so no real host is named in a public repository.
912
const IPV4_PATTERN = /\b(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})\b/g
1013

1114
export const maskIp = (value: string | null | undefined): string => {

‎Benchmarking/src/features/benchmarks/store/useJupyterRunStore.ts‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,10 @@ import type { JupyterRun, JupyterRunStore } from '../types'
66

77
// The notebook URL isn't in the launch response — it arrives later in the log
88
// stream on the definitive ready line, e.g.
9-
// "✓ Jupyter Lab running at: http://10.6.12.22:8899/lab". Match only that line
10-
// (not any stray "running at" log) so the URL — and the Open button — surface
11-
// exactly when Jupyter is actually ready.
9+
// "✓ Jupyter Lab running at: http://192.0.2.22:8899/lab" (a RFC 5737
10+
// documentation address, not a real host). Match only that line — not any
11+
// stray "running at" log — so the URL, and the Open button, surface exactly
12+
// when Jupyter is actually ready.
1213
const extractUrlFromLog = (line: string): string | null => {
1314
const match = line.match(/jupyter\s*lab\s+running at:?\s*(https?:\/\/\S+)/i)
1415
return match ? match[1].replace(/[.,)]+$/, '') : null

0 commit comments

Comments
 (0)