@open-aippt/cli 1.3.2
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/LICENSE +21 -0
- package/README.md +38 -0
- package/dist/cli.js +311 -0
- package/package.json +56 -0
- package/template/.agents/skills/apply-comments/SKILL.md +83 -0
- package/template/.agents/skills/create-slide/SKILL.md +91 -0
- package/template/.agents/skills/create-theme/SKILL.md +250 -0
- package/template/.agents/skills/current-slide/SKILL.md +110 -0
- package/template/.agents/skills/slide-authoring/SKILL.md +625 -0
- package/template/AGENTS.md +32 -0
- package/template/README.md +64 -0
- package/template/assets/.gitkeep +0 -0
- package/template/netlify.toml +4 -0
- package/template/open-aippt.config.ts +5 -0
- package/template/package.json +22 -0
- package/template/slides/.folders.json +4 -0
- package/template/slides/getting-started/assets/claude.svg +1 -0
- package/template/slides/getting-started/assets/cloudflare.svg +1 -0
- package/template/slides/getting-started/assets/gemini.svg +1 -0
- package/template/slides/getting-started/assets/openai.svg +1 -0
- package/template/slides/getting-started/assets/opencode.svg +7 -0
- package/template/slides/getting-started/assets/vercel.svg +1 -0
- package/template/slides/getting-started/assets/zeabur.svg +5 -0
- package/template/slides/getting-started/index.tsx +3406 -0
- package/template/themes/.gitkeep +0 -0
- package/template/tsconfig.json +17 -0
- package/template/vercel.json +3 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 aibabelx
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# @open-aippt/cli
|
|
2
|
+
|
|
3
|
+
Scaffold a workspace for [open-aippt](https://github.com/aibabelx/open-aippt) — a React-based slide framework with Claude Code skills preconfigured.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx @open-aippt/cli init my-slide
|
|
9
|
+
cd my-slide
|
|
10
|
+
pnpm install
|
|
11
|
+
pnpm dev
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
This creates a workspace containing:
|
|
15
|
+
|
|
16
|
+
- `slides/getting-started/` — a starter slide you can edit or delete.
|
|
17
|
+
- `package.json` — depends on `@open-aippt/core`, which provides the runtime (home page, slide viewer, fullscreen mode) and the `open-aippt` CLI.
|
|
18
|
+
- `open-aippt.config.ts` — optional typed config (slidesDir, port).
|
|
19
|
+
- `.claude/skills/` and `.agents/skills/` — Claude Code skills (`create-slide`, `apply-comments`, …).
|
|
20
|
+
- `CLAUDE.md` — agent guide for authoring slides.
|
|
21
|
+
|
|
22
|
+
You won't see any Vite, React, or tsconfig files in the workspace. They live inside `@open-aippt/core` and you never touch them.
|
|
23
|
+
|
|
24
|
+
## Commands
|
|
25
|
+
|
|
26
|
+
| Command | Description |
|
|
27
|
+
| --- | --- |
|
|
28
|
+
| `open-aippt init [dir]` | Scaffold a new workspace in `dir` (defaults to current dir). |
|
|
29
|
+
| `open-aippt init --force` | Scaffold into a non-empty directory. |
|
|
30
|
+
| `open-aippt init --name <name>` | Override the generated `package.json` name. |
|
|
31
|
+
|
|
32
|
+
(Once installed in the workspace, `@open-aippt/core` provides `open-aippt dev`, `open-aippt build`, and `open-aippt preview` via its own bin.)
|
|
33
|
+
|
|
34
|
+
## Authoring
|
|
35
|
+
|
|
36
|
+
Inside the scaffolded workspace, slides live under `slides/<kebab-case-id>/index.tsx` and default-export an array of `Page` components. Each page renders into a fixed 1920×1080 canvas; the framework handles scaling.
|
|
37
|
+
|
|
38
|
+
Ask Claude Code to "make slides about X" and the `create-slide` skill will take it from there.
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import { cp, mkdir, readFile, readdir, rm, symlink, writeFile } from "node:fs/promises";
|
|
4
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { Command } from "commander";
|
|
7
|
+
import prompts from "prompts";
|
|
8
|
+
import { spawn } from "node:child_process";
|
|
9
|
+
import { existsSync } from "node:fs";
|
|
10
|
+
|
|
11
|
+
//#region src/git.ts
|
|
12
|
+
const IS_WINDOWS$1 = process.platform === "win32";
|
|
13
|
+
async function run$1(cmd, args, cwd) {
|
|
14
|
+
return new Promise((resolve$1, reject) => {
|
|
15
|
+
const child = spawn(cmd, args, {
|
|
16
|
+
cwd,
|
|
17
|
+
stdio: [
|
|
18
|
+
"ignore",
|
|
19
|
+
"pipe",
|
|
20
|
+
"pipe"
|
|
21
|
+
],
|
|
22
|
+
shell: IS_WINDOWS$1
|
|
23
|
+
});
|
|
24
|
+
let stdout = "";
|
|
25
|
+
let stderr = "";
|
|
26
|
+
child.stdout?.on("data", (d) => {
|
|
27
|
+
stdout += d.toString();
|
|
28
|
+
});
|
|
29
|
+
child.stderr?.on("data", (d) => {
|
|
30
|
+
stderr += d.toString();
|
|
31
|
+
});
|
|
32
|
+
child.on("error", reject);
|
|
33
|
+
child.on("close", (code) => resolve$1({
|
|
34
|
+
code,
|
|
35
|
+
stdout,
|
|
36
|
+
stderr
|
|
37
|
+
}));
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
async function isGitAvailable() {
|
|
41
|
+
try {
|
|
42
|
+
const res = await run$1("git", ["--version"], process.cwd());
|
|
43
|
+
return res.code === 0;
|
|
44
|
+
} catch {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
async function isInsideWorkTree(cwd) {
|
|
49
|
+
try {
|
|
50
|
+
const res = await run$1("git", ["rev-parse", "--is-inside-work-tree"], cwd);
|
|
51
|
+
return res.code === 0 && res.stdout.trim() === "true";
|
|
52
|
+
} catch {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
async function gitInitAndCommit(target) {
|
|
57
|
+
if (!await isGitAvailable()) return {
|
|
58
|
+
status: "skipped-no-git",
|
|
59
|
+
message: "git binary not found on PATH"
|
|
60
|
+
};
|
|
61
|
+
if (await isInsideWorkTree(target)) return {
|
|
62
|
+
status: "skipped-nested",
|
|
63
|
+
message: "target is already inside a git work tree; leaving parent repo alone"
|
|
64
|
+
};
|
|
65
|
+
const init$1 = await run$1("git", ["init"], target);
|
|
66
|
+
if (init$1.code !== 0) return {
|
|
67
|
+
status: "failed",
|
|
68
|
+
message: `git init failed: ${init$1.stderr.trim() || init$1.stdout.trim()}`
|
|
69
|
+
};
|
|
70
|
+
const add = await run$1("git", ["add", "-A"], target);
|
|
71
|
+
if (add.code !== 0) return {
|
|
72
|
+
status: "failed",
|
|
73
|
+
message: `git add failed: ${add.stderr.trim() || add.stdout.trim()}`
|
|
74
|
+
};
|
|
75
|
+
const commit = await run$1("git", [
|
|
76
|
+
"commit",
|
|
77
|
+
"-m",
|
|
78
|
+
"chore: init open-aippt project"
|
|
79
|
+
], target);
|
|
80
|
+
if (commit.code !== 0) return {
|
|
81
|
+
status: "failed",
|
|
82
|
+
message: `git commit failed: ${commit.stderr.trim() || commit.stdout.trim()}`
|
|
83
|
+
};
|
|
84
|
+
return { status: "committed" };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
//#endregion
|
|
88
|
+
//#region src/init.ts
|
|
89
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
90
|
+
const TEMPLATE_DIR = resolve(HERE, "..", "template");
|
|
91
|
+
const IS_WINDOWS = process.platform === "win32";
|
|
92
|
+
function sanitizeDirName(value) {
|
|
93
|
+
const trimmed = value.trim();
|
|
94
|
+
if (trimmed === "." || trimmed === "..") return trimmed;
|
|
95
|
+
const cleaned = trimmed.replace(/\s+/g, "-").replace(/[^\\\p{L}\p{N}_./-]/gu, "-").replace(/-+/g, "-").replace(/(^-|-$)/g, "").replace(/-*([/\\])-*/g, "$1");
|
|
96
|
+
if (cleaned === "" || /^[/\\]+$/.test(cleaned)) return "my-slides";
|
|
97
|
+
return cleaned;
|
|
98
|
+
}
|
|
99
|
+
async function isDirNonEmpty(target) {
|
|
100
|
+
if (!existsSync(target)) return false;
|
|
101
|
+
const entries = await readdir(target);
|
|
102
|
+
return entries.some((e) => !e.startsWith("."));
|
|
103
|
+
}
|
|
104
|
+
function coreVersionRange() {
|
|
105
|
+
return `^1.13.2`;
|
|
106
|
+
}
|
|
107
|
+
async function linkOrCopy(relSrc, dst) {
|
|
108
|
+
await rm(dst, {
|
|
109
|
+
recursive: true,
|
|
110
|
+
force: true
|
|
111
|
+
});
|
|
112
|
+
if (IS_WINDOWS) {
|
|
113
|
+
await cp(resolve(dirname(dst), relSrc), dst, { recursive: true });
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
await symlink(relSrc, dst);
|
|
117
|
+
}
|
|
118
|
+
async function materializeTemplateLinks(target) {
|
|
119
|
+
const claudeMd = join(target, "CLAUDE.md");
|
|
120
|
+
if (!existsSync(claudeMd) && existsSync(join(target, "AGENTS.md"))) await linkOrCopy("AGENTS.md", claudeMd);
|
|
121
|
+
const agentsSkills = join(target, ".agents", "skills");
|
|
122
|
+
if (!existsSync(agentsSkills)) return;
|
|
123
|
+
const claudeSkills = join(target, ".claude", "skills");
|
|
124
|
+
await mkdir(claudeSkills, { recursive: true });
|
|
125
|
+
const entries = await readdir(agentsSkills, { withFileTypes: true });
|
|
126
|
+
for (const entry of entries) {
|
|
127
|
+
if (!entry.isDirectory()) continue;
|
|
128
|
+
await linkOrCopy(join("..", "..", ".agents", "skills", entry.name), join(claudeSkills, entry.name));
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
async function runInstall(pm, cwd) {
|
|
132
|
+
await new Promise((res, rej) => {
|
|
133
|
+
const child = spawn(pm, ["install"], {
|
|
134
|
+
cwd,
|
|
135
|
+
stdio: "inherit",
|
|
136
|
+
shell: IS_WINDOWS
|
|
137
|
+
});
|
|
138
|
+
child.on("error", rej);
|
|
139
|
+
child.on("close", (code) => code === 0 ? res() : rej(new Error(`${pm} install exited with code ${code}`)));
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
async function init(opts) {
|
|
143
|
+
const { dir, force, name, packageManager, install, git } = opts;
|
|
144
|
+
if (!existsSync(TEMPLATE_DIR)) throw new Error(`Template missing at ${TEMPLATE_DIR}. If you are running from source, run \`pnpm --filter @open-aippt/cli build\` first.`);
|
|
145
|
+
const target = resolve(process.cwd(), dir);
|
|
146
|
+
await mkdir(target, { recursive: true });
|
|
147
|
+
if (await isDirNonEmpty(target) && !force) throw new Error(`Target ${target} is not empty. Pass --force to scaffold into it anyway.`);
|
|
148
|
+
await cp(TEMPLATE_DIR, target, { recursive: true });
|
|
149
|
+
await materializeTemplateLinks(target);
|
|
150
|
+
const pkgPath = join(target, "package.json");
|
|
151
|
+
if (existsSync(pkgPath)) {
|
|
152
|
+
const pkg = JSON.parse(await readFile(pkgPath, "utf8"));
|
|
153
|
+
pkg.name = name ?? basename(target);
|
|
154
|
+
pkg.version = "0.0.0";
|
|
155
|
+
pkg.private = true;
|
|
156
|
+
if (pkg.dependencies?.["@open-aippt/core"]) pkg.dependencies["@open-aippt/core"] = coreVersionRange();
|
|
157
|
+
await writeFile(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`);
|
|
158
|
+
}
|
|
159
|
+
await writeFile(join(target, ".gitignore"), "node_modules\ndist\n.DS_Store\n");
|
|
160
|
+
const cdTarget = dir === "." ? basename(target) : dir;
|
|
161
|
+
process.stdout.write(`\n${chalk.green.bold("✔ Created open-aippt workspace")} ${chalk.dim(`in ${target}`)}\n`);
|
|
162
|
+
let installed = false;
|
|
163
|
+
if (install) {
|
|
164
|
+
process.stdout.write(`\n${chalk.bold(`Installing dependencies with ${packageManager}…`)}\n\n`);
|
|
165
|
+
try {
|
|
166
|
+
await runInstall(packageManager, target);
|
|
167
|
+
installed = true;
|
|
168
|
+
} catch (err) {
|
|
169
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
170
|
+
process.stdout.write(`\n${chalk.yellow("! Dependency install failed:")} ${chalk.dim(msg)}\n` + chalk.dim(` You can retry manually with \`${packageManager} install\`.\n`));
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (git) {
|
|
174
|
+
const result = await gitInitAndCommit(target);
|
|
175
|
+
if (result.status === "committed") process.stdout.write(`${chalk.green("✔")} Initialized git repository with first commit.\n`);
|
|
176
|
+
else if (result.status === "skipped-nested") process.stdout.write(`${chalk.yellow("!")} Skipped ${chalk.bold("git init")}: ${chalk.dim(result.message ?? "")}\n`);
|
|
177
|
+
else if (result.status === "skipped-no-git") process.stdout.write(`${chalk.yellow("!")} Skipped git setup: ${chalk.dim(result.message ?? "")}\n`);
|
|
178
|
+
else process.stdout.write(`${chalk.yellow("!")} Git setup failed: ${chalk.dim(result.message ?? "")}\n` + chalk.dim(" You can initialize the repo manually.\n"));
|
|
179
|
+
}
|
|
180
|
+
process.stdout.write(`\n${chalk.bold("Next steps:")}\n`);
|
|
181
|
+
process.stdout.write(` ${chalk.cyan(`cd ${cdTarget}`)}\n`);
|
|
182
|
+
if (!installed && install) process.stdout.write(` ${chalk.cyan(`${packageManager} install`)}\n`);
|
|
183
|
+
else if (!install) process.stdout.write(` ${chalk.cyan(`${packageManager} install`)} ${chalk.dim("# install was skipped")}\n`);
|
|
184
|
+
const devCommand = packageManager === "npm" ? "npm run dev" : `${packageManager} dev`;
|
|
185
|
+
process.stdout.write(` ${chalk.cyan(devCommand)}\n`);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
//#endregion
|
|
189
|
+
//#region src/package-manager.ts
|
|
190
|
+
const PACKAGE_MANAGERS = [
|
|
191
|
+
"npm",
|
|
192
|
+
"pnpm",
|
|
193
|
+
"yarn",
|
|
194
|
+
"bun"
|
|
195
|
+
];
|
|
196
|
+
function detectPackageManager() {
|
|
197
|
+
const ua = process.env.npm_config_user_agent ?? "";
|
|
198
|
+
if (ua.startsWith("pnpm")) return "pnpm";
|
|
199
|
+
if (ua.startsWith("yarn")) return "yarn";
|
|
200
|
+
if (ua.startsWith("bun")) return "bun";
|
|
201
|
+
return "npm";
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
//#endregion
|
|
205
|
+
//#region src/index.ts
|
|
206
|
+
async function readVersion() {
|
|
207
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
208
|
+
const pkg = JSON.parse(await readFile(join(here, "..", "package.json"), "utf8"));
|
|
209
|
+
return pkg.version;
|
|
210
|
+
}
|
|
211
|
+
function onCancel() {
|
|
212
|
+
process.stdout.write(chalk.dim("\nCancelled.\n"));
|
|
213
|
+
process.exit(130);
|
|
214
|
+
}
|
|
215
|
+
function packageManagerFromFlags(flags) {
|
|
216
|
+
const picks = [];
|
|
217
|
+
if (flags.useNpm) picks.push("npm");
|
|
218
|
+
if (flags.usePnpm) picks.push("pnpm");
|
|
219
|
+
if (flags.useYarn) picks.push("yarn");
|
|
220
|
+
if (flags.useBun) picks.push("bun");
|
|
221
|
+
if (picks.length > 1) throw new Error(`Only one of --use-npm / --use-pnpm / --use-yarn / --use-bun may be specified (got ${picks.map((p) => `--use-${p}`).join(", ")}).`);
|
|
222
|
+
return picks[0];
|
|
223
|
+
}
|
|
224
|
+
async function runInit(dirArg, flags) {
|
|
225
|
+
const isTTY = Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
226
|
+
let dir = dirArg;
|
|
227
|
+
const name = flags.name;
|
|
228
|
+
let force = flags.force ?? false;
|
|
229
|
+
let packageManager = packageManagerFromFlags(flags);
|
|
230
|
+
if (isTTY && dir === void 0) {
|
|
231
|
+
const answers = await prompts({
|
|
232
|
+
type: "text",
|
|
233
|
+
name: "dir",
|
|
234
|
+
message: "Target directory",
|
|
235
|
+
initial: "."
|
|
236
|
+
}, { onCancel });
|
|
237
|
+
dir = answers.dir;
|
|
238
|
+
}
|
|
239
|
+
if (dir !== void 0) {
|
|
240
|
+
const safe = sanitizeDirName(dir);
|
|
241
|
+
if (safe !== dir) {
|
|
242
|
+
if (!isTTY) throw new Error(`Target directory "${dir}" contains characters that break shell commands (spaces, quotes, etc.). Try "${safe}" instead.`);
|
|
243
|
+
process.stdout.write(`${chalk.yellow("!")} ${chalk.bold(`"${dir}"`)} has characters that confuse shells.\n Suggested: ${chalk.cyan(`"${safe}"`)}\n`);
|
|
244
|
+
const answers = await prompts({
|
|
245
|
+
type: "text",
|
|
246
|
+
name: "dir",
|
|
247
|
+
message: "Directory name",
|
|
248
|
+
initial: safe
|
|
249
|
+
}, { onCancel });
|
|
250
|
+
dir = sanitizeDirName(answers.dir ?? safe);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
if (isTTY && packageManager === void 0 && flags.install !== false) {
|
|
254
|
+
const detected = detectPackageManager();
|
|
255
|
+
const answers = await prompts({
|
|
256
|
+
type: "select",
|
|
257
|
+
name: "packageManager",
|
|
258
|
+
message: "Package manager",
|
|
259
|
+
choices: PACKAGE_MANAGERS.map((pm) => ({
|
|
260
|
+
title: pm,
|
|
261
|
+
value: pm
|
|
262
|
+
})),
|
|
263
|
+
initial: PACKAGE_MANAGERS.indexOf(detected)
|
|
264
|
+
}, { onCancel });
|
|
265
|
+
packageManager = answers.packageManager;
|
|
266
|
+
}
|
|
267
|
+
const resolvedDir = dir ?? ".";
|
|
268
|
+
const target = resolve(process.cwd(), resolvedDir);
|
|
269
|
+
if (!force && await isDirNonEmpty(target)) {
|
|
270
|
+
if (!isTTY) throw new Error(`Target ${target} is not empty. Pass --force to scaffold into it anyway.`);
|
|
271
|
+
const { overwrite } = await prompts({
|
|
272
|
+
type: "confirm",
|
|
273
|
+
name: "overwrite",
|
|
274
|
+
message: `${chalk.yellow(target)} is not empty. Scaffold into it anyway?`,
|
|
275
|
+
initial: false
|
|
276
|
+
}, { onCancel });
|
|
277
|
+
if (!overwrite) {
|
|
278
|
+
process.stdout.write(chalk.dim("Aborted.\n"));
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
force = true;
|
|
282
|
+
}
|
|
283
|
+
const opts = {
|
|
284
|
+
dir: resolvedDir,
|
|
285
|
+
force,
|
|
286
|
+
name,
|
|
287
|
+
packageManager: packageManager ?? detectPackageManager(),
|
|
288
|
+
install: flags.install !== false,
|
|
289
|
+
git: flags.git !== false
|
|
290
|
+
};
|
|
291
|
+
await init(opts);
|
|
292
|
+
}
|
|
293
|
+
async function run(argv) {
|
|
294
|
+
const version = await readVersion();
|
|
295
|
+
const program = new Command();
|
|
296
|
+
program.name("open-aippt").description("Scaffold and manage open-aippt workspaces.").version(version, "-v, --version", "print version").helpOption("-h, --help", "show help").showHelpAfterError(chalk.dim("(run `open-aippt --help` for usage)"));
|
|
297
|
+
program.command("init").description("Create a new open-aippt workspace").argument("[dir]", "target directory", void 0).option("-f, --force", "overwrite non-empty target directory", false).option("-n, --name <name>", "override package name (defaults to folder name)").option("--use-npm", "use npm to install dependencies").option("--use-pnpm", "use pnpm to install dependencies").option("--use-yarn", "use yarn to install dependencies").option("--use-bun", "use bun to install dependencies").option("--no-install", "skip dependency installation").option("--no-git", "skip git init and initial commit").action(async (dir, flags) => {
|
|
298
|
+
await runInit(dir, flags);
|
|
299
|
+
});
|
|
300
|
+
await program.parseAsync(argv, { from: "user" });
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
//#endregion
|
|
304
|
+
//#region src/cli.ts
|
|
305
|
+
run(process.argv.slice(2)).catch((err) => {
|
|
306
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
307
|
+
process.stderr.write(`${chalk.red("error:")} ${message}\n`);
|
|
308
|
+
process.exit(1);
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
//#endregion
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@open-aippt/cli",
|
|
3
|
+
"version": "1.3.2",
|
|
4
|
+
"description": "Scaffold an open-aippt workspace with Claude Code skills preconfigured.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"open-aippt": "dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"template",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=18"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"slides",
|
|
19
|
+
"presentation",
|
|
20
|
+
"claude-code",
|
|
21
|
+
"scaffold"
|
|
22
|
+
],
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"author": {
|
|
25
|
+
"name": "aibabelx",
|
|
26
|
+
"url": "https://github.com/aibabelx"
|
|
27
|
+
},
|
|
28
|
+
"homepage": "https://open-aippt.dev",
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "git+https://github.com/aibabelx/open-aippt.git",
|
|
32
|
+
"directory": "packages/cli"
|
|
33
|
+
},
|
|
34
|
+
"bugs": {
|
|
35
|
+
"url": "https://github.com/aibabelx/open-aippt/issues"
|
|
36
|
+
},
|
|
37
|
+
"publishConfig": {
|
|
38
|
+
"access": "public"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"chalk": "^5.3.0",
|
|
42
|
+
"commander": "^15.0.0",
|
|
43
|
+
"prompts": "^2.4.2"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@types/node": "^22.19.17",
|
|
47
|
+
"@types/prompts": "^2.4.9",
|
|
48
|
+
"tsdown": "^0.9.9",
|
|
49
|
+
"typescript": "^5.9.3"
|
|
50
|
+
},
|
|
51
|
+
"scripts": {
|
|
52
|
+
"build": "tsdown",
|
|
53
|
+
"typecheck": "tsc --noEmit",
|
|
54
|
+
"sync:template-skills": "node scripts/sync-template-skills.mjs"
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: apply-comments
|
|
3
|
+
description: Apply pending @slide-comment markers written by the open-aippt inspector tool. Use when the user asks to "apply comments", "process slide comments", "apply the inspector comments", or references markers left inside `slides/<id>/index.tsx`.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Apply slide comments
|
|
7
|
+
|
|
8
|
+
The open-aippt editor has an inspector tool that lets the user click on a rendered page element and attach a textual comment (e.g. *"make this red"*, *"change to 'open-aippt Rocks'"*). Each comment is persisted as an in-source JSX marker inside `slides/<slideId>/index.tsx`.
|
|
9
|
+
|
|
10
|
+
Your job: read those markers, perform the described edits, and delete the markers.
|
|
11
|
+
|
|
12
|
+
> **Before making any page edit**, consult the **`slide-authoring`** skill — it is the technical reference for how `slides/<id>/index.tsx` is structured (canvas, type scale, palette, assets, file contract). A comment like *"make this bigger"* or *"change the accent colour"* should be applied in a way that stays consistent with those rules.
|
|
13
|
+
|
|
14
|
+
## Marker format
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
{/* @slide-comment id="c-<8hex>" ts="<ISO>" text="<base64url(JSON)>" */}
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
- Always sits on its own line as the **first child inside** the JSX element it refers to (i.e. between that element's opening `>` and its other children). The marker is dropped *into* its target, not floated above it.
|
|
21
|
+
- `text` is base64url-encoded JSON: `{"note": "...", "hint"?: "..."}`.
|
|
22
|
+
- Detection regex (authoritative — use exactly this):
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
/\{\/\*\s*@slide-comment\s+id="(c-[a-f0-9]+)"\s+ts="([^"]+)"\s+text="([A-Za-z0-9_\-]+={0,2})"\s*\*\/\}/g
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Procedure
|
|
29
|
+
|
|
30
|
+
1. **Identify the target slide(s).**
|
|
31
|
+
- If the user names one (`example-slide`, `youbike-3-survey`, etc.), work on that single `slides/<slideId>/index.tsx`.
|
|
32
|
+
- If they say "all" or don't specify, scan every `slides/*/index.tsx`. Process each slide one at a time.
|
|
33
|
+
|
|
34
|
+
2. **Read the file and find all markers.**
|
|
35
|
+
- Run the regex above against the whole file.
|
|
36
|
+
- For each match, base64url-decode `text` and `JSON.parse` it to get `{ note, hint? }`.
|
|
37
|
+
- Record each hit as `{ id, lineIndex (0-based), indent, note, hint }`.
|
|
38
|
+
- If there are no markers, tell the user and stop.
|
|
39
|
+
|
|
40
|
+
3. **Understand each comment in context.**
|
|
41
|
+
- The targeted JSX element is the **enclosing** element of the marker — i.e. read upward from the marker line until you reach the unclosed JSX opening tag whose body the marker lives in. That element is the target. (For self-closing elements like `<img />`, the inspector hoists the marker to the nearest non-self-closing ancestor; in that case the comment usually refers to a child of the enclosing element rather than the enclosing element itself — use the `note` text to disambiguate.)
|
|
42
|
+
- Read enough surrounding code (parent element, sibling elements, inline styles) to apply the change faithfully. A comment inside a `<div>` with an inline `background` style usually refers to that element's styling, for example.
|
|
43
|
+
- If the `note` is ambiguous, do the smallest reasonable interpretation and mention the assumption in your summary.
|
|
44
|
+
|
|
45
|
+
4. **Apply edits in reverse line order.**
|
|
46
|
+
- Sort markers by descending `lineIndex` and process one at a time, using the `Edit` tool.
|
|
47
|
+
- Processing top-down would invalidate line numbers for later markers as the file shrinks/grows.
|
|
48
|
+
|
|
49
|
+
5. **Remove each marker after applying its edit.**
|
|
50
|
+
- Delete the entire marker line including its trailing `\n`.
|
|
51
|
+
- Never leave a marker behind. An un-removed marker signals a failure.
|
|
52
|
+
|
|
53
|
+
6. **Verify.**
|
|
54
|
+
- After all edits, re-read the file and confirm zero remaining markers.
|
|
55
|
+
- Run `pnpm tsc --noEmit` and `pnpm biome check` (or `pnpm lint`). Fix any introduced errors.
|
|
56
|
+
|
|
57
|
+
7. **Report.**
|
|
58
|
+
- Summarise: `N applied, 0 remaining` plus a one-line description of each change (including the slide id).
|
|
59
|
+
|
|
60
|
+
## base64url decoding helper
|
|
61
|
+
|
|
62
|
+
```js
|
|
63
|
+
function decode(s) {
|
|
64
|
+
const pad = s.length % 4 === 0 ? '' : '='.repeat(4 - (s.length % 4));
|
|
65
|
+
return Buffer.from(s.replace(/-/g, '+').replace(/_/g, '/') + pad, 'base64').toString('utf8');
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
You can run this inline via `node -e '...'` if you need to inspect a payload; otherwise just reason about the decoded string.
|
|
70
|
+
|
|
71
|
+
## Edge cases
|
|
72
|
+
|
|
73
|
+
- **Marker with no enclosing JSX element** (shouldn't happen — the inspector won't write one — but if you find one): delete it and note as orphan.
|
|
74
|
+
- **Multiple markers stacked on consecutive lines inside the same element**: they all refer to that enclosing element. Apply them in source order but still delete each line individually.
|
|
75
|
+
- **`_debugSource` used SWC instead of Babel**: not your problem — the marker line is authoritative.
|
|
76
|
+
- **Comment asks for something outside the target element's scope** (e.g. "add a new page"): do the closest-reasonable edit and mention the scope expansion in your summary.
|
|
77
|
+
- **Can't resolve the comment** (e.g. truly ambiguous, or the file changed shape such that the target element doesn't exist): leave the marker in place and report it as skipped. Don't guess.
|
|
78
|
+
|
|
79
|
+
## Do not
|
|
80
|
+
|
|
81
|
+
- Do not touch `package.json`, `open-aippt.config.ts`, or files outside `slides/`.
|
|
82
|
+
- Do not add dependencies.
|
|
83
|
+
- Do not re-introduce markers or leave `TODO` breadcrumbs — the user already has a record in git.
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: create-slide
|
|
3
|
+
description: Use this skill when the user wants to create, draft, author, or generate new slides / a presentation in this open-aippt repo. Triggers on phrases like "make slides about X", "create a presentation", "draft slides for", "new slide", or when the user asks to add content under `slides/`. Do NOT use for editing the framework itself — only for authoring content inside `slides/<id>/`.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Create a slide in open-aippt
|
|
7
|
+
|
|
8
|
+
This skill owns the **workflow** for drafting a new deck. The technical reference — file contract, 1920×1080 canvas, type scale, palette, layout, assets — lives in the **`slide-authoring`** skill. Read that skill whenever you need details on *how* a page is structured. This skill assumes you'll consult it before writing code.
|
|
9
|
+
|
|
10
|
+
You only write files under `slides/<id>/`. Never modify `package.json`, `open-aippt.config.ts`, or existing slides.
|
|
11
|
+
|
|
12
|
+
## Step 1 — Pick a theme
|
|
13
|
+
|
|
14
|
+
List files under `themes/`. If any theme markdown files exist (anything other than `README.md`), call `AskUserQuestion` with each theme id as an option plus a final **"no theme — design from scratch"** option.
|
|
15
|
+
|
|
16
|
+
- If the user picks a theme: read `themes/<id>.md` end-to-end. The theme's palette, typography, layout, and Title/Footer components are now authoritative — copy them directly into the slide. **Also set `theme: '<theme-id>'` on the `meta` export in `index.tsx`** (e.g. `export const meta: SlideMeta = { title: '…', theme: '<theme-id>' };`) so the slide back-links to the theme (chip on the slide card + listing on `/themes/<id>`). In Step 2, skip the **aesthetic direction** question (the theme already commits to one direction); you still need the topic itself, so confirm it before moving on. Page count, text density, and motion are independent of theme — ask those normally.
|
|
17
|
+
- If the user picks "no theme", or `themes/` is empty (or contains only `README.md`): proceed to Step 2 unchanged.
|
|
18
|
+
|
|
19
|
+
If you skip the aesthetic question because a theme was picked, restate the theme name in Step 2 so the user can correct course before you start writing.
|
|
20
|
+
|
|
21
|
+
## Step 2 — Clarify requirements (MUST ask before writing code)
|
|
22
|
+
|
|
23
|
+
**Before writing any code, lock in the four key style decisions below via `AskUserQuestion`.** They shape every downstream choice (layout, type scale, asset needs, motion code), so locking them in up front avoids rework. Only skip a question when the user's original message already gave an unambiguous answer for it — and if you skip, restate your assumption so they can correct it.
|
|
24
|
+
|
|
25
|
+
**Topic comes first.** A meaningful aesthetic recommendation requires knowing what the deck is about. If the user's initial request is thin ("make me a deck", "draft some slides"), make a *separate* `AskUserQuestion` call first to gather topic, audience, and any draft outline. Skip this only if the topic is already clear from the user's message — in which case restate your reading of the topic in the next call so they can correct course.
|
|
26
|
+
|
|
27
|
+
Then ask these four in a single `AskUserQuestion` call (multi-question form):
|
|
28
|
+
|
|
29
|
+
1. **Aesthetic direction** — propose 3 visual directions tailored to *this* topic. Do **not** pull from a fixed preset list. Each option must combine a vibe word + a concrete visual cue (palette, typography, motif) so the user can picture it; bare labels like "minimal" or "corporate" alone are too vague. The three options should feel meaningfully different from each other — not three flavors of the same idea.
|
|
30
|
+
|
|
31
|
+
How options should shift with topic:
|
|
32
|
+
- *"Intro to Rust for backend engineers"* → **rust-orange technical editorial** (warm rust/charcoal, mono headings, code-grid layout) · **blueprint dev-doc** (cyan grid on near-black, monospace, schematic feel) · **brutalist terminal** (lime-on-black, ASCII rules, no-nonsense)
|
|
33
|
+
- *"Q2 product roadmap for stakeholders"* → **calm corporate clean** (off-white, single accent, generous whitespace) · **confident editorial** (large display serif, tight grid, one bold accent) · **data-forward dashboard** (charts as hero, muted neutrals + status colors)
|
|
34
|
+
- *"Kindergarten parent night"* → **playful crayon** (paper texture, hand-drawn accents, primary colors) · **soft pastel storybook** (peach/mint, rounded type, illustrated icons) · **warm photo-led** (full-bleed kid photos, simple captions)
|
|
35
|
+
|
|
36
|
+
Mark the option that best fits the topic and audience as "(Recommended)" so the user has a sensible default. (`AskUserQuestion` already auto-adds "Other" — don't add a generic catch-all yourself.)
|
|
37
|
+
|
|
38
|
+
2. **Page count** — rough length. Offer brackets: 3–5 (short), 6–10 (standard), 11–20 (deep dive), custom.
|
|
39
|
+
3. **Text density per page** — how much copy lives on each page? Offer: minimal (one line / big number), light (heading + 2–3 bullets), standard (heading + 4–5 bullets or short paragraph), dense (multi-column / detailed). This directly drives type scale and layout.
|
|
40
|
+
4. **Motion** — does the user want CSS/React animations and transitions, or a fully static deck? Offer: static (no motion), subtle (fades / entrance only), rich (keyframes, staggered reveals, looping visuals). If animated, plan to use CSS `@keyframes` / inline `style` + `useEffect`; no extra libraries.
|
|
41
|
+
|
|
42
|
+
After those four, ask follow-ups **only if still unclear**: brand colors, required assets. Don't pad the conversation with questions already answered.
|
|
43
|
+
|
|
44
|
+
## Step 3 — Pick a slide id
|
|
45
|
+
|
|
46
|
+
Use **kebab-case**, short, descriptive. Examples: `rust-intro`, `q2-roadmap`, `team-offsite-2026`. Check `slides/` to avoid collisions.
|
|
47
|
+
|
|
48
|
+
## Step 4 — Plan the structure
|
|
49
|
+
|
|
50
|
+
Sketch the slide as a list of page roles before writing code. Common page types:
|
|
51
|
+
|
|
52
|
+
| Role | Purpose |
|
|
53
|
+
| ---------------- | --------------------------------------------- |
|
|
54
|
+
| Cover | Title + subtitle, strong visual |
|
|
55
|
+
| Agenda | What's coming (3–5 items) |
|
|
56
|
+
| Section divider | Big label between chapters |
|
|
57
|
+
| Content | Heading + 2–5 bullets OR heading + one visual |
|
|
58
|
+
| Big number | One statistic the size of the canvas |
|
|
59
|
+
| Quote | Pull-quote with attribution |
|
|
60
|
+
| Comparison | Two-column before/after or A vs B |
|
|
61
|
+
| Closing | CTA, thanks, contact |
|
|
62
|
+
|
|
63
|
+
**Rule of thumb**: one idea per page. If you're tempted to put two, split them.
|
|
64
|
+
|
|
65
|
+
If the deck topic naturally calls for specific real images the user must supply (product screenshots, team photos, customer dashboards), plan where those go and use `<ImagePlaceholder>` from `@open-aippt/core` — see the **Image placeholders** section in `slide-authoring`. Default is **no placeholders**: only insert one when a real image is genuinely required.
|
|
66
|
+
|
|
67
|
+
## Step 5 — Commit to a visual direction
|
|
68
|
+
|
|
69
|
+
Pick one coherent palette / type scale / aesthetic and hold it across every page. The full set of constraints (palette structure, type scale, padding, aesthetic options) lives in `slide-authoring` — apply it.
|
|
70
|
+
|
|
71
|
+
**Default: declare a top-level `export const design: DesignSystem = { … }`** at the top of `index.tsx` (after imports) using the chosen palette / type scale, and reference the values via `var(--osd-X)` from inline styles. This keeps the slide tweakable from the Design panel after generation, which is what the user almost always wants. Only skip the `design` const for a one-off slide whose palette is intentionally locked and not meant to be re-themed — in that case, fall back to the local `palette` constants pattern. The "Design system" section of `slide-authoring` covers the format and available tokens.
|
|
72
|
+
|
|
73
|
+
Consult the `frontend-design` skill for deeper aesthetic guidance if the user wants something bold.
|
|
74
|
+
|
|
75
|
+
## Step 6 — Write `slides/<id>/index.tsx`
|
|
76
|
+
|
|
77
|
+
Read the **`slide-authoring`** skill before writing — it covers the file contract, canvas rules, type scale, spacing, and asset imports, and it includes a starter template you can copy. Don't duplicate that knowledge here; use it.
|
|
78
|
+
|
|
79
|
+
## Step 7 — Self-review
|
|
80
|
+
|
|
81
|
+
Run the checklist in `slide-authoring` ("Self-review before finishing"). It covers structural correctness, layout discipline, and asset existence.
|
|
82
|
+
|
|
83
|
+
## Step 8 — Hand off to the user
|
|
84
|
+
|
|
85
|
+
Tell the user:
|
|
86
|
+
|
|
87
|
+
- The slide id and file path you created.
|
|
88
|
+
- That the dev server will hot-reload — they can open `http://localhost:5173/s/<id>` (or refresh the home page).
|
|
89
|
+
- If dev isn't running: `pnpm dev` from the repo root.
|
|
90
|
+
|
|
91
|
+
Don't run the dev server yourself unless asked.
|