fix(ci): add safe.directory for containerized pipeline job (#1108)

* feat(S01/T01): Scaffolded the `studio` Electron workspace with a workin…

- package.json
- studio/package.json
- studio/electron.vite.config.ts
- studio/src/main/index.ts
- studio/src/preload/index.ts
- studio/src/renderer/src/styles/index.css
- studio/src/renderer/src/App.tsx

* chore: init gsd

* fix(ci): add safe.directory for containerized pipeline job

The Dev Publish job runs inside a Docker container where the checkout
user differs from the container user (root), causing git's dubious
ownership check to reject git operations in version-stamp.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(ci): remove .gsd/.gitignore from tracking

The no-gsd-dir CI check fails when .gsd/ exists as a directory, even
if only .gitignore is tracked inside it.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
TÂCHES 2026-03-18 01:11:52 -06:00 committed by GitHub
parent 1020c140af
commit 920f1bed9a
22 changed files with 4117 additions and 2 deletions

View file

@ -29,6 +29,9 @@ jobs:
steps:
- uses: actions/checkout@v6
- name: Mark workspace safe for git
run: git config --global --add safe.directory "$GITHUB_WORKSPACE"
- uses: actions/setup-node@v6
with:
node-version: 22

3596
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -13,7 +13,8 @@
},
"type": "module",
"workspaces": [
"packages/*"
"packages/*",
"studio"
],
"bin": {
"gsd": "dist/loader.js",

View file

@ -0,0 +1,39 @@
import { resolve } from 'node:path'
import { defineConfig } from 'electron-vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
main: {
build: {
outDir: 'dist/main',
rollupOptions: {
input: {
index: resolve(__dirname, 'src/main/index.ts')
}
}
}
},
preload: {
build: {
outDir: 'dist/preload',
rollupOptions: {
input: {
index: resolve(__dirname, 'src/preload/index.ts')
}
}
}
},
renderer: {
root: resolve(__dirname, 'src/renderer'),
resolve: {
alias: {
'@': resolve(__dirname, 'src/renderer/src')
}
},
plugins: [tailwindcss(), react()],
build: {
outDir: resolve(__dirname, 'dist/renderer')
}
}
})

31
studio/package.json Normal file
View file

@ -0,0 +1,31 @@
{
"name": "@gsd/studio",
"private": true,
"version": "0.0.0",
"type": "module",
"main": "dist/main/index.js",
"scripts": {
"dev": "electron-vite dev",
"build": "electron-vite build",
"preview": "electron-vite preview",
"test": "node --test test/*.test.mjs"
},
"dependencies": {
"@phosphor-icons/react": "^2.1.10",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-resizable-panels": "^4.7.3",
"zustand": "^5.0.8"
},
"devDependencies": {
"@tailwindcss/vite": "^4.2.1",
"@types/node": "^22.18.6",
"@types/react": "^19.2.2",
"@types/react-dom": "^19.2.2",
"@vitejs/plugin-react": "^5.1.0",
"electron": "^41.0.3",
"electron-vite": "^5.0.0",
"tailwindcss": "^4.2.1",
"typescript": "^5.9.3"
}
}

56
studio/src/main/index.ts Normal file
View file

@ -0,0 +1,56 @@
import { app, BrowserWindow } from 'electron'
import { fileURLToPath } from 'node:url'
import { dirname, join } from 'node:path'
const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)
let mainWindow: BrowserWindow | null = null
function createWindow(): BrowserWindow {
const preload = join(__dirname, '../preload/index.mjs')
const window = new BrowserWindow({
width: 1400,
height: 900,
minWidth: 1100,
minHeight: 720,
backgroundColor: '#0a0a0a',
titleBarStyle: process.platform === 'darwin' ? 'hiddenInset' : 'default',
trafficLightPosition: process.platform === 'darwin' ? { x: 16, y: 16 } : undefined,
webPreferences: {
preload,
contextIsolation: true,
nodeIntegration: false
}
})
const rendererUrl = process.env.ELECTRON_RENDERER_URL
if (rendererUrl) {
void window.loadURL(rendererUrl)
} else {
void window.loadFile(join(__dirname, '../renderer/index.html'))
}
console.log('[studio] window created')
console.log('GSD Studio ready')
return window
}
app.whenReady().then(() => {
mainWindow = createWindow()
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
mainWindow = createWindow()
}
})
})
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit()
}
})

9
studio/src/preload/index.d.ts vendored Normal file
View file

@ -0,0 +1,9 @@
import type { StudioBridge } from './index'
declare global {
interface Window {
studio: StudioBridge
}
}
export {}

View file

@ -0,0 +1,22 @@
import { contextBridge } from 'electron'
export type StudioStatus = {
connected: boolean
}
export type StudioBridge = {
onEvent: (callback: (event: unknown) => void) => () => void
sendCommand: (command: string, args?: Record<string, unknown>) => void
spawn: () => void
getStatus: () => Promise<StudioStatus>
}
const studio: StudioBridge = {
onEvent: (_callback) => () => undefined,
sendCommand: (_command, _args) => undefined,
spawn: () => undefined,
getStatus: () => Promise.resolve({ connected: false })
}
console.log('[studio] preload loaded')
contextBridge.exposeInMainWorld('studio', studio)

View file

@ -0,0 +1,17 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>GSD Studio</title>
<link rel="preload" href="./src/assets/fonts/Inter-Regular.woff2" as="font" type="font/woff2" crossorigin />
<link rel="preload" href="./src/assets/fonts/Inter-Medium.woff2" as="font" type="font/woff2" crossorigin />
<link rel="preload" href="./src/assets/fonts/Inter-SemiBold.woff2" as="font" type="font/woff2" crossorigin />
<link rel="preload" href="./src/assets/fonts/JetBrainsMono-Regular.woff2" as="font" type="font/woff2" crossorigin />
<link rel="preload" href="./src/assets/fonts/JetBrainsMono-Medium.woff2" as="font" type="font/woff2" crossorigin />
</head>
<body>
<div id="root"></div>
<script type="module" src="./src/main.tsx"></script>
</body>
</html>

View file

@ -0,0 +1,77 @@
import { BracketsCurly, Lightning, Palette } from '@phosphor-icons/react'
import { colors, fonts, fontSizes } from './lib/theme/tokens'
const statusRows = [
{ label: 'Shell', value: 'electron-vite + React 19', icon: Lightning },
{ label: 'Theme', value: colors.accent, icon: Palette },
{ label: 'Code', value: fonts.mono, icon: BracketsCurly }
]
export default function App() {
return (
<main className="min-h-screen bg-bg-primary text-text-primary">
<div className="mx-auto flex min-h-screen max-w-6xl flex-col justify-center px-10 py-16">
<div className="grid gap-10 lg:grid-cols-[1.2fr_0.8fr]">
<section className="rounded-[28px] border border-border bg-[linear-gradient(180deg,rgba(255,255,255,0.04),rgba(255,255,255,0.01))] p-10 shadow-[0_24px_80px_rgba(0,0,0,0.35)] backdrop-blur-sm">
<div className="mb-8 inline-flex items-center gap-3 rounded-full border border-[color:var(--color-accent-muted)] bg-[color:var(--color-accent-muted)] px-4 py-2 text-xs font-medium uppercase tracking-[0.28em] text-accent">
<span className="h-2 w-2 rounded-full bg-accent shadow-[0_0_18px_rgba(212,160,78,0.7)]" />
Studio bootstrap
</div>
<h1 className="max-w-3xl text-[clamp(3.4rem,9vw,6.8rem)] font-semibold leading-[0.92] tracking-[-0.06em] text-balance text-text-primary">
GSD Studio ships with a dark shell that actually feels deliberate.
</h1>
<p className="mt-6 max-w-2xl text-lg leading-8 text-text-secondary">
Inter drives the interface, JetBrains Mono handles code surfaces, and the warm amber system accent keeps the palette restrained instead of drifting into generic app chrome.
</p>
<div className="mt-10 grid gap-4 sm:grid-cols-3">
{statusRows.map(({ label, value, icon: Icon }) => (
<div key={label} className="rounded-2xl border border-border bg-bg-secondary/70 p-4">
<div className="mb-4 flex items-center justify-between">
<span className="text-xs uppercase tracking-[0.24em] text-text-tertiary">{label}</span>
<Icon size={18} weight="duotone" className="text-accent" />
</div>
<p className="text-sm font-medium text-text-primary">{value}</p>
</div>
))}
</div>
</section>
<aside className="space-y-4 rounded-[28px] border border-border bg-bg-secondary/80 p-8 shadow-[inset_0_1px_0_rgba(255,255,255,0.03)]">
<div>
<p className="text-xs uppercase tracking-[0.24em] text-text-tertiary">Typography proof</p>
<p className="mt-3 text-2xl font-semibold text-text-primary">Inter 600 for hierarchy</p>
<p className="mt-2 text-sm leading-7 text-text-secondary">
The first task only validates the shell and token system. Three-column layout and primitives land in T02.
</p>
</div>
<div className="rounded-2xl border border-[color:var(--color-accent-muted)] bg-[#120f09] p-5">
<p className="text-xs uppercase tracking-[0.24em] text-accent/80">Code surface</p>
<pre className="mt-4 overflow-x-auto rounded-xl border border-border bg-black/30 p-4 text-sm leading-7 text-[#f5deb3]">
<code>{`const studio = await window.studio.getStatus();\nif (!studio.connected) {\n console.log('Renderer scaffold ready');\n}`}</code>
</pre>
</div>
<dl className="grid grid-cols-3 gap-3 text-sm">
<div className="rounded-2xl border border-border bg-bg-primary p-4">
<dt className="text-text-tertiary">Accent</dt>
<dd className="mt-2 font-medium text-accent">{colors.accent}</dd>
</div>
<div className="rounded-2xl border border-border bg-bg-primary p-4">
<dt className="text-text-tertiary">UI font</dt>
<dd className="mt-2 font-medium text-text-primary">{fontSizes.body}</dd>
</div>
<div className="rounded-2xl border border-border bg-bg-primary p-4">
<dt className="text-text-tertiary">Mono</dt>
<dd className="mt-2 font-mono text-[13px] text-text-primary">{fonts.mono}</dd>
</div>
</dl>
</aside>
</div>
</div>
</main>
)
}

View file

@ -0,0 +1,28 @@
export const colors = {
bgPrimary: '#0a0a0a',
bgSecondary: '#111111',
bgTertiary: '#1a1a1a',
bgHover: '#222222',
border: '#262626',
borderActive: '#333333',
textPrimary: '#e5e5e5',
textSecondary: '#a3a3a3',
textTertiary: '#737373',
accent: '#d4a04e',
accentHover: '#e0b366',
accentMuted: 'rgba(212, 160, 78, 0.15)'
} as const
export const fonts = {
sans: "'Inter', system-ui, sans-serif",
mono: "'JetBrains Mono', ui-monospace, monospace"
} as const
export const fontSizes = {
hero: '4.75rem',
display: '3.5rem',
title: '2rem',
bodyLg: '1.125rem',
body: '0.9375rem',
caption: '0.75rem'
} as const

View file

@ -0,0 +1,16 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import App from './App'
import './styles/index.css'
const rootElement = document.getElementById('root')
if (!rootElement) {
throw new Error('Root element #root was not found')
}
createRoot(rootElement).render(
<StrictMode>
<App />
</StrictMode>
)

View file

@ -0,0 +1,129 @@
@import "tailwindcss";
@font-face {
font-family: 'Inter';
src: url('../assets/fonts/Inter-Regular.woff2') format('woff2');
font-weight: 400;
font-style: normal;
font-display: block;
}
@font-face {
font-family: 'Inter';
src: url('../assets/fonts/Inter-Medium.woff2') format('woff2');
font-weight: 500;
font-style: normal;
font-display: block;
}
@font-face {
font-family: 'Inter';
src: url('../assets/fonts/Inter-SemiBold.woff2') format('woff2');
font-weight: 600;
font-style: normal;
font-display: block;
}
@font-face {
font-family: 'JetBrains Mono';
src: url('../assets/fonts/JetBrainsMono-Regular.woff2') format('woff2');
font-weight: 400;
font-style: normal;
font-display: block;
}
@font-face {
font-family: 'JetBrains Mono';
src: url('../assets/fonts/JetBrainsMono-Medium.woff2') format('woff2');
font-weight: 500;
font-style: normal;
font-display: block;
}
@theme {
--color-bg-primary: #0a0a0a;
--color-bg-secondary: #111111;
--color-bg-tertiary: #1a1a1a;
--color-bg-hover: #222222;
--color-border: #262626;
--color-border-active: #333333;
--color-text-primary: #e5e5e5;
--color-text-secondary: #a3a3a3;
--color-text-tertiary: #737373;
--color-accent: #d4a04e;
--color-accent-hover: #e0b366;
--color-accent-muted: rgba(212, 160, 78, 0.15);
--font-sans: 'Inter', system-ui, sans-serif;
--font-mono: 'JetBrains Mono', ui-monospace, monospace;
--text-hero: 4.75rem;
--text-display: 3.5rem;
--text-title: 2rem;
--text-body-lg: 1.125rem;
--text-body: 0.9375rem;
--text-caption: 0.75rem;
}
:root {
color-scheme: dark;
background-color: var(--color-bg-primary);
}
* {
box-sizing: border-box;
}
html {
background: var(--color-bg-primary);
}
body {
margin: 0;
min-height: 100vh;
background: var(--color-bg-primary);
color: var(--color-text-primary);
font-family: var(--font-sans);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-rendering: optimizeLegibility;
}
#root {
min-height: 100vh;
}
code,
pre,
.font-mono {
font-family: var(--font-mono);
}
::selection {
background: rgba(212, 160, 78, 0.28);
color: var(--color-text-primary);
}
* {
scrollbar-width: thin;
scrollbar-color: #3a3125 #111111;
}
*::-webkit-scrollbar {
width: 10px;
height: 10px;
}
*::-webkit-scrollbar-track {
background: #111111;
}
*::-webkit-scrollbar-thumb {
border-radius: 999px;
background: linear-gradient(180deg, #403223 0%, #2d241a 100%);
border: 2px solid #111111;
}
*::-webkit-scrollbar-thumb:hover {
background: linear-gradient(180deg, #5b4731 0%, #3c2f21 100%);
}

View file

@ -0,0 +1,39 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import { readFile } from 'node:fs/promises'
const cssPath = new URL('../src/renderer/src/styles/index.css', import.meta.url)
const tokensPath = new URL('../src/renderer/src/lib/theme/tokens.ts', import.meta.url)
test('theme CSS defines required color tokens and font-display block', async () => {
const css = await readFile(cssPath, 'utf8')
for (const token of [
'--color-bg-primary',
'--color-bg-secondary',
'--color-bg-tertiary',
'--color-bg-hover',
'--color-border',
'--color-border-active',
'--color-text-primary',
'--color-text-secondary',
'--color-text-tertiary',
'--color-accent',
'--color-accent-hover',
'--color-accent-muted',
'--font-sans',
'--font-mono'
]) {
assert.match(css, new RegExp(token.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&')))
}
const blockMatches = css.match(/font-display:\s*block;/g) ?? []
assert.equal(blockMatches.length, 5)
})
test('token module exports key theme primitives', async () => {
const tokensFile = await readFile(tokensPath, 'utf8')
assert.match(tokensFile, /accent: '#d4a04e'/)
assert.match(tokensFile, /mono: "'JetBrains Mono'/)
assert.match(tokensFile, /body: '0\.9375rem'/)
})

7
studio/tsconfig.json Normal file
View file

@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.node.json" },
{ "path": "./tsconfig.web.json" }
]
}

22
studio/tsconfig.node.json Normal file
View file

@ -0,0 +1,22 @@
{
"compilerOptions": {
"composite": true,
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "Bundler",
"lib": ["ES2022"],
"types": ["node", "electron-vite/node"],
"strict": true,
"skipLibCheck": true,
"noEmit": true,
"allowSyntheticDefaultImports": true,
"resolveJsonModule": true,
"isolatedModules": true
},
"include": [
"electron.vite.config.ts",
"src/main/**/*.ts",
"src/preload/**/*.ts",
"src/preload/**/*.d.ts"
]
}

25
studio/tsconfig.web.json Normal file
View file

@ -0,0 +1,25 @@
{
"compilerOptions": {
"composite": true,
"target": "ES2022",
"useDefineForClassFields": true,
"module": "ESNext",
"moduleResolution": "Bundler",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"jsx": "react-jsx",
"types": ["node"],
"strict": true,
"skipLibCheck": true,
"noEmit": true,
"allowSyntheticDefaultImports": true,
"resolveJsonModule": true,
"baseUrl": ".",
"paths": {
"@/*": ["src/renderer/src/*"]
}
},
"include": [
"src/renderer/src/**/*",
"src/preload/index.d.ts"
]
}