Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "designmd-pptx",
"version": "2.1.0",
"version": "2.1.1",
"description": "Compile awesome-design-md / Stitch DESIGN.md into officecli PPTX tokens, ordered decks, and staging-safe apply.",
"author": {
"name": "Min-Gul Kim",
Expand Down
4 changes: 2 additions & 2 deletions .grok/skills/officecli-pptx-designmd/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
---
name: officecli-pptx-designmd
description: "designmd-pptx v2.1: compile awesome-design-md / Stitch DESIGN.md into officecli PPTX tokens, ordered decks, staging-safe apply; compose briefs; constraint layout + generative freeform; refine/animate; extract/restyle/reconstruct; a11y + public benchmark; doctor --install; Windows one-file installer; VS Code extension; 60+ patterns; CJK text-fit; Gate 3 vision; render via agent-bridge. Trigger on DESIGN.md, getdesign.md, brand design for slides, deck outline, restyle deck, modernize slides, slide master, potx template, /designmd-pptx, /officecli-pptx-designmd."
description: "designmd-pptx v2.1.1: compile awesome-design-md / Stitch DESIGN.md into officecli PPTX tokens, ordered decks, staging-safe apply; compose briefs; constraint layout + generative freeform; refine/animate; extract/restyle/reconstruct; a11y + public benchmark; doctor --install; Windows one-file installer; VS Code extension; 60+ patterns; CJK text-fit; Gate 3 vision; render via agent-bridge. Trigger on DESIGN.md, getdesign.md, brand design for slides, deck outline, restyle deck, modernize slides, slide master, potx template, /designmd-pptx, /officecli-pptx-designmd."
---

# officecli-pptx-designmd (v2.1)
# officecli-pptx-designmd (v2.1.1)

## Locate the toolkit

Expand Down
30 changes: 30 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,36 @@

All notable changes to designmd-pptx are documented here.

## [2.1.1] — 2026-07-15

Adversarial review follow-up (Codex gpt-5.6-sol@high **BLOCK** findings).

### Security
- **VS Code extension (#45):** run CLI via `child_process.spawn` + `shell:false`
argv array; never `terminal.sendText` of shell-joined user feedback.

### Fixed
- **Animation (#40):** CT_Slide child order (`transition`/`timing` before
`extLst`); numeric slide sort; in-place/`-o` overwrite requires `--force`;
appear uses `animEffect` (no broken `p:set`); `pulse` emphasis removed as
unimplemented.
- **Refine (#19):** deferred continuation inserts so multi-slide density
patches no longer shift indices mid-pass.
- **Generative (#21):** overflow lists/prose preserved under `content.overflow`;
external placements validated on-canvas; `DESIGNMD_LAYOUT_CMD` wired into
freeform path.
- **Extract (#22):** `c:barChart` + `barDir=col` → `column` (not bar).
- **Windows installer (#35):** pin `designmd-pptx==2.1.1`; InstallRoot must
stay under LocalAppData product dir; uninstall requires product manifest;
OfficeCLI extract limited to `officecli.exe` + optional SHA-256.
- **Public benchmark (#42):** recipe-builder smoke per fixture; honest docs
(synthetic deck-spec suite, not rendered PPTX corpus); gate not hard-coded
pass without smoke.

### Changed
- Version **2.1.1**; `npm run check` requires exact version equality across
plugin / package / `__version__`.

## [2.1.0] — 2026-07-15

Phase 5 / **intelligence** release. Production core from v2.0.0 plus generative
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# designmd-pptx

[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
[![Version](https://img.shields.io/badge/version-2.1.0-brightgreen)](plugin.json)
[![Release](https://img.shields.io/badge/release-v2.1.0-blue)](https://github.com/kimmingul/designmd-pptx/releases/tag/v2.1.0)
[![Version](https://img.shields.io/badge/version-2.1.1-brightgreen)](plugin.json)
[![Release](https://img.shields.io/badge/release-v2.1.1-blue)](https://github.com/kimmingul/designmd-pptx/releases/tag/v2.1.1)

**awesome-design-md / Stitch `DESIGN.md` → [OfficeCLI](https://github.com/officecli/officecli) PPTX** — packaged for **Claude Code, OpenAI Codex, and Grok Build** (**v2.1**).

Expand Down
4 changes: 2 additions & 2 deletions docs/install.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,8 @@ Primary **editor surface** (decision: [editor-integration-decision.md](editor-in

```bash
cd editor/vscode
npx @vscode/vsce package --no-dependencies # → designmd-pptx-0.1.0.vsix
code --install-extension designmd-pptx-0.1.0.vsix
npx @vscode/vsce package --no-dependencies # → designmd-pptx-0.2.0.vsix
code --install-extension designmd-pptx-0.2.0.vsix
# Cursor: Extensions → Install from VSIX…
```

Expand Down
5 changes: 5 additions & 0 deletions docs/public-benchmark.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Public benchmark methodology (v1.0.0)

> **Honesty (v2.1.1):** This is a **synthetic deck-spec + recipe-builder smoke
> suite**, not a rendered-PPTX corpus. It does **not** run OfficeCLI apply,
> screenshots, or live Gate 3 vision. “PASS” means a11y auto-correct + recipe
> ops emission succeeded for each fixture.

## Rights

- **License:** CC0-1.0
Expand Down
14 changes: 8 additions & 6 deletions docs/windows-installer.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
# Windows standalone installer (#35)
# Windows one-file bootstrap installer (#35)

Non-developer friendly install of **designmd-pptx** + a **pinned official
OfficeCLI** on Windows 10/11 — without requiring a pre-configured Python
dev environment.
Non-developer friendly **network bootstrap** of **designmd-pptx** + a **pinned
official OfficeCLI** on Windows 10/11. This is **not** an offline MSI that
bundles Python/OfficeCLI; it needs network access (winget/PyPI/GitHub) on first
install.

## One-file installer (primary)

Expand All @@ -20,8 +21,9 @@ powershell -ExecutionPolicy Bypass -File Install-DesignmdPptx.ps1 -DryRun
| `-DryRun` | Print actions only |
| `-SkipOfficeCli` | Skip pinned OfficeCLI download |
| `-SkipPath` | Do not modify user PATH |
| `-PackageSource X` | `designmd-pptx` (default) or local path/wheel |
| `-InstallRoot PATH` | Override `%LOCALAPPDATA%\designmd-pptx` |
| `-PackageSource X` | default `designmd-pptx==2.1.1` (pinned); or local path/wheel |
| `-OfficeCliSha256` | Optional expected SHA-256 of the officecli-dist tarball |
| `-InstallRoot PATH` | Must stay under `%LOCALAPPDATA%\designmd-pptx` |

### Layout

Expand Down
41 changes: 29 additions & 12 deletions editor/vscode/cli.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
/* Pure CLI resolution helpers — no vscode import (unit-testable). */
/* Pure CLI resolution helpers — no vscode import (unit-testable).
*
* Security (#45 adversarial fix): never build shell command strings from
* user input. Callers must spawn with argv + shell:false / Task API.
*/
"use strict";

const path = require("path");
const fs = require("fs");

/**
* @deprecated Prefer resolveCli().argv — kept only for display/debug labels.
* Does NOT make shell-safe strings for user-controlled input.
*/
function quote(s) {
if (/[\s"]/g.test(String(s))) {
return `"${String(s).replace(/"/g, '\\"')}"`;
}
return String(s);
// Escape for display only; never use for shell execution of untrusted input.
return JSON.stringify(String(s));
}

/**
Expand All @@ -19,6 +25,7 @@ function quote(s) {
* @param {string} [opts.pythonPathExtra]
* @param {string[]} opts.args
* @param {NodeJS.ProcessEnv} [opts.env]
* @returns {{ argv: string[], cwd: string, env: NodeJS.ProcessEnv, display: string }}
*/
function resolveCli(opts) {
const root = opts.workspaceRoot || process.cwd();
Expand All @@ -37,15 +44,16 @@ function resolveCli(opts) {
.filter(Boolean)
.join(path.delimiter);
}
let shellCmd;
/** @type {string[]} */
let argv;
if (cliPath) {
shellCmd = [quote(cliPath), ...opts.args.map(quote)].join(" ");
argv = [cliPath, ...opts.args];
} else {
shellCmd = [quote(python), "-m", "designmd_pptx", ...opts.args.map(quote)].join(
" ",
);
argv = [python, "-m", "designmd_pptx", ...opts.args];
}
return { shellCmd, cwd: root, env };
// Display form is JSON-quoted for human terminals only — never pass to a shell.
const display = argv.map((a) => JSON.stringify(String(a))).join(" ");
return { argv, cwd: root, env, display, shellCmd: display };
}

/**
Expand Down Expand Up @@ -89,4 +97,13 @@ function diagnosticsFromReport(data) {
return diags;
}

module.exports = { quote, resolveCli, diagnosticsFromReport };
/**
* Detect shell metacharacters that would be dangerous if argv were joined into a shell.
* Used by unit tests; runtime never shells user args.
* @param {string} s
*/
function hasShellMeta(s) {
return /[$`;&|<>(){}!]/.test(String(s));
}

module.exports = { quote, resolveCli, diagnosticsFromReport, hasShellMeta };
58 changes: 36 additions & 22 deletions editor/vscode/extension.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
const vscode = require("vscode");
const path = require("path");
const fs = require("fs");
const { spawn } = require("child_process");
const { resolveCli: resolveCliPure, diagnosticsFromReport } = require("./cli");

const DIAG_COLLECTION = "designmd-pptx";
Expand Down Expand Up @@ -83,7 +84,7 @@ function workspaceOutput(rel) {

/**
* @param {string[]} args
* @returns {{ shellCmd: string, cwd: string, env: NodeJS.ProcessEnv }}
* @returns {{ argv: string[], cwd: string, env: NodeJS.ProcessEnv, display: string }}
*/
function resolveCli(args) {
const root = workspaceRoot() || process.cwd();
Expand All @@ -98,35 +99,48 @@ function resolveCli(args) {
});
}

function quote(s) {
if (/[\s"]/g.test(String(s))) {
return `"${String(s).replace(/"/g, '\\"')}"`;
}
return String(s);
}

/**
* Run designmd-pptx with argv (shell:false) — never interpolates user input
* into a shell string (adversarial #45 P1).
* @param {string[]} args
* @param {{ reveal?: boolean, title?: string }} [opts]
* @returns {Thenable<void>}
*/
function runCli(args, opts = {}) {
const { shellCmd, cwd, env } = resolveCli(args);
const { argv, cwd, env, display } = resolveCli(args);
const name = opts.title || `designmd-pptx ${args[0] || ""}`.trim();
const terminal =
vscode.window.terminals.find((t) => t.name === "designmd-pptx") ||
vscode.window.createTerminal({ name: "designmd-pptx", cwd, env });
const out = vscode.window.createOutputChannel("designmd-pptx", { log: true });
if (opts.reveal !== false) {
terminal.show(true);
out.show(true);
}
if (env.PYTHONPATH) {
const exp =
process.platform === "win32"
? `$env:PYTHONPATH = '${env.PYTHONPATH.replace(/'/g, "''")}'`
: `export PYTHONPATH=${quote(env.PYTHONPATH)}`;
terminal.sendText(exp, true);
}
terminal.sendText(shellCmd, true);
return vscode.window.setStatusBarMessage(`$(sync~spin) ${name}`, 4000);
out.appendLine(`$ ${display}`);
const status = vscode.window.setStatusBarMessage(`$(sync~spin) ${name}`, 60000);

return new Promise((resolve) => {
const [cmd, ...cmdArgs] = argv;
const child = spawn(cmd, cmdArgs, {
cwd,
env,
shell: false,
windowsHide: true,
});
child.stdout?.on("data", (buf) => out.append(buf.toString()));
child.stderr?.on("data", (buf) => out.append(buf.toString()));
child.on("error", (err) => {
out.appendLine(`error: ${err.message}`);
status.dispose();
vscode.window.showErrorMessage(`designmd-pptx failed to start: ${err.message}`);
resolve();
});
child.on("close", (code) => {
out.appendLine(`\n[exit ${code}]`);
status.dispose();
if (code !== 0) {
vscode.window.showWarningMessage(`${name} exited ${code} — see Output → designmd-pptx`);
}
resolve();
});
});
}

// ── Commands ────────────────────────────────────────────────────────────────
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "designmd-pptx",
"version": "2.1.0",
"version": "2.1.1",
"description": "DESIGN.md → officecli PPTX compiler, packaged for Claude Code / Codex / Grok Build (v2.1)",
"license": "MIT",
"private": true,
Expand Down
58 changes: 50 additions & 8 deletions packaging/windows/Install-DesignmdPptx.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -44,20 +44,35 @@ param(
[switch]$DryRun,
[switch]$SkipOfficeCli,
[switch]$SkipPath,
[string]$PackageSource = "designmd-pptx",
# Pin package by default (adversarial #35) — override with -PackageSource for editable installs
[string]$PackageSource = "designmd-pptx==2.1.1",
[string]$InstallRoot = "",
[string]$OfficeCliPin = "", # empty → read from embedded default / env
[string]$OfficeCliSha256 = "", # optional expected SHA-256 of the tarball
[string]$PythonMin = "3.10"
)

$ErrorActionPreference = "Stop"
$ProductName = "designmd-pptx"
# Keep in sync with python/designmd_pptx/compatibility.json official.recommended
$DefaultOfficeCliPin = "0.2.117"
$DefaultLocalRoot = Join-Path $env:LOCALAPPDATA $ProductName

if (-not $InstallRoot) {
$InstallRoot = Join-Path $env:LOCALAPPDATA $ProductName
$InstallRoot = $DefaultLocalRoot
}
# Guard: install/uninstall root must live under LocalAppData\designmd-pptx
# (or explicit subpath). Blocks recursive delete of arbitrary trees.
function Assert-SafeInstallRoot([string]$root) {
$full = [System.IO.Path]::GetFullPath($root)
$allowed = [System.IO.Path]::GetFullPath($DefaultLocalRoot)
if (-not ($full.Equals($allowed, [System.StringComparison]::OrdinalIgnoreCase) -or
$full.StartsWith($allowed + [IO.Path]::DirectorySeparatorChar,
[System.StringComparison]::OrdinalIgnoreCase))) {
throw "InstallRoot must be under $allowed (got $full). Refusing unsafe path."
}
}
Assert-SafeInstallRoot $InstallRoot
$BinDir = Join-Path $InstallRoot "bin"
$VenvDir = Join-Path $InstallRoot "venv"
$Manifest = Join-Path $InstallRoot "install.manifest.json"
Expand Down Expand Up @@ -214,19 +229,36 @@ function Install-OfficeCliPin {
if (-not (Test-Path $tmp) -or ((Get-Item $tmp).Length -lt 64)) {
throw "Download too small or missing: $tmp"
}
# tar is available on Windows 10+
$expectSha = if ($OfficeCliSha256) { $OfficeCliSha256 } else { $env:DESIGNMD_OFFICECLI_SHA256 }
if ($expectSha) {
$hash = (Get-FileHash -Algorithm SHA256 -Path $tmp).Hash.ToLowerInvariant()
if ($hash -ne $expectSha.ToLowerInvariant()) {
throw "OfficeCLI tarball SHA-256 mismatch: got $hash expected $expectSha"
}
Write-Ok "SHA-256 verified"
} else {
Write-Info "No DESIGNMD_OFFICECLI_SHA256 / -OfficeCliSha256 set — skipping hash verify (pin URL only)"
}
# tar is available on Windows 10+; extract only under our temp root (path safety)
$extract = Join-Path $env:TEMP "designmd-officecli-extract-$pin"
if (Test-Path $extract) { Remove-Item -Recurse -Force $extract }
New-Item -ItemType Directory -Force $extract | Out-Null
tar -xzf $tmp -C $extract
$extractFull = [System.IO.Path]::GetFullPath($extract)
$exe = Get-ChildItem -Path $extract -Recurse -Filter "officecli.exe" -ErrorAction SilentlyContinue |
Where-Object {
$p = [System.IO.Path]::GetFullPath($_.FullName)
$p.StartsWith($extractFull, [System.StringComparison]::OrdinalIgnoreCase)
} |
Select-Object -First 1
if (-not $exe) {
$exe = Get-ChildItem -Path $extract -Recurse -Filter "officecli*" -File -ErrorAction SilentlyContinue |
Where-Object { $_.Name -notmatch '\.tar|\.gz|\.txt|\.md' } |
Select-Object -First 1
throw "Archive has no officecli.exe under extract root (refusing ambiguous officecli* matches)"
}
# Reject zip-slip / path traversal outside extract root
$exeFull = [System.IO.Path]::GetFullPath($exe.FullName)
if (-not $exeFull.StartsWith($extractFull, [System.StringComparison]::OrdinalIgnoreCase)) {
throw "Refusing officecli path outside extract root: $exeFull"
}
if (-not $exe) { throw "Archive has no officecli binary" }
$destOfficial = Join-Path $OfficialDir "officecli.exe"
$destBin = Join-Path $BinDir "officecli.exe"
Copy-Item -Force $exe.FullName $destOfficial
Expand Down Expand Up @@ -357,6 +389,11 @@ function Remove-UserPathEntry {

function Invoke-Uninstall {
Write-Step "Uninstall $ProductName from $InstallRoot"
Assert-SafeInstallRoot $InstallRoot
# Require product marker unless dry-run of empty tree
if (-not $DryRun -and (Test-Path $InstallRoot) -and -not (Test-Path $Manifest)) {
throw "Refusing uninstall: $Manifest missing (not a designmd-pptx install root)"
}
if ($DryRun) {
Write-Info "dry-run: remove $InstallRoot and PATH entry"
return
Expand All @@ -366,7 +403,12 @@ function Invoke-Uninstall {
try {
$m = Get-Content $Manifest -Raw | ConvertFrom-Json
$pathModified = [bool]$m.path_modified
} catch {}
if ($m.product -and $m.product -ne $ProductName) {
throw "Refusing uninstall: manifest product is $($m.product)"
}
} catch {
if ("$_" -match "Refusing") { throw }
}
}
if ($pathModified -or -not $SkipPath) {
Remove-UserPathEntry
Expand Down
2 changes: 1 addition & 1 deletion packaging/windows/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

| Artifact | Role |
|---|---|
| **`Install-DesignmdPptx.ps1`** | **One-file installer** (primary). Run on any Windows 10+ with PowerShell 5.1+. |
| **`Install-DesignmdPptx.ps1`** | **One-file network bootstrap** (primary). Not offline MSI. |
| `Uninstall` via same script (`-Uninstall`) or copied `Uninstall-DesignmdPptx.ps1` | **Uninstall path** |
| `designmd-pptx.iss` + `build-installer.ps1` | Optional **Setup.exe** (Inno Setup 6) GUI wrapper |
| `python/designmd_pptx/win_install.py` | Cross-platform plan/manifest helpers + CLI |
Expand Down
Loading
Loading