@deckio/deck-engine 1.7.5 → 1.7.7
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/components/BottomBar.jsx +9 -9
- package/components/BottomBar.module.css +17 -17
- package/components/Navigation.jsx +155 -106
- package/components/Navigation.module.css +154 -145
- package/components/Slide.jsx +15 -15
- package/components/exportDeckPdf.js +134 -0
- package/context/SlideContext.jsx +171 -171
- package/index.js +5 -5
- package/instructions/AGENTS.md +26 -26
- package/instructions/deck-config.instructions.md +34 -34
- package/instructions/deck-project.instructions.md +34 -34
- package/instructions/slide-css.instructions.md +91 -91
- package/instructions/slide-jsx.instructions.md +34 -34
- package/package.json +49 -45
- package/scripts/capture-screen.mjs +110 -110
- package/scripts/export-pdf.mjs +287 -287
- package/scripts/generate-image.mjs +110 -110
- package/scripts/init-project.mjs +214 -188
- package/skills/deck-add-slide/SKILL.md +217 -217
- package/skills/deck-delete-slide/SKILL.md +51 -51
- package/skills/deck-generate-image/SKILL.md +85 -85
- package/skills/deck-inspect/SKILL.md +60 -60
- package/skills/deck-sketch/SKILL.md +91 -91
- package/skills/deck-validate-project/SKILL.md +80 -80
- package/slides/GenericThankYouSlide.jsx +31 -31
- package/slides/ThankYouSlide.module.css +131 -131
- package/styles/global.css +191 -191
- package/vite.js +26 -26
|
@@ -1,110 +1,110 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* Generate an image using OpenAI chatgpt-image-latest (Image API) and save
|
|
4
|
-
* it for use in slides.
|
|
5
|
-
*
|
|
6
|
-
* Requires OPENAI_API_KEY environment variable.
|
|
7
|
-
*
|
|
8
|
-
* Usage:
|
|
9
|
-
* node scripts/generate-image.mjs --prompt "a bridge icon" --name bridge-icon
|
|
10
|
-
* node scripts/generate-image.mjs --prompt "..." --name hero --size 1536x1024
|
|
11
|
-
* node scripts/generate-image.mjs --prompt "..." --name icon --quality high
|
|
12
|
-
* node scripts/generate-image.mjs --prompt "..." --name x --model gpt-image-1.5
|
|
13
|
-
*/
|
|
14
|
-
import { writeFileSync, mkdirSync, readFileSync, existsSync } from 'fs'
|
|
15
|
-
import path from 'path'
|
|
16
|
-
|
|
17
|
-
const root = process.cwd()
|
|
18
|
-
const args = process.argv.slice(2)
|
|
19
|
-
const arg = (name, fb) => {
|
|
20
|
-
const i = args.indexOf(`--${name}`)
|
|
21
|
-
return i !== -1 && args[i + 1] ? args[i + 1] : fb
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const outputDir = arg('out-dir', path.join('src', 'data', 'generated'))
|
|
25
|
-
const imagesDir = path.resolve(root, outputDir)
|
|
26
|
-
const envPath = path.join(root, '.env')
|
|
27
|
-
|
|
28
|
-
if (existsSync(envPath)) {
|
|
29
|
-
for (const line of readFileSync(envPath, 'utf-8').split(/\r?\n/)) {
|
|
30
|
-
const m = line.match(/^([A-Z_][A-Z0-9_]*)=(.+)$/)
|
|
31
|
-
if (m) process.env[m[1]] = m[2].trim()
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const PROMPT = arg('prompt', null)
|
|
36
|
-
const NAME = arg('name', null)
|
|
37
|
-
const SIZE = arg('size', '1024x1024')
|
|
38
|
-
const QUALITY = arg('quality', 'auto')
|
|
39
|
-
const MODEL = arg('model', 'chatgpt-image-latest')
|
|
40
|
-
|
|
41
|
-
const API_KEY = process.env.OPENAI_API_KEY
|
|
42
|
-
if (!API_KEY) {
|
|
43
|
-
console.error('❌ Set OPENAI_API_KEY environment variable')
|
|
44
|
-
process.exit(1)
|
|
45
|
-
}
|
|
46
|
-
if (!PROMPT) {
|
|
47
|
-
console.error('❌ --prompt is required')
|
|
48
|
-
process.exit(1)
|
|
49
|
-
}
|
|
50
|
-
if (!NAME) {
|
|
51
|
-
console.error('❌ --name is required (used as filename)')
|
|
52
|
-
process.exit(1)
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
async function main() {
|
|
56
|
-
mkdirSync(imagesDir, { recursive: true })
|
|
57
|
-
console.log(`🎨 Generating image: "${PROMPT}"`)
|
|
58
|
-
console.log(` Model: ${MODEL} Size: ${SIZE} Quality: ${QUALITY}`)
|
|
59
|
-
|
|
60
|
-
const body = {
|
|
61
|
-
model: MODEL,
|
|
62
|
-
prompt: PROMPT,
|
|
63
|
-
n: 1,
|
|
64
|
-
size: SIZE,
|
|
65
|
-
quality: QUALITY,
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const res = await fetch('https://api.openai.com/v1/images/generations', {
|
|
69
|
-
method: 'POST',
|
|
70
|
-
headers: {
|
|
71
|
-
'Content-Type': 'application/json',
|
|
72
|
-
Authorization: `Bearer ${API_KEY}`,
|
|
73
|
-
},
|
|
74
|
-
body: JSON.stringify(body),
|
|
75
|
-
})
|
|
76
|
-
|
|
77
|
-
if (!res.ok) {
|
|
78
|
-
const err = await res.text()
|
|
79
|
-
console.error(`❌ API error (${res.status}): ${err}`)
|
|
80
|
-
process.exit(1)
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
const data = await res.json()
|
|
84
|
-
const imageB64 = data.data?.[0]?.b64_json || null
|
|
85
|
-
const imageUrl = data.data?.[0]?.url || null
|
|
86
|
-
|
|
87
|
-
if (!imageB64 && !imageUrl) {
|
|
88
|
-
console.error('❌ Could not extract image from response.')
|
|
89
|
-
console.error(JSON.stringify(data).slice(0, 2000))
|
|
90
|
-
process.exit(1)
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
const filepath = path.join(imagesDir, `${NAME}.png`)
|
|
94
|
-
let imgBuffer
|
|
95
|
-
if (imageB64) {
|
|
96
|
-
imgBuffer = Buffer.from(imageB64, 'base64')
|
|
97
|
-
} else {
|
|
98
|
-
const dl = await fetch(imageUrl)
|
|
99
|
-
imgBuffer = Buffer.from(await dl.arrayBuffer())
|
|
100
|
-
}
|
|
101
|
-
writeFileSync(filepath, imgBuffer)
|
|
102
|
-
console.log(`📸 Saved: ${path.relative(root, filepath)}`)
|
|
103
|
-
console.log(`\n📦 Import in a slide:`)
|
|
104
|
-
console.log(` import ${NAME}Img from '../../data/generated/${NAME}.png'`)
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
main().catch(err => {
|
|
108
|
-
console.error('❌', err?.message || err)
|
|
109
|
-
process.exit(1)
|
|
110
|
-
})
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Generate an image using OpenAI chatgpt-image-latest (Image API) and save
|
|
4
|
+
* it for use in slides.
|
|
5
|
+
*
|
|
6
|
+
* Requires OPENAI_API_KEY environment variable.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* node scripts/generate-image.mjs --prompt "a bridge icon" --name bridge-icon
|
|
10
|
+
* node scripts/generate-image.mjs --prompt "..." --name hero --size 1536x1024
|
|
11
|
+
* node scripts/generate-image.mjs --prompt "..." --name icon --quality high
|
|
12
|
+
* node scripts/generate-image.mjs --prompt "..." --name x --model gpt-image-1.5
|
|
13
|
+
*/
|
|
14
|
+
import { writeFileSync, mkdirSync, readFileSync, existsSync } from 'fs'
|
|
15
|
+
import path from 'path'
|
|
16
|
+
|
|
17
|
+
const root = process.cwd()
|
|
18
|
+
const args = process.argv.slice(2)
|
|
19
|
+
const arg = (name, fb) => {
|
|
20
|
+
const i = args.indexOf(`--${name}`)
|
|
21
|
+
return i !== -1 && args[i + 1] ? args[i + 1] : fb
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const outputDir = arg('out-dir', path.join('src', 'data', 'generated'))
|
|
25
|
+
const imagesDir = path.resolve(root, outputDir)
|
|
26
|
+
const envPath = path.join(root, '.env')
|
|
27
|
+
|
|
28
|
+
if (existsSync(envPath)) {
|
|
29
|
+
for (const line of readFileSync(envPath, 'utf-8').split(/\r?\n/)) {
|
|
30
|
+
const m = line.match(/^([A-Z_][A-Z0-9_]*)=(.+)$/)
|
|
31
|
+
if (m) process.env[m[1]] = m[2].trim()
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const PROMPT = arg('prompt', null)
|
|
36
|
+
const NAME = arg('name', null)
|
|
37
|
+
const SIZE = arg('size', '1024x1024')
|
|
38
|
+
const QUALITY = arg('quality', 'auto')
|
|
39
|
+
const MODEL = arg('model', 'chatgpt-image-latest')
|
|
40
|
+
|
|
41
|
+
const API_KEY = process.env.OPENAI_API_KEY
|
|
42
|
+
if (!API_KEY) {
|
|
43
|
+
console.error('❌ Set OPENAI_API_KEY environment variable')
|
|
44
|
+
process.exit(1)
|
|
45
|
+
}
|
|
46
|
+
if (!PROMPT) {
|
|
47
|
+
console.error('❌ --prompt is required')
|
|
48
|
+
process.exit(1)
|
|
49
|
+
}
|
|
50
|
+
if (!NAME) {
|
|
51
|
+
console.error('❌ --name is required (used as filename)')
|
|
52
|
+
process.exit(1)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function main() {
|
|
56
|
+
mkdirSync(imagesDir, { recursive: true })
|
|
57
|
+
console.log(`🎨 Generating image: "${PROMPT}"`)
|
|
58
|
+
console.log(` Model: ${MODEL} Size: ${SIZE} Quality: ${QUALITY}`)
|
|
59
|
+
|
|
60
|
+
const body = {
|
|
61
|
+
model: MODEL,
|
|
62
|
+
prompt: PROMPT,
|
|
63
|
+
n: 1,
|
|
64
|
+
size: SIZE,
|
|
65
|
+
quality: QUALITY,
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const res = await fetch('https://api.openai.com/v1/images/generations', {
|
|
69
|
+
method: 'POST',
|
|
70
|
+
headers: {
|
|
71
|
+
'Content-Type': 'application/json',
|
|
72
|
+
Authorization: `Bearer ${API_KEY}`,
|
|
73
|
+
},
|
|
74
|
+
body: JSON.stringify(body),
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
if (!res.ok) {
|
|
78
|
+
const err = await res.text()
|
|
79
|
+
console.error(`❌ API error (${res.status}): ${err}`)
|
|
80
|
+
process.exit(1)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const data = await res.json()
|
|
84
|
+
const imageB64 = data.data?.[0]?.b64_json || null
|
|
85
|
+
const imageUrl = data.data?.[0]?.url || null
|
|
86
|
+
|
|
87
|
+
if (!imageB64 && !imageUrl) {
|
|
88
|
+
console.error('❌ Could not extract image from response.')
|
|
89
|
+
console.error(JSON.stringify(data).slice(0, 2000))
|
|
90
|
+
process.exit(1)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const filepath = path.join(imagesDir, `${NAME}.png`)
|
|
94
|
+
let imgBuffer
|
|
95
|
+
if (imageB64) {
|
|
96
|
+
imgBuffer = Buffer.from(imageB64, 'base64')
|
|
97
|
+
} else {
|
|
98
|
+
const dl = await fetch(imageUrl)
|
|
99
|
+
imgBuffer = Buffer.from(await dl.arrayBuffer())
|
|
100
|
+
}
|
|
101
|
+
writeFileSync(filepath, imgBuffer)
|
|
102
|
+
console.log(`📸 Saved: ${path.relative(root, filepath)}`)
|
|
103
|
+
console.log(`\n📦 Import in a slide:`)
|
|
104
|
+
console.log(` import ${NAME}Img from '../../data/generated/${NAME}.png'`)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
main().catch(err => {
|
|
108
|
+
console.error('❌', err?.message || err)
|
|
109
|
+
process.exit(1)
|
|
110
|
+
})
|
package/scripts/init-project.mjs
CHANGED
|
@@ -1,188 +1,214 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* deck-engine init — provisions Copilot skills and state into a deck project.
|
|
4
|
-
*
|
|
5
|
-
* Copies .github/skills/ from the engine package and bootstraps
|
|
6
|
-
* .github/memory/state.md with project metadata from deck.config.js.
|
|
7
|
-
*
|
|
8
|
-
* Usage:
|
|
9
|
-
* node node_modules/@deckio/deck-engine/scripts/init-project.mjs
|
|
10
|
-
* npx deck-init (if bin is configured)
|
|
11
|
-
*
|
|
12
|
-
* Idempotent — safe to re-run. Updates skills, preserves state.
|
|
13
|
-
*/
|
|
14
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync, copyFileSync, rmSync } from 'fs'
|
|
15
|
-
import { join, dirname } from 'path'
|
|
16
|
-
import { fileURLToPath } from 'url'
|
|
17
|
-
|
|
18
|
-
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
19
|
-
const engineRoot = join(__dirname, '..')
|
|
20
|
-
const projectRoot = process.cwd()
|
|
21
|
-
|
|
22
|
-
// ── Discover project metadata from deck.config.js ──
|
|
23
|
-
|
|
24
|
-
function readProjectMeta() {
|
|
25
|
-
const configPath = join(projectRoot, 'deck.config.js')
|
|
26
|
-
if (!existsSync(configPath)) {
|
|
27
|
-
console.error('❌ No deck.config.js found in', projectRoot)
|
|
28
|
-
process.exit(1)
|
|
29
|
-
}
|
|
30
|
-
const content = readFileSync(configPath, 'utf-8')
|
|
31
|
-
const str = (key) => {
|
|
32
|
-
const m = content.match(new RegExp(`${key}:\\s*['"\`]([^'"\`]+)['"\`]`))
|
|
33
|
-
return m ? m[1] : null
|
|
34
|
-
}
|
|
35
|
-
return {
|
|
36
|
-
id: str('id') || 'unknown',
|
|
37
|
-
title: str('title') || 'Deck Project',
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// ── Copy skills ──
|
|
42
|
-
|
|
43
|
-
function copySkills() {
|
|
44
|
-
const srcSkills = join(engineRoot, 'skills')
|
|
45
|
-
if (!existsSync(srcSkills)) {
|
|
46
|
-
console.warn('⚠️ No skills directory in engine package')
|
|
47
|
-
return 0
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const destSkills = join(projectRoot, '.github', 'skills')
|
|
51
|
-
let count = 0
|
|
52
|
-
|
|
53
|
-
// Collect engine skill names to detect stale skills
|
|
54
|
-
const engineSkillNames = new Set()
|
|
55
|
-
for (const entry of readdirSync(srcSkills, { withFileTypes: true })) {
|
|
56
|
-
if (!entry.isDirectory()) continue
|
|
57
|
-
const skillFile = join(srcSkills, entry.name, 'SKILL.md')
|
|
58
|
-
if (!existsSync(skillFile)) continue
|
|
59
|
-
engineSkillNames.add(entry.name)
|
|
60
|
-
|
|
61
|
-
const destDir = join(destSkills, entry.name)
|
|
62
|
-
mkdirSync(destDir, { recursive: true })
|
|
63
|
-
copyFileSync(skillFile, join(destDir, 'SKILL.md'))
|
|
64
|
-
count++
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// Remove stale skills no longer in the engine
|
|
68
|
-
if (existsSync(destSkills)) {
|
|
69
|
-
for (const entry of readdirSync(destSkills, { withFileTypes: true })) {
|
|
70
|
-
if (!entry.isDirectory()) continue
|
|
71
|
-
if (!engineSkillNames.has(entry.name)) {
|
|
72
|
-
rmSync(join(destSkills, entry.name), { recursive: true, force: true })
|
|
73
|
-
console.log(` Removed stale skill: ${entry.name}`)
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
return count
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// ── Copy instructions ──
|
|
82
|
-
|
|
83
|
-
function copyInstructions() {
|
|
84
|
-
const srcInstructions = join(engineRoot, 'instructions')
|
|
85
|
-
if (!existsSync(srcInstructions)) {
|
|
86
|
-
console.warn('⚠️ No instructions directory in engine package')
|
|
87
|
-
return 0
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
const destInstructions = join(projectRoot, '.github', 'instructions')
|
|
91
|
-
mkdirSync(destInstructions, { recursive: true })
|
|
92
|
-
let count = 0
|
|
93
|
-
|
|
94
|
-
// Collect engine instruction names to detect stale instructions
|
|
95
|
-
const engineInstrNames = new Set()
|
|
96
|
-
for (const entry of readdirSync(srcInstructions, { withFileTypes: true })) {
|
|
97
|
-
if (!entry.isFile() || !entry.name.endsWith('.instructions.md')) continue
|
|
98
|
-
engineInstrNames.add(entry.name)
|
|
99
|
-
copyFileSync(
|
|
100
|
-
join(srcInstructions, entry.name),
|
|
101
|
-
join(destInstructions, entry.name)
|
|
102
|
-
)
|
|
103
|
-
count++
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// Remove stale instructions no longer in the engine
|
|
107
|
-
for (const entry of readdirSync(destInstructions, { withFileTypes: true })) {
|
|
108
|
-
if (!entry.isFile() || !entry.name.endsWith('.instructions.md')) continue
|
|
109
|
-
if (!engineInstrNames.has(entry.name)) {
|
|
110
|
-
rmSync(join(destInstructions, entry.name), { force: true })
|
|
111
|
-
console.log(` Removed stale instruction: ${entry.name}`)
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
return count
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// ── Copy AGENTS.md ──
|
|
119
|
-
|
|
120
|
-
function copyAgentsMd() {
|
|
121
|
-
const src = join(engineRoot, 'instructions', 'AGENTS.md')
|
|
122
|
-
if (!existsSync(src)) return false
|
|
123
|
-
copyFileSync(src, join(projectRoot, 'AGENTS.md'))
|
|
124
|
-
return true
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// ── Bootstrap state.md ──
|
|
128
|
-
|
|
129
|
-
function bootstrapState(meta) {
|
|
130
|
-
const stateDir = join(projectRoot, '.github', 'memory')
|
|
131
|
-
const statePath = join(stateDir, 'state.md')
|
|
132
|
-
|
|
133
|
-
// Don't overwrite existing state (preserves user's port/url config)
|
|
134
|
-
if (existsSync(statePath)) {
|
|
135
|
-
console.log(' state.md already exists — preserved')
|
|
136
|
-
return
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
mkdirSync(stateDir, { recursive: true })
|
|
140
|
-
const content = `# Deck State
|
|
141
|
-
|
|
142
|
-
## Project
|
|
143
|
-
id: ${meta.id}
|
|
144
|
-
title: ${meta.title}
|
|
145
|
-
|
|
146
|
-
## Dev Server
|
|
147
|
-
port: 5173
|
|
148
|
-
url: http://localhost:5173/
|
|
149
|
-
`
|
|
150
|
-
writeFileSync(statePath, content, 'utf-8')
|
|
151
|
-
console.log(' Created .github/memory/state.md')
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// ── Create eyes directory ──
|
|
155
|
-
|
|
156
|
-
function createEyesDir() {
|
|
157
|
-
const eyesDir = join(projectRoot, '.github', 'eyes')
|
|
158
|
-
mkdirSync(eyesDir, { recursive: true })
|
|
159
|
-
|
|
160
|
-
// Add to .gitignore if not already there
|
|
161
|
-
const gitignorePath = join(projectRoot, '.gitignore')
|
|
162
|
-
if (existsSync(gitignorePath)) {
|
|
163
|
-
const gitignore = readFileSync(gitignorePath, 'utf-8')
|
|
164
|
-
if (!gitignore.includes('.github/eyes')) {
|
|
165
|
-
writeFileSync(gitignorePath, gitignore.trimEnd() + '\n.github/eyes/\n', 'utf-8')
|
|
166
|
-
console.log(' Added .github/eyes/ to .gitignore')
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* deck-engine init — provisions Copilot skills and state into a deck project.
|
|
4
|
+
*
|
|
5
|
+
* Copies .github/skills/ from the engine package and bootstraps
|
|
6
|
+
* .github/memory/state.md with project metadata from deck.config.js.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* node node_modules/@deckio/deck-engine/scripts/init-project.mjs
|
|
10
|
+
* npx deck-init (if bin is configured)
|
|
11
|
+
*
|
|
12
|
+
* Idempotent — safe to re-run. Updates skills, preserves state.
|
|
13
|
+
*/
|
|
14
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync, copyFileSync, rmSync } from 'fs'
|
|
15
|
+
import { join, dirname } from 'path'
|
|
16
|
+
import { fileURLToPath } from 'url'
|
|
17
|
+
|
|
18
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
19
|
+
const engineRoot = join(__dirname, '..')
|
|
20
|
+
const projectRoot = process.cwd()
|
|
21
|
+
|
|
22
|
+
// ── Discover project metadata from deck.config.js ──
|
|
23
|
+
|
|
24
|
+
function readProjectMeta() {
|
|
25
|
+
const configPath = join(projectRoot, 'deck.config.js')
|
|
26
|
+
if (!existsSync(configPath)) {
|
|
27
|
+
console.error('❌ No deck.config.js found in', projectRoot)
|
|
28
|
+
process.exit(1)
|
|
29
|
+
}
|
|
30
|
+
const content = readFileSync(configPath, 'utf-8')
|
|
31
|
+
const str = (key) => {
|
|
32
|
+
const m = content.match(new RegExp(`${key}:\\s*['"\`]([^'"\`]+)['"\`]`))
|
|
33
|
+
return m ? m[1] : null
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
id: str('id') || 'unknown',
|
|
37
|
+
title: str('title') || 'Deck Project',
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ── Copy skills ──
|
|
42
|
+
|
|
43
|
+
function copySkills() {
|
|
44
|
+
const srcSkills = join(engineRoot, 'skills')
|
|
45
|
+
if (!existsSync(srcSkills)) {
|
|
46
|
+
console.warn('⚠️ No skills directory in engine package')
|
|
47
|
+
return 0
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const destSkills = join(projectRoot, '.github', 'skills')
|
|
51
|
+
let count = 0
|
|
52
|
+
|
|
53
|
+
// Collect engine skill names to detect stale skills
|
|
54
|
+
const engineSkillNames = new Set()
|
|
55
|
+
for (const entry of readdirSync(srcSkills, { withFileTypes: true })) {
|
|
56
|
+
if (!entry.isDirectory()) continue
|
|
57
|
+
const skillFile = join(srcSkills, entry.name, 'SKILL.md')
|
|
58
|
+
if (!existsSync(skillFile)) continue
|
|
59
|
+
engineSkillNames.add(entry.name)
|
|
60
|
+
|
|
61
|
+
const destDir = join(destSkills, entry.name)
|
|
62
|
+
mkdirSync(destDir, { recursive: true })
|
|
63
|
+
copyFileSync(skillFile, join(destDir, 'SKILL.md'))
|
|
64
|
+
count++
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Remove stale skills no longer in the engine
|
|
68
|
+
if (existsSync(destSkills)) {
|
|
69
|
+
for (const entry of readdirSync(destSkills, { withFileTypes: true })) {
|
|
70
|
+
if (!entry.isDirectory()) continue
|
|
71
|
+
if (!engineSkillNames.has(entry.name)) {
|
|
72
|
+
rmSync(join(destSkills, entry.name), { recursive: true, force: true })
|
|
73
|
+
console.log(` Removed stale skill: ${entry.name}`)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return count
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ── Copy instructions ──
|
|
82
|
+
|
|
83
|
+
function copyInstructions() {
|
|
84
|
+
const srcInstructions = join(engineRoot, 'instructions')
|
|
85
|
+
if (!existsSync(srcInstructions)) {
|
|
86
|
+
console.warn('⚠️ No instructions directory in engine package')
|
|
87
|
+
return 0
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const destInstructions = join(projectRoot, '.github', 'instructions')
|
|
91
|
+
mkdirSync(destInstructions, { recursive: true })
|
|
92
|
+
let count = 0
|
|
93
|
+
|
|
94
|
+
// Collect engine instruction names to detect stale instructions
|
|
95
|
+
const engineInstrNames = new Set()
|
|
96
|
+
for (const entry of readdirSync(srcInstructions, { withFileTypes: true })) {
|
|
97
|
+
if (!entry.isFile() || !entry.name.endsWith('.instructions.md')) continue
|
|
98
|
+
engineInstrNames.add(entry.name)
|
|
99
|
+
copyFileSync(
|
|
100
|
+
join(srcInstructions, entry.name),
|
|
101
|
+
join(destInstructions, entry.name)
|
|
102
|
+
)
|
|
103
|
+
count++
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Remove stale instructions no longer in the engine
|
|
107
|
+
for (const entry of readdirSync(destInstructions, { withFileTypes: true })) {
|
|
108
|
+
if (!entry.isFile() || !entry.name.endsWith('.instructions.md')) continue
|
|
109
|
+
if (!engineInstrNames.has(entry.name)) {
|
|
110
|
+
rmSync(join(destInstructions, entry.name), { force: true })
|
|
111
|
+
console.log(` Removed stale instruction: ${entry.name}`)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return count
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ── Copy AGENTS.md ──
|
|
119
|
+
|
|
120
|
+
function copyAgentsMd() {
|
|
121
|
+
const src = join(engineRoot, 'instructions', 'AGENTS.md')
|
|
122
|
+
if (!existsSync(src)) return false
|
|
123
|
+
copyFileSync(src, join(projectRoot, 'AGENTS.md'))
|
|
124
|
+
return true
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ── Bootstrap state.md ──
|
|
128
|
+
|
|
129
|
+
function bootstrapState(meta) {
|
|
130
|
+
const stateDir = join(projectRoot, '.github', 'memory')
|
|
131
|
+
const statePath = join(stateDir, 'state.md')
|
|
132
|
+
|
|
133
|
+
// Don't overwrite existing state (preserves user's port/url config)
|
|
134
|
+
if (existsSync(statePath)) {
|
|
135
|
+
console.log(' state.md already exists — preserved')
|
|
136
|
+
return
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
mkdirSync(stateDir, { recursive: true })
|
|
140
|
+
const content = `# Deck State
|
|
141
|
+
|
|
142
|
+
## Project
|
|
143
|
+
id: ${meta.id}
|
|
144
|
+
title: ${meta.title}
|
|
145
|
+
|
|
146
|
+
## Dev Server
|
|
147
|
+
port: 5173
|
|
148
|
+
url: http://localhost:5173/
|
|
149
|
+
`
|
|
150
|
+
writeFileSync(statePath, content, 'utf-8')
|
|
151
|
+
console.log(' Created .github/memory/state.md')
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ── Create eyes directory ──
|
|
155
|
+
|
|
156
|
+
function createEyesDir() {
|
|
157
|
+
const eyesDir = join(projectRoot, '.github', 'eyes')
|
|
158
|
+
mkdirSync(eyesDir, { recursive: true })
|
|
159
|
+
|
|
160
|
+
// Add to .gitignore if not already there
|
|
161
|
+
const gitignorePath = join(projectRoot, '.gitignore')
|
|
162
|
+
if (existsSync(gitignorePath)) {
|
|
163
|
+
const gitignore = readFileSync(gitignorePath, 'utf-8')
|
|
164
|
+
if (!gitignore.includes('.github/eyes')) {
|
|
165
|
+
writeFileSync(gitignorePath, gitignore.trimEnd() + '\n.github/eyes/\n', 'utf-8')
|
|
166
|
+
console.log(' Added .github/eyes/ to .gitignore')
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function ensureVSCodeSettings() {
|
|
172
|
+
const vscodeDir = join(projectRoot, '.vscode')
|
|
173
|
+
const settingsPath = join(vscodeDir, 'settings.json')
|
|
174
|
+
|
|
175
|
+
mkdirSync(vscodeDir, { recursive: true })
|
|
176
|
+
|
|
177
|
+
let settings = {}
|
|
178
|
+
if (existsSync(settingsPath)) {
|
|
179
|
+
try {
|
|
180
|
+
settings = JSON.parse(readFileSync(settingsPath, 'utf-8'))
|
|
181
|
+
} catch {
|
|
182
|
+
console.warn('⚠️ Could not parse .vscode/settings.json — leaving it unchanged')
|
|
183
|
+
return
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (settings['simpleBrowser.useIntegratedBrowser'] === true) {
|
|
188
|
+
return
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
settings['simpleBrowser.useIntegratedBrowser'] = true
|
|
192
|
+
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8')
|
|
193
|
+
console.log(' Ensured .vscode/settings.json uses the integrated browser')
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// ── Main ──
|
|
197
|
+
|
|
198
|
+
const meta = readProjectMeta()
|
|
199
|
+
console.log(`\n🎯 Initializing deck project: ${meta.title} (${meta.id})`)
|
|
200
|
+
|
|
201
|
+
const skillCount = copySkills()
|
|
202
|
+
console.log(` Copied ${skillCount} Copilot skills to .github/skills/`)
|
|
203
|
+
|
|
204
|
+
const instrCount = copyInstructions()
|
|
205
|
+
console.log(` Copied ${instrCount} Copilot instructions to .github/instructions/`)
|
|
206
|
+
|
|
207
|
+
const agentsCopied = copyAgentsMd()
|
|
208
|
+
if (agentsCopied) console.log(' Copied AGENTS.md to project root')
|
|
209
|
+
|
|
210
|
+
bootstrapState(meta)
|
|
211
|
+
createEyesDir()
|
|
212
|
+
ensureVSCodeSettings()
|
|
213
|
+
|
|
214
|
+
console.log(`\n✅ Done. Run \`copilot --yolo\` to start editing with Copilot skills.\n`)
|