@liustack/pptwise 0.22.0 → 0.24.0
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/README.md +72 -41
- package/README.zh-CN.md +72 -41
- package/dist/{chunk-M35M4QUC.js → chunk-2CA3JRFR.js} +460 -499
- package/dist/chunk-2CA3JRFR.js.map +1 -0
- package/dist/{chunk-WL5KWYKS.js → chunk-G7WQ7KSP.js} +34800 -30625
- package/dist/chunk-G7WQ7KSP.js.map +1 -0
- package/dist/{chunk-VUOLBHD7.js → chunk-RQUKZYSH.js} +1 -1
- package/dist/{chunk-VUOLBHD7.js.map → chunk-RQUKZYSH.js.map} +1 -1
- package/dist/{chunk-3ZUKISTY.js → chunk-VEZGVYJS.js} +2 -2
- package/dist/cli.js +911 -462
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +5320 -1534
- package/dist/index.js +15 -13
- package/dist/node.d.ts +1 -1
- package/dist/node.js +2 -2
- package/dist/{pixel-audit-H5K6JK3X.js → pixel-audit-M5Q5WCST.js} +4 -4
- package/dist/pixel-audit-M5Q5WCST.js.map +1 -0
- package/dist/{registry-C0GJH7ZT.d.ts → registry-D8bkC8ff.d.ts} +1 -1
- package/dsh/preview-tool.js +20 -17
- package/package.json +2 -1
- package/skills/pptwise/SKILL.md +133 -70
- package/skills/pptwise/SKILL.zh-CN.md +136 -73
- package/skills/pptwise/references/branding.md +34 -8
- package/skills/pptwise/references/branding.zh-CN.md +36 -8
- package/skills/pptwise/references/components.md +63 -33
- package/skills/pptwise/references/components.zh-CN.md +63 -33
- package/skills/pptwise/references/density.md +23 -9
- package/skills/pptwise/references/density.zh-CN.md +23 -9
- package/skills/pptwise/references/images.md +26 -13
- package/skills/pptwise/references/images.zh-CN.md +26 -13
- package/skills/pptwise/references/layouts.md +30 -26
- package/skills/pptwise/references/layouts.zh-CN.md +30 -26
- package/skills/pptwise/references/spec.md +143 -69
- package/skills/pptwise/references/spec.zh-CN.md +143 -69
- package/skills/pptwise/references/validate.md +33 -45
- package/skills/pptwise/references/validate.zh-CN.md +33 -45
- package/skills/pptwise/scripts/run.ps1 +1 -1
- package/skills/pptwise/scripts/run.sh +1 -1
- package/dist/chunk-M35M4QUC.js.map +0 -1
- package/dist/chunk-WL5KWYKS.js.map +0 -1
- package/dist/pixel-audit-H5K6JK3X.js.map +0 -1
- /package/dist/{chunk-3ZUKISTY.js.map → chunk-VEZGVYJS.js.map} +0 -0
package/dist/cli.js
CHANGED
|
@@ -1,66 +1,63 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
-
PptxIRV3Schema,
|
|
4
3
|
VERSION,
|
|
5
4
|
assembleDeck,
|
|
6
5
|
buildAssetBrief,
|
|
6
|
+
copyThemePreset,
|
|
7
7
|
disassembleDeck,
|
|
8
8
|
extractBrandTheme,
|
|
9
|
+
forkTheme,
|
|
9
10
|
formatInvalidSpecError,
|
|
10
|
-
migrateBannerHeadingToTwoColumn,
|
|
11
|
-
migrateBloomToClassroom,
|
|
12
|
-
migrateChromeToBranding,
|
|
13
|
-
migrateDeckPlanToSpec,
|
|
14
|
-
migrateIrV3ToV4,
|
|
15
|
-
migrateLogoWallToImageGrid,
|
|
16
11
|
parseBrandThemeFile,
|
|
17
|
-
registerBrandThemeFile,
|
|
18
12
|
resolveSpecThemeId,
|
|
19
13
|
slugify,
|
|
20
14
|
specJsonSchema,
|
|
21
15
|
validateSpec
|
|
22
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-2CA3JRFR.js";
|
|
23
17
|
import {
|
|
24
18
|
installNodePlatform,
|
|
25
19
|
isMissingModuleError,
|
|
26
20
|
proxyFetch
|
|
27
|
-
} from "./chunk-
|
|
21
|
+
} from "./chunk-VEZGVYJS.js";
|
|
28
22
|
import {
|
|
29
23
|
AUDIENCE_VALUES,
|
|
30
24
|
CANONICAL_THEME_IDS,
|
|
31
25
|
CANVAS_H_PX,
|
|
32
26
|
CANVAS_W_PX,
|
|
33
27
|
FORMAT_BY_MIME,
|
|
28
|
+
LAYOUT_REGISTRY,
|
|
34
29
|
MIME_BY_SNIFFED_FORMAT,
|
|
35
30
|
NARRATIVE_PRESETS,
|
|
36
31
|
PACING_BUDGETS,
|
|
37
32
|
PptwiseError,
|
|
38
33
|
STRATEGY_DEFINITIONS,
|
|
39
|
-
|
|
40
|
-
|
|
34
|
+
THEME_ID_CONSTRAINT,
|
|
35
|
+
THEME_ID_PATTERN,
|
|
36
|
+
THEME_OCCASIONS,
|
|
37
|
+
ThemeFileSchema,
|
|
41
38
|
auditDeck,
|
|
42
39
|
formatIssues,
|
|
43
40
|
formatWarnings,
|
|
44
41
|
generatePptx,
|
|
45
|
-
|
|
42
|
+
getThemeDefinition,
|
|
43
|
+
installThemeFile,
|
|
46
44
|
irJsonSchema,
|
|
47
45
|
listThemes,
|
|
48
46
|
renderSlideSvg,
|
|
49
47
|
resolveNarrative,
|
|
50
48
|
sniffImageFormat,
|
|
51
|
-
styleJsonSchema,
|
|
52
49
|
validateIr
|
|
53
|
-
} from "./chunk-
|
|
50
|
+
} from "./chunk-G7WQ7KSP.js";
|
|
54
51
|
import {
|
|
55
52
|
getPlatform
|
|
56
|
-
} from "./chunk-
|
|
53
|
+
} from "./chunk-RQUKZYSH.js";
|
|
57
54
|
|
|
58
55
|
// src/cli.ts
|
|
59
56
|
import { Command } from "commander";
|
|
60
57
|
|
|
61
58
|
// src/cli/commands.ts
|
|
62
|
-
import { mkdir as
|
|
63
|
-
import { basename as
|
|
59
|
+
import { mkdir as mkdir5, readFile as readFile6, rm as rm2, writeFile as writeFile4 } from "fs/promises";
|
|
60
|
+
import { basename as basename5, dirname as dirname5, join as join7, relative as relative2, resolve as resolve7 } from "path";
|
|
64
61
|
|
|
65
62
|
// src/cli/config.ts
|
|
66
63
|
import { readFile as readFile2 } from "fs/promises";
|
|
@@ -119,8 +116,8 @@ function pptwiseHome(opts = {}) {
|
|
|
119
116
|
}
|
|
120
117
|
function migrateLegacyHomes(home, nextDir) {
|
|
121
118
|
if (existsSync(nextDir)) return;
|
|
122
|
-
for (const
|
|
123
|
-
const legacy = join(home,
|
|
119
|
+
for (const dirname8 of LEGACY_HOME_DIRNAMES) {
|
|
120
|
+
const legacy = join(home, dirname8);
|
|
124
121
|
if (existsSync(legacy)) {
|
|
125
122
|
copyLegacyHome(legacy, nextDir);
|
|
126
123
|
return;
|
|
@@ -448,14 +445,10 @@ function missingKeysError(kind) {
|
|
|
448
445
|
|
|
449
446
|
// src/cli/config.ts
|
|
450
447
|
var ConfigSchema = z2.object({
|
|
451
|
-
theme: z2.string().optional(),
|
|
452
|
-
style: StyleOverrideSchema.optional(),
|
|
453
448
|
decksDir: z2.string().optional(),
|
|
454
449
|
outDir: z2.string().optional()
|
|
455
450
|
}).strict();
|
|
456
451
|
var UserConfigSchema = z2.object({
|
|
457
|
-
theme: z2.string().optional(),
|
|
458
|
-
style: StyleOverrideSchema.optional(),
|
|
459
452
|
decksDir: z2.string().optional(),
|
|
460
453
|
images: ImagesConfigSchema.optional()
|
|
461
454
|
}).strict();
|
|
@@ -606,7 +599,6 @@ async function resolveLocalAssets(ir, baseDir, workspaceAssetsDir) {
|
|
|
606
599
|
}
|
|
607
600
|
|
|
608
601
|
// src/cli/deck-dir.ts
|
|
609
|
-
var PLAN_FILENAME = "deck.plan.json";
|
|
610
602
|
var SPEC_FILENAME = "deck.spec.json";
|
|
611
603
|
var PAGES_DIRNAME = "pages";
|
|
612
604
|
var ASSETS_DIRNAME = "assets";
|
|
@@ -657,18 +649,6 @@ function expectedLayoutHint() {
|
|
|
657
649
|
}
|
|
658
650
|
async function readSpecFile(dir) {
|
|
659
651
|
const specPath = join3(dir, SPEC_FILENAME);
|
|
660
|
-
const planPath = join3(dir, PLAN_FILENAME);
|
|
661
|
-
const [specExists, planExists] = await Promise.all([pathExists(specPath), pathExists(planPath)]);
|
|
662
|
-
if (specExists && planExists) {
|
|
663
|
-
throw new PptwiseError(
|
|
664
|
-
`both ${SPEC_FILENAME} and ${PLAN_FILENAME} exist in ${dir} \u2014 ambiguous, refusing to guess which one wins. Delete ${PLAN_FILENAME} once you have confirmed ${SPEC_FILENAME} is correct (\`pptwise migrate\` never deletes the source file it read)`
|
|
665
|
-
);
|
|
666
|
-
}
|
|
667
|
-
if (!specExists && planExists) {
|
|
668
|
-
throw new PptwiseError(
|
|
669
|
-
`${dir} has ${PLAN_FILENAME} but no ${SPEC_FILENAME} \u2014 deck project directories now use ${SPEC_FILENAME}. Run \`pptwise migrate ${dir} -o ${dir}\` to convert it`
|
|
670
|
-
);
|
|
671
|
-
}
|
|
672
652
|
let text;
|
|
673
653
|
try {
|
|
674
654
|
text = await readFile4(specPath, "utf8");
|
|
@@ -729,14 +709,26 @@ async function scanAssets(dir) {
|
|
|
729
709
|
}
|
|
730
710
|
return images2;
|
|
731
711
|
}
|
|
712
|
+
function specThemeFromRaw(spec2) {
|
|
713
|
+
if (typeof spec2 !== "object" || spec2 === null) return void 0;
|
|
714
|
+
const theme2 = spec2.theme;
|
|
715
|
+
return typeof theme2 === "string" ? theme2 : void 0;
|
|
716
|
+
}
|
|
732
717
|
async function readDeckDir(dir) {
|
|
733
718
|
const deckDir = resolve4(dir);
|
|
719
|
+
const specPath = join3(deckDir, SPEC_FILENAME);
|
|
734
720
|
const spec2 = await readSpecFile(deckDir);
|
|
721
|
+
const specTheme = specThemeFromRaw(spec2);
|
|
735
722
|
const pages = await readPages(deckDir);
|
|
736
|
-
const { ir
|
|
723
|
+
const { ir } = assembleDeck(spec2, pages);
|
|
737
724
|
const images2 = await scanAssets(deckDir);
|
|
738
725
|
const merged = { ...ir, assets: { images: { ...ir.assets.images, ...images2 } } };
|
|
739
|
-
return {
|
|
726
|
+
return {
|
|
727
|
+
ir: merged,
|
|
728
|
+
deckDir,
|
|
729
|
+
specPath,
|
|
730
|
+
...specTheme !== void 0 ? { specTheme } : {}
|
|
731
|
+
};
|
|
740
732
|
}
|
|
741
733
|
async function writeDeckAssets(images2, outDir, sourceBaseDir) {
|
|
742
734
|
const entries = Object.entries(images2);
|
|
@@ -778,6 +770,36 @@ async function writeOneAsset(id, src, assetsDir, sourceBaseDir) {
|
|
|
778
770
|
}
|
|
779
771
|
}
|
|
780
772
|
|
|
773
|
+
// src/cli/theme-write.ts
|
|
774
|
+
import { link, mkdir as mkdir3, rename, rm, writeFile as writeFile3 } from "fs/promises";
|
|
775
|
+
import { basename as basename3, dirname as dirname2, join as join4 } from "path";
|
|
776
|
+
import { randomBytes } from "crypto";
|
|
777
|
+
async function writeThemeFile(path2, file, force = false, hooks) {
|
|
778
|
+
const dir = dirname2(path2);
|
|
779
|
+
await mkdir3(dir, { recursive: true });
|
|
780
|
+
const tmp = join4(dir, `.${basename3(path2)}.${randomBytes(8).toString("hex")}.tmp`);
|
|
781
|
+
try {
|
|
782
|
+
await writeFile3(tmp, JSON.stringify(file, null, 2) + "\n");
|
|
783
|
+
await hooks?.beforePublish?.(tmp, path2);
|
|
784
|
+
if (force) {
|
|
785
|
+
await rename(tmp, path2);
|
|
786
|
+
return;
|
|
787
|
+
}
|
|
788
|
+
try {
|
|
789
|
+
await link(tmp, path2);
|
|
790
|
+
} catch (error) {
|
|
791
|
+
if (error.code === "EEXIST") {
|
|
792
|
+
throw new PptwiseError(`theme file already exists: ${path2}. Pass --force to overwrite.`);
|
|
793
|
+
}
|
|
794
|
+
throw error;
|
|
795
|
+
}
|
|
796
|
+
await rm(tmp, { force: true });
|
|
797
|
+
} catch (error) {
|
|
798
|
+
await rm(tmp, { force: true });
|
|
799
|
+
throw error;
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
|
|
781
803
|
// src/lib/slide-edge.ts
|
|
782
804
|
var TAG = /<(\/?)(g|rect)\b([^>]*?)(\/?)>/g;
|
|
783
805
|
var ATTR_CACHE = /* @__PURE__ */ new Map();
|
|
@@ -1195,6 +1217,61 @@ ${findingsDataScript}
|
|
|
1195
1217
|
</html>
|
|
1196
1218
|
`;
|
|
1197
1219
|
}
|
|
1220
|
+
var CONTACT_SHEET_CSS = `
|
|
1221
|
+
:root { color-scheme: light; }
|
|
1222
|
+
* { box-sizing: border-box; }
|
|
1223
|
+
html, body { margin: 0; padding: 0; }
|
|
1224
|
+
body {
|
|
1225
|
+
font: 14px/1.4 -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
|
|
1226
|
+
color: #111;
|
|
1227
|
+
background: #f4f4f1;
|
|
1228
|
+
padding: 24px;
|
|
1229
|
+
}
|
|
1230
|
+
h1 { font-size: 18px; font-weight: 600; margin: 0 0 16px; }
|
|
1231
|
+
table.cs { width: 100%; border-collapse: collapse; table-layout: fixed; }
|
|
1232
|
+
table.cs th, table.cs td { vertical-align: top; padding: 8px; }
|
|
1233
|
+
table.cs thead th { text-align: left; font-size: 13px; font-weight: 600; }
|
|
1234
|
+
table.cs tbody th { text-align: left; font-weight: 500; color: #555; width: 88px; }
|
|
1235
|
+
.cs-cell svg { display: block; width: 100%; height: auto; background: #fff; }
|
|
1236
|
+
`.trim();
|
|
1237
|
+
function buildContactSheetHtml(input) {
|
|
1238
|
+
const { title, themes } = input;
|
|
1239
|
+
const rowLabels = [];
|
|
1240
|
+
for (const col of themes) {
|
|
1241
|
+
for (const slide of col.slides) {
|
|
1242
|
+
const label = slide.label ?? slide.type;
|
|
1243
|
+
if (!rowLabels.includes(label)) rowLabels.push(label);
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
const head = themes.map((t) => `<th scope="col">${escapeHtml(t.id)}</th>`).join("");
|
|
1247
|
+
const body = rowLabels.map((label) => {
|
|
1248
|
+
const cells = themes.map((t, colIdx) => {
|
|
1249
|
+
const slide = t.slides.find((s) => (s.label ?? s.type) === label);
|
|
1250
|
+
if (!slide) return "<td></td>";
|
|
1251
|
+
const svg = namespaceSvgIds(slide.svg, `t${colIdx}-${label}-`);
|
|
1252
|
+
return `<td class="cs-cell">${svg}</td>`;
|
|
1253
|
+
}).join("");
|
|
1254
|
+
return `<tr><th scope="row">${escapeHtml(label)}</th>${cells}</tr>`;
|
|
1255
|
+
}).join("");
|
|
1256
|
+
const escapedTitle = escapeHtml(title);
|
|
1257
|
+
return `<!doctype html>
|
|
1258
|
+
<html lang="en">
|
|
1259
|
+
<head>
|
|
1260
|
+
<meta charset="utf-8">
|
|
1261
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
1262
|
+
<title>${escapedTitle} \u2014 theme comparison</title>
|
|
1263
|
+
<style>${CONTACT_SHEET_CSS}</style>
|
|
1264
|
+
</head>
|
|
1265
|
+
<body>
|
|
1266
|
+
<h1>${escapedTitle}</h1>
|
|
1267
|
+
<table class="cs">
|
|
1268
|
+
<thead><tr><th></th>${head}</tr></thead>
|
|
1269
|
+
<tbody>${body}</tbody>
|
|
1270
|
+
</table>
|
|
1271
|
+
</body>
|
|
1272
|
+
</html>
|
|
1273
|
+
`;
|
|
1274
|
+
}
|
|
1198
1275
|
|
|
1199
1276
|
// src/cli/preview-manifest.ts
|
|
1200
1277
|
var PREVIEW_MANIFEST_VERSION = 1;
|
|
@@ -1261,8 +1338,8 @@ function buildPreviewManifest(input) {
|
|
|
1261
1338
|
|
|
1262
1339
|
// src/cli/workspace.ts
|
|
1263
1340
|
import { existsSync as existsSync2 } from "fs";
|
|
1264
|
-
import { appendFile, mkdir as
|
|
1265
|
-
import { basename as
|
|
1341
|
+
import { appendFile, mkdir as mkdir4, readFile as readFile5, readdir as readdir2, stat as stat2, unlink } from "fs/promises";
|
|
1342
|
+
import { basename as basename4, dirname as dirname3, extname as extname3, join as join5, resolve as resolve5 } from "path";
|
|
1266
1343
|
|
|
1267
1344
|
// src/cli/child.ts
|
|
1268
1345
|
import {
|
|
@@ -1288,14 +1365,14 @@ function envValue(env, name) {
|
|
|
1288
1365
|
}
|
|
1289
1366
|
async function findOnPath(bin, env, deps = {}) {
|
|
1290
1367
|
const platform = deps.platform ?? process.platform;
|
|
1291
|
-
const
|
|
1368
|
+
const join12 = deps.join ?? defaultJoin;
|
|
1292
1369
|
const delimiter = deps.delimiter ?? defaultDelimiter;
|
|
1293
1370
|
const tryAccess = deps.access ?? ((target, mode) => fsAccess(target, mode));
|
|
1294
1371
|
const dirs = (envValue(env, "PATH") ?? "").split(delimiter).filter(Boolean);
|
|
1295
1372
|
const suffixes = platform === "win32" ? [...(envValue(env, "PATHEXT") ?? DEFAULT_PATHEXT).split(";").map((ext) => ext.trim()).filter(Boolean), ""] : [""];
|
|
1296
1373
|
for (const dir of dirs) {
|
|
1297
1374
|
for (const suffix of suffixes) {
|
|
1298
|
-
const full =
|
|
1375
|
+
const full = join12(dir, `${bin}${suffix}`);
|
|
1299
1376
|
try {
|
|
1300
1377
|
await tryAccess(full, constants.X_OK);
|
|
1301
1378
|
return full;
|
|
@@ -1567,11 +1644,11 @@ var ChildTimeoutError = class extends Error {
|
|
|
1567
1644
|
async function runChild(command, args = [], options = {}) {
|
|
1568
1645
|
const { timeoutMs, signal, ...spawnOptions } = options;
|
|
1569
1646
|
const cwd = typeof spawnOptions.cwd === "string" ? spawnOptions.cwd : void 0;
|
|
1570
|
-
const
|
|
1571
|
-
return new Promise((
|
|
1572
|
-
const child = spawnHidden(
|
|
1647
|
+
const plan = await resolveSpawnPlan(command, args, spawnOptions.env ?? process.env, cwd);
|
|
1648
|
+
return new Promise((resolve9, reject) => {
|
|
1649
|
+
const child = spawnHidden(plan.command, plan.args, {
|
|
1573
1650
|
...spawnOptions,
|
|
1574
|
-
env:
|
|
1651
|
+
env: plan.env ?? spawnOptions.env,
|
|
1575
1652
|
stdio: ["ignore", "pipe", "pipe"]
|
|
1576
1653
|
});
|
|
1577
1654
|
let stdout = "";
|
|
@@ -1595,7 +1672,7 @@ async function runChild(command, args = [], options = {}) {
|
|
|
1595
1672
|
reject(new ChildTimeoutError(timeoutMs ?? 0));
|
|
1596
1673
|
return;
|
|
1597
1674
|
}
|
|
1598
|
-
|
|
1675
|
+
resolve9({ code: code ?? 0, stdout, stderr });
|
|
1599
1676
|
};
|
|
1600
1677
|
const restartDrain = () => {
|
|
1601
1678
|
if (!exited || settled) return;
|
|
@@ -1644,33 +1721,33 @@ var WORKSPACE_DIRNAME = ".pptwise";
|
|
|
1644
1721
|
var LEGACY_WORKSPACE_DIRNAMES = [".pptpress", ".pptfast"];
|
|
1645
1722
|
var WORKSPACE_IGNORE_ENTRY = `${WORKSPACE_DIRNAME}/`;
|
|
1646
1723
|
function defaultWorkspaceDirname(anchor) {
|
|
1647
|
-
if (existsSync2(
|
|
1724
|
+
if (existsSync2(join5(anchor, WORKSPACE_DIRNAME))) return WORKSPACE_DIRNAME;
|
|
1648
1725
|
for (const name of LEGACY_WORKSPACE_DIRNAMES) {
|
|
1649
|
-
if (existsSync2(
|
|
1726
|
+
if (existsSync2(join5(anchor, name))) return name;
|
|
1650
1727
|
}
|
|
1651
1728
|
return WORKSPACE_DIRNAME;
|
|
1652
1729
|
}
|
|
1653
|
-
function ignoreEntryFor(
|
|
1654
|
-
return `${
|
|
1730
|
+
function ignoreEntryFor(dirname8) {
|
|
1731
|
+
return `${dirname8}/`;
|
|
1655
1732
|
}
|
|
1656
1733
|
var RENDERED_SVG_PATTERN = /^\d{3}-[a-z-]+\.svg$/;
|
|
1657
1734
|
function deckSlug(target, isDir) {
|
|
1658
|
-
const base =
|
|
1735
|
+
const base = basename4(target);
|
|
1659
1736
|
const name = isDir ? base : base.slice(0, base.length - extname3(base).length);
|
|
1660
1737
|
const slug = slugify(name, "deck");
|
|
1661
1738
|
assertSafeFileSegment(slug, "deck slug");
|
|
1662
1739
|
return slug;
|
|
1663
1740
|
}
|
|
1664
1741
|
function resolveWorkspaceRoot(opts) {
|
|
1665
|
-
const anchor = opts.projectConfigPath ?
|
|
1742
|
+
const anchor = opts.projectConfigPath ? dirname3(resolve5(opts.projectConfigPath)) : resolve5(opts.cwd);
|
|
1666
1743
|
const configured = opts.outDir !== void 0;
|
|
1667
|
-
const root = opts.outDir !== void 0 ? resolve5(anchor, opts.outDir) :
|
|
1744
|
+
const root = opts.outDir !== void 0 ? resolve5(anchor, opts.outDir) : join5(anchor, defaultWorkspaceDirname(anchor));
|
|
1668
1745
|
return { anchor, root, configured };
|
|
1669
1746
|
}
|
|
1670
1747
|
function resolveWorkspaceLocation(opts) {
|
|
1671
1748
|
const { anchor, root, configured } = resolveWorkspaceRoot(opts);
|
|
1672
1749
|
const slug = deckSlug(opts.target, opts.isDir);
|
|
1673
|
-
return { anchor, root, dir:
|
|
1750
|
+
return { anchor, root, dir: join5(root, slug), slug, configured };
|
|
1674
1751
|
}
|
|
1675
1752
|
var runGitDefault = async (args, cwd) => {
|
|
1676
1753
|
try {
|
|
@@ -1692,7 +1769,7 @@ async function gitIgnoreStatus(dir, entry, runGit = runGitDefault) {
|
|
|
1692
1769
|
async function inspectWorkspace(opts) {
|
|
1693
1770
|
const { anchor, root, configured } = resolveWorkspaceRoot(opts);
|
|
1694
1771
|
if (configured) return { anchor, root, configured, ignore: "skipped" };
|
|
1695
|
-
const ignore = await gitIgnoreStatus(anchor, ignoreEntryFor(
|
|
1772
|
+
const ignore = await gitIgnoreStatus(anchor, ignoreEntryFor(basename4(root)), opts.runGit);
|
|
1696
1773
|
return { anchor, root, configured, ignore };
|
|
1697
1774
|
}
|
|
1698
1775
|
async function ensureGitIgnored(dir, entry, runGit = runGitDefault) {
|
|
@@ -1701,9 +1778,9 @@ async function ensureGitIgnored(dir, entry, runGit = runGitDefault) {
|
|
|
1701
1778
|
if (status === "not-a-repo") return { kind: "no-repo" };
|
|
1702
1779
|
const common = await runGit(["rev-parse", "--git-common-dir"], dir);
|
|
1703
1780
|
if (common === null || common.code !== 0 || common.stdout.trim() === "") return { kind: "no-repo" };
|
|
1704
|
-
const excludePath =
|
|
1781
|
+
const excludePath = join5(resolve5(dir, common.stdout.trim()), "info", "exclude");
|
|
1705
1782
|
try {
|
|
1706
|
-
await
|
|
1783
|
+
await mkdir4(dirname3(excludePath), { recursive: true });
|
|
1707
1784
|
let existing = "";
|
|
1708
1785
|
try {
|
|
1709
1786
|
existing = await readFile5(excludePath, "utf8");
|
|
@@ -1729,7 +1806,7 @@ async function exists(path2) {
|
|
|
1729
1806
|
async function prepareWorkspaceDir(location, opts = {}) {
|
|
1730
1807
|
const rootExisted = await exists(location.root);
|
|
1731
1808
|
try {
|
|
1732
|
-
await
|
|
1809
|
+
await mkdir4(location.dir, { recursive: true });
|
|
1733
1810
|
} catch (e) {
|
|
1734
1811
|
throw new PptwiseError(
|
|
1735
1812
|
`cannot create the output directory ${location.dir}: ${e.message}
|
|
@@ -1737,7 +1814,7 @@ async function prepareWorkspaceDir(location, opts = {}) {
|
|
|
1737
1814
|
);
|
|
1738
1815
|
}
|
|
1739
1816
|
if (rootExisted || location.configured || opts.gitIgnore === false) return [];
|
|
1740
|
-
const ignoreEntry = ignoreEntryFor(
|
|
1817
|
+
const ignoreEntry = ignoreEntryFor(basename4(location.root));
|
|
1741
1818
|
const outcome = await ensureGitIgnored(location.anchor, ignoreEntry, opts.runGit);
|
|
1742
1819
|
if (outcome.kind === "appended") {
|
|
1743
1820
|
return [
|
|
@@ -1759,7 +1836,7 @@ async function pruneRenderedSvgs(dir) {
|
|
|
1759
1836
|
return 0;
|
|
1760
1837
|
}
|
|
1761
1838
|
const stale = entries.filter((name) => RENDERED_SVG_PATTERN.test(name));
|
|
1762
|
-
await Promise.all(stale.map((name) => unlink(
|
|
1839
|
+
await Promise.all(stale.map((name) => unlink(join5(dir, name))));
|
|
1763
1840
|
return stale.length;
|
|
1764
1841
|
}
|
|
1765
1842
|
var WORKSPACE_IMAGE_EXTS = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".gif", ".webp"]);
|
|
@@ -1776,7 +1853,7 @@ async function scanWorkspaceAssets(assetsDir) {
|
|
|
1776
1853
|
for (const name of names) {
|
|
1777
1854
|
const ext = extname3(name).toLowerCase();
|
|
1778
1855
|
if (ext === ".json" || !WORKSPACE_IMAGE_EXTS.has(ext)) continue;
|
|
1779
|
-
const id =
|
|
1856
|
+
const id = basename4(name, extname3(name));
|
|
1780
1857
|
const previous = sourceFile.get(id);
|
|
1781
1858
|
if (previous !== void 0) {
|
|
1782
1859
|
throw new PptwiseError(
|
|
@@ -1784,63 +1861,336 @@ async function scanWorkspaceAssets(assetsDir) {
|
|
|
1784
1861
|
);
|
|
1785
1862
|
}
|
|
1786
1863
|
sourceFile.set(id, name);
|
|
1787
|
-
images2[id] = { src:
|
|
1864
|
+
images2[id] = { src: join5(assetsDir, name) };
|
|
1788
1865
|
}
|
|
1789
1866
|
return images2;
|
|
1790
1867
|
}
|
|
1791
1868
|
|
|
1792
|
-
// src/cli/
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
if (
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1869
|
+
// src/cli/theme-resolve.ts
|
|
1870
|
+
import { dirname as dirname4, join as join6, resolve as resolve6 } from "path";
|
|
1871
|
+
var WORKSPACE_THEMES_DIRNAME = "themes";
|
|
1872
|
+
function sortKeysDeep(value) {
|
|
1873
|
+
if (Array.isArray(value)) return value.map(sortKeysDeep);
|
|
1874
|
+
if (value !== null && typeof value === "object") {
|
|
1875
|
+
const sorted = {};
|
|
1876
|
+
for (const key of Object.keys(value).sort()) {
|
|
1877
|
+
sorted[key] = sortKeysDeep(value[key]);
|
|
1878
|
+
}
|
|
1879
|
+
return sorted;
|
|
1800
1880
|
}
|
|
1801
|
-
return
|
|
1881
|
+
return value;
|
|
1802
1882
|
}
|
|
1803
|
-
|
|
1883
|
+
function menusEqual(a, b) {
|
|
1884
|
+
return JSON.stringify(sortKeysDeep(a)) === JSON.stringify(sortKeysDeep(b));
|
|
1885
|
+
}
|
|
1886
|
+
function assertThemeId(id) {
|
|
1887
|
+
if (!THEME_ID_PATTERN.test(id)) {
|
|
1888
|
+
throw new PptwiseError(`invalid theme id "${id}". ${THEME_ID_CONSTRAINT}`);
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1891
|
+
async function readThemeFile(path2) {
|
|
1804
1892
|
const raw = await loadIrFile(path2, "theme");
|
|
1805
|
-
return
|
|
1893
|
+
return parseBrandThemeFile(raw, path2);
|
|
1894
|
+
}
|
|
1895
|
+
function isCanonicalThemeId(name) {
|
|
1896
|
+
return CANONICAL_THEME_IDS.includes(name);
|
|
1897
|
+
}
|
|
1898
|
+
function menuForThemeId(id) {
|
|
1899
|
+
const def = getThemeDefinition(id);
|
|
1900
|
+
return def.menu;
|
|
1806
1901
|
}
|
|
1807
|
-
|
|
1808
|
-
const
|
|
1809
|
-
|
|
1902
|
+
function publicStyle(style, id) {
|
|
1903
|
+
const colors = { ...style.colors };
|
|
1904
|
+
const fonts = {
|
|
1905
|
+
heading: [...style.fonts.heading],
|
|
1906
|
+
body: [...style.fonts.body]
|
|
1907
|
+
};
|
|
1908
|
+
if (style.fonts.mono !== void 0) fonts.mono = [...style.fonts.mono];
|
|
1909
|
+
const out = {
|
|
1910
|
+
id,
|
|
1911
|
+
colors,
|
|
1912
|
+
fonts,
|
|
1913
|
+
defaultBackgrounds: structuredClone(style.defaultBackgrounds)
|
|
1914
|
+
};
|
|
1915
|
+
if (style.allowCustomBackground !== void 0) out.allowCustomBackground = style.allowCustomBackground;
|
|
1916
|
+
if (style.shape !== void 0) {
|
|
1917
|
+
const shape = {};
|
|
1918
|
+
if (style.shape.radius !== void 0) shape.radius = style.shape.radius;
|
|
1919
|
+
if (style.shape.gapScale !== void 0) shape.gapScale = style.shape.gapScale;
|
|
1920
|
+
if (style.shape.typeScale !== void 0) shape.typeScale = style.shape.typeScale;
|
|
1921
|
+
if (Object.keys(shape).length > 0) out.shape = shape;
|
|
1922
|
+
}
|
|
1923
|
+
return out;
|
|
1924
|
+
}
|
|
1925
|
+
function themeFileFromPreset(presetId, identity) {
|
|
1926
|
+
const copy = copyThemePreset(presetId, identity.id);
|
|
1927
|
+
const file = {
|
|
1928
|
+
version: 2,
|
|
1929
|
+
id: identity.id,
|
|
1930
|
+
label: identity.label ?? copy.label,
|
|
1931
|
+
style: publicStyle(copy.style, identity.id),
|
|
1932
|
+
brand: copy.brand,
|
|
1933
|
+
occasions: copy.occasions !== void 0 ? [...copy.occasions] : void 0,
|
|
1934
|
+
identity: copy.identity,
|
|
1935
|
+
menu: copy.menu
|
|
1936
|
+
};
|
|
1937
|
+
return ThemeFileSchema.parse(file);
|
|
1938
|
+
}
|
|
1939
|
+
async function tryParseThemeFile(path2) {
|
|
1940
|
+
const raw = await loadIrFile(path2, "theme");
|
|
1941
|
+
const parsed = ThemeFileSchema.safeParse(raw);
|
|
1942
|
+
return parsed.success ? parsed.data : void 0;
|
|
1943
|
+
}
|
|
1944
|
+
async function acceptThemeFile(path2, file) {
|
|
1945
|
+
installThemeFile(file);
|
|
1946
|
+
return { kind: "file", id: file.id, path: path2, file };
|
|
1947
|
+
}
|
|
1948
|
+
async function acceptIfNameMatches(path2, name, file) {
|
|
1949
|
+
if (file.id !== name) return void 0;
|
|
1950
|
+
return acceptThemeFile(path2, file);
|
|
1951
|
+
}
|
|
1952
|
+
async function resolveDeckThemeFile(deckDir, name) {
|
|
1953
|
+
const boundPath = join6(deckDir, THEME_FILENAME);
|
|
1954
|
+
if (await pathExists(boundPath)) {
|
|
1955
|
+
const file = await readThemeFile(boundPath);
|
|
1956
|
+
const hit = await acceptIfNameMatches(boundPath, name, file);
|
|
1957
|
+
if (hit !== void 0) return hit;
|
|
1958
|
+
}
|
|
1959
|
+
const namedTheme = join6(deckDir, `${name}.theme.json`);
|
|
1960
|
+
if (await pathExists(namedTheme)) {
|
|
1961
|
+
const file = await readThemeFile(namedTheme);
|
|
1962
|
+
const hit = await acceptIfNameMatches(namedTheme, name, file);
|
|
1963
|
+
if (hit !== void 0) return hit;
|
|
1964
|
+
}
|
|
1965
|
+
const namedJson = join6(deckDir, `${name}.json`);
|
|
1966
|
+
if (await pathExists(namedJson)) {
|
|
1967
|
+
const file = await tryParseThemeFile(namedJson);
|
|
1968
|
+
if (file !== void 0) {
|
|
1969
|
+
const hit = await acceptIfNameMatches(namedJson, name, file);
|
|
1970
|
+
if (hit !== void 0) return hit;
|
|
1971
|
+
}
|
|
1972
|
+
}
|
|
1973
|
+
return void 0;
|
|
1974
|
+
}
|
|
1975
|
+
async function resolveWorkspaceThemeFile(startDir, name) {
|
|
1976
|
+
let dir = resolve6(startDir);
|
|
1977
|
+
for (; ; ) {
|
|
1978
|
+
const candidates = [
|
|
1979
|
+
join6(dir, WORKSPACE_THEMES_DIRNAME, `${name}.theme.json`),
|
|
1980
|
+
join6(dir, WORKSPACE_THEMES_DIRNAME, `${name}.json`),
|
|
1981
|
+
join6(dir, WORKSPACE_THEMES_DIRNAME, name, THEME_FILENAME)
|
|
1982
|
+
];
|
|
1983
|
+
for (const candidate of candidates) {
|
|
1984
|
+
if (!await pathExists(candidate)) continue;
|
|
1985
|
+
const isLooseJson = candidate.endsWith(`${name}.json`) && !candidate.endsWith(`${name}.theme.json`);
|
|
1986
|
+
if (isLooseJson) {
|
|
1987
|
+
const file2 = await tryParseThemeFile(candidate);
|
|
1988
|
+
if (file2 !== void 0) {
|
|
1989
|
+
const hit2 = await acceptIfNameMatches(candidate, name, file2);
|
|
1990
|
+
if (hit2 !== void 0) return hit2;
|
|
1991
|
+
}
|
|
1992
|
+
continue;
|
|
1993
|
+
}
|
|
1994
|
+
const file = await readThemeFile(candidate);
|
|
1995
|
+
const hit = await acceptIfNameMatches(candidate, name, file);
|
|
1996
|
+
if (hit !== void 0) return hit;
|
|
1997
|
+
}
|
|
1998
|
+
const parent = dirname4(dir);
|
|
1999
|
+
if (parent === dir) return void 0;
|
|
2000
|
+
dir = parent;
|
|
2001
|
+
}
|
|
1810
2002
|
}
|
|
1811
|
-
function
|
|
1812
|
-
|
|
1813
|
-
if (
|
|
1814
|
-
|
|
1815
|
-
|
|
2003
|
+
async function resolveThemeByName(name, opts) {
|
|
2004
|
+
assertThemeId(name);
|
|
2005
|
+
if (opts.deckDir !== void 0) {
|
|
2006
|
+
const deckHit = await resolveDeckThemeFile(opts.deckDir, name);
|
|
2007
|
+
if (deckHit !== void 0) return deckHit;
|
|
2008
|
+
}
|
|
2009
|
+
const workspaceHit = await resolveWorkspaceThemeFile(opts.startDir, name);
|
|
2010
|
+
if (workspaceHit !== void 0) return workspaceHit;
|
|
2011
|
+
if (isCanonicalThemeId(name)) return { kind: "builtin", id: name };
|
|
2012
|
+
const places = [
|
|
2013
|
+
opts.deckDir !== void 0 ? `deck directory ${opts.deckDir}` : void 0,
|
|
2014
|
+
`workspace ${WORKSPACE_THEMES_DIRNAME}/ walking up from ${resolve6(opts.startDir)}`,
|
|
2015
|
+
"built-in presets"
|
|
2016
|
+
].filter((place) => place !== void 0);
|
|
2017
|
+
throw new PptwiseError(`unknown theme "${name}". Searched ${places.join(", ")}.`);
|
|
2018
|
+
}
|
|
2019
|
+
var REBIND_SUFFIX = "A same-menu color fork is allowed. A different menu is a new theme. Start over from the theme layer (keep intent, narrative, and harvested materials, rewrite the spec).";
|
|
2020
|
+
async function assertThemeRebind(deckDir, resolved) {
|
|
2021
|
+
if (deckDir === void 0) return;
|
|
2022
|
+
const boundPath = resolve6(join6(deckDir, THEME_FILENAME));
|
|
2023
|
+
if (!await pathExists(boundPath)) return;
|
|
2024
|
+
if (resolved.kind === "file" && resolve6(resolved.path) === boundPath) return;
|
|
2025
|
+
const bound = parseBrandThemeFile(await loadIrFile(boundPath, "theme"), boundPath);
|
|
2026
|
+
const nextId = resolved.id;
|
|
2027
|
+
const nextMenu = resolved.kind === "file" ? resolved.file.menu : menuForThemeId(resolved.id);
|
|
2028
|
+
if (menusEqual(bound.menu, nextMenu)) return;
|
|
2029
|
+
throw new PptwiseError(`cannot rebind theme "${bound.id}" to "${nextId}": menus differ. ${REBIND_SUFFIX}`);
|
|
2030
|
+
}
|
|
2031
|
+
async function registerThemeSelection(name, opts) {
|
|
2032
|
+
if (name === void 0 || name.length === 0) return void 0;
|
|
2033
|
+
return resolveThemeByName(name, opts);
|
|
2034
|
+
}
|
|
2035
|
+
function themeNameFromUnknown(raw) {
|
|
2036
|
+
if (typeof raw !== "object" || raw === null) return void 0;
|
|
2037
|
+
const theme2 = raw.theme;
|
|
2038
|
+
return typeof theme2 === "string" ? theme2 : void 0;
|
|
2039
|
+
}
|
|
2040
|
+
|
|
2041
|
+
// src/cli/fixtures/theme-try-sample.ts
|
|
2042
|
+
var THEME_TRY_SAMPLE_IR = {
|
|
2043
|
+
version: "5",
|
|
2044
|
+
filename: "northstar-review",
|
|
2045
|
+
theme: { id: "consulting" },
|
|
2046
|
+
slides: [
|
|
2047
|
+
{
|
|
2048
|
+
type: "cover",
|
|
2049
|
+
heading: "Northstar Review",
|
|
2050
|
+
subheading: "Q3 operating report for the board"
|
|
2051
|
+
},
|
|
2052
|
+
{
|
|
2053
|
+
type: "chapter",
|
|
2054
|
+
heading: "The Quarter"
|
|
2055
|
+
},
|
|
2056
|
+
{
|
|
2057
|
+
type: "content",
|
|
2058
|
+
kind: "points",
|
|
2059
|
+
heading: "What we decided",
|
|
2060
|
+
components: [
|
|
2061
|
+
{
|
|
2062
|
+
type: "bullets",
|
|
2063
|
+
items: [
|
|
2064
|
+
"Hold hiring at 42",
|
|
2065
|
+
"Cut the analytics rebuild",
|
|
2066
|
+
"Share the support rotation"
|
|
2067
|
+
]
|
|
2068
|
+
}
|
|
2069
|
+
]
|
|
2070
|
+
},
|
|
2071
|
+
{
|
|
2072
|
+
type: "content",
|
|
2073
|
+
kind: "list",
|
|
2074
|
+
heading: "Open workstreams",
|
|
2075
|
+
components: [
|
|
2076
|
+
{
|
|
2077
|
+
type: "bullets",
|
|
2078
|
+
items: [
|
|
2079
|
+
"EU residency addendum",
|
|
2080
|
+
"Partner invite flow",
|
|
2081
|
+
"Invoice localization"
|
|
2082
|
+
]
|
|
2083
|
+
}
|
|
2084
|
+
]
|
|
2085
|
+
},
|
|
2086
|
+
{
|
|
2087
|
+
type: "content",
|
|
2088
|
+
kind: "comparison",
|
|
2089
|
+
heading: "Build vs buy",
|
|
2090
|
+
components: [
|
|
2091
|
+
{
|
|
2092
|
+
type: "comparison",
|
|
2093
|
+
columns: ["Build", "Buy"],
|
|
2094
|
+
rows: [
|
|
2095
|
+
{ label: "Time to first report", cells: ["14 weeks", "3 weeks"] },
|
|
2096
|
+
{ label: "Year-one cost", cells: ["$420k", "$180k"] },
|
|
2097
|
+
{ label: "Lock-in", cells: ["None", "Vendor roadmap"] }
|
|
2098
|
+
]
|
|
2099
|
+
}
|
|
2100
|
+
]
|
|
2101
|
+
},
|
|
2102
|
+
{
|
|
2103
|
+
type: "content",
|
|
2104
|
+
kind: "process",
|
|
2105
|
+
heading: "How we ship",
|
|
2106
|
+
components: [
|
|
2107
|
+
{
|
|
2108
|
+
type: "steps",
|
|
2109
|
+
items: [
|
|
2110
|
+
{ title: "Scope", text: "One page, one owner" },
|
|
2111
|
+
{ title: "Build", text: "Two-week slices" },
|
|
2112
|
+
{ title: "Prove", text: "A number in production" }
|
|
2113
|
+
]
|
|
2114
|
+
}
|
|
2115
|
+
]
|
|
2116
|
+
},
|
|
2117
|
+
{
|
|
2118
|
+
type: "content",
|
|
2119
|
+
kind: "data",
|
|
2120
|
+
heading: "Pipeline by stage",
|
|
2121
|
+
components: [
|
|
2122
|
+
{
|
|
2123
|
+
type: "chart",
|
|
2124
|
+
chart_type: "bar",
|
|
2125
|
+
series: [
|
|
2126
|
+
{
|
|
2127
|
+
name: "Qualified",
|
|
2128
|
+
data: [
|
|
2129
|
+
{ x: "Discover", y: 42 },
|
|
2130
|
+
{ x: "Propose", y: 19 },
|
|
2131
|
+
{ x: "Close", y: 11 }
|
|
2132
|
+
]
|
|
2133
|
+
}
|
|
2134
|
+
]
|
|
2135
|
+
}
|
|
2136
|
+
]
|
|
2137
|
+
},
|
|
2138
|
+
{
|
|
2139
|
+
type: "content",
|
|
2140
|
+
kind: "statement",
|
|
2141
|
+
heading: "We will not add a fourth product line this year",
|
|
2142
|
+
components: [
|
|
2143
|
+
{
|
|
2144
|
+
type: "blockquote",
|
|
2145
|
+
text: "Three products is the house."
|
|
2146
|
+
}
|
|
2147
|
+
]
|
|
2148
|
+
},
|
|
2149
|
+
{
|
|
2150
|
+
type: "content",
|
|
2151
|
+
kind: "fact",
|
|
2152
|
+
heading: "NPS",
|
|
2153
|
+
components: [
|
|
2154
|
+
{
|
|
2155
|
+
type: "kpi_cards",
|
|
2156
|
+
items: [{ value: "61", label: "trailing 90-day NPS", delta: "up" }]
|
|
2157
|
+
}
|
|
2158
|
+
]
|
|
2159
|
+
},
|
|
2160
|
+
{
|
|
2161
|
+
type: "ending",
|
|
2162
|
+
heading: "See you Thursday"
|
|
2163
|
+
}
|
|
2164
|
+
]
|
|
2165
|
+
};
|
|
2166
|
+
|
|
2167
|
+
// src/cli/commands.ts
|
|
2168
|
+
async function registerThemesFromSpecSource(specPath, opts) {
|
|
2169
|
+
if (!await pathExists(specPath)) return;
|
|
2170
|
+
const specRaw = await loadIrFile(specPath, "spec");
|
|
2171
|
+
await registerThemeSelection(themeNameFromUnknown(specRaw), opts);
|
|
1816
2172
|
}
|
|
1817
2173
|
function resolveDecksDirSource(projectHit, userHit) {
|
|
1818
2174
|
if (projectHit?.config.decksDir !== void 0) {
|
|
1819
|
-
return { decksDir:
|
|
2175
|
+
return { decksDir: resolve7(dirname5(projectHit.path), projectHit.config.decksDir) };
|
|
1820
2176
|
}
|
|
1821
2177
|
return userHit?.config;
|
|
1822
2178
|
}
|
|
2179
|
+
function deckLookupDir(resolvedTarget, isDir) {
|
|
2180
|
+
return isDir ? resolvedTarget : dirname5(resolvedTarget);
|
|
2181
|
+
}
|
|
1823
2182
|
async function applyDeckConfig(raw, opts) {
|
|
1824
2183
|
if (typeof raw !== "object" || raw === null) return;
|
|
1825
2184
|
const deck = raw;
|
|
1826
2185
|
const irTheme = typeof deck.theme === "object" && deck.theme !== null ? deck.theme : {};
|
|
1827
|
-
const
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
const installedThemeIds = getInstalledThemeIds();
|
|
1836
|
-
if (!installedThemeIds.includes(theme)) {
|
|
1837
|
-
throw new PptwiseError(
|
|
1838
|
-
`unknown theme "${theme}" (from ${describeThemeSource(opts, projectHit, userHit)}) \u2014 available: ${installedThemeIds.join(", ")} (see \`pptwise themes\`)`
|
|
1839
|
-
);
|
|
1840
|
-
}
|
|
1841
|
-
}
|
|
1842
|
-
if (theme === void 0 && style === void 0) return;
|
|
1843
|
-
deck.theme = { ...irTheme, id: theme, ...style !== void 0 ? { style } : {} };
|
|
2186
|
+
const authoredName = opts.specTheme ?? (opts.fromDeckDir ? void 0 : typeof irTheme.id === "string" ? irTheme.id : void 0);
|
|
2187
|
+
if (authoredName === void 0) return;
|
|
2188
|
+
const resolved = await resolveThemeByName(authoredName, { startDir: opts.cwd, deckDir: opts.deckDir });
|
|
2189
|
+
await assertThemeRebind(opts.deckDir, resolved);
|
|
2190
|
+
deck.theme = {
|
|
2191
|
+
...irTheme,
|
|
2192
|
+
id: resolved.id
|
|
2193
|
+
};
|
|
1844
2194
|
}
|
|
1845
2195
|
function mergeWorkspaceImages(raw, extras) {
|
|
1846
2196
|
if (typeof raw !== "object" || raw === null) return raw;
|
|
@@ -1856,30 +2206,32 @@ async function loadWorkspaceStock(cwd, projectHit, resolvedTarget, isDir) {
|
|
|
1856
2206
|
target: resolvedTarget,
|
|
1857
2207
|
isDir
|
|
1858
2208
|
});
|
|
1859
|
-
const workspaceAssetsDir =
|
|
2209
|
+
const workspaceAssetsDir = join7(location.dir, ASSETS_DIRNAME);
|
|
1860
2210
|
const images2 = await scanWorkspaceAssets(workspaceAssetsDir);
|
|
1861
2211
|
return { workspaceAssetsDir, images: images2 };
|
|
1862
2212
|
}
|
|
1863
2213
|
async function loadDeckTarget(arg, cwd, projectHit, userHit) {
|
|
1864
2214
|
const target = await resolveDeckTarget(arg, resolveDecksDirSource(projectHit, userHit), cwd);
|
|
1865
2215
|
if (await isDeckDirectory(target)) {
|
|
1866
|
-
await
|
|
1867
|
-
const { ir, deckDir } = await readDeckDir(target);
|
|
2216
|
+
await registerThemesFromSpecSource(join7(target, SPEC_FILENAME), { startDir: cwd, deckDir: target });
|
|
2217
|
+
const { ir, deckDir, specTheme, specPath } = await readDeckDir(target);
|
|
1868
2218
|
const stock2 = await loadWorkspaceStock(cwd, projectHit, deckDir, true);
|
|
1869
2219
|
return {
|
|
1870
2220
|
raw: mergeWorkspaceImages(ir, stock2.images),
|
|
1871
2221
|
baseDir: deckDir,
|
|
1872
2222
|
isDir: true,
|
|
1873
2223
|
resolvedTarget: deckDir,
|
|
1874
|
-
workspaceAssetsDir: stock2.workspaceAssetsDir
|
|
2224
|
+
workspaceAssetsDir: stock2.workspaceAssetsDir,
|
|
2225
|
+
specTheme,
|
|
2226
|
+
specPath
|
|
1875
2227
|
};
|
|
1876
2228
|
}
|
|
1877
2229
|
const raw = await loadIrFile(target);
|
|
1878
|
-
const resolvedFile =
|
|
2230
|
+
const resolvedFile = resolve7(target);
|
|
1879
2231
|
const stock = await loadWorkspaceStock(cwd, projectHit, resolvedFile, false);
|
|
1880
2232
|
return {
|
|
1881
2233
|
raw: mergeWorkspaceImages(raw, stock.images),
|
|
1882
|
-
baseDir:
|
|
2234
|
+
baseDir: dirname5(resolvedFile),
|
|
1883
2235
|
isDir: false,
|
|
1884
2236
|
resolvedTarget: resolvedFile,
|
|
1885
2237
|
workspaceAssetsDir: stock.workspaceAssetsDir
|
|
@@ -1887,8 +2239,16 @@ async function loadDeckTarget(arg, cwd, projectHit, userHit) {
|
|
|
1887
2239
|
}
|
|
1888
2240
|
async function loadValidatedDeckIr(target, cwd) {
|
|
1889
2241
|
const [projectHit, userHit] = await Promise.all([findConfig(cwd), findUserConfig()]);
|
|
1890
|
-
const { raw, baseDir, workspaceAssetsDir } = await loadDeckTarget(target, cwd, projectHit, userHit);
|
|
1891
|
-
await applyDeckConfig(raw, {
|
|
2242
|
+
const { raw, baseDir, workspaceAssetsDir, isDir, resolvedTarget, specTheme, specPath } = await loadDeckTarget(target, cwd, projectHit, userHit);
|
|
2243
|
+
await applyDeckConfig(raw, {
|
|
2244
|
+
cwd,
|
|
2245
|
+
projectHit,
|
|
2246
|
+
userHit,
|
|
2247
|
+
specTheme,
|
|
2248
|
+
specPath,
|
|
2249
|
+
fromDeckDir: isDir,
|
|
2250
|
+
deckDir: deckLookupDir(resolvedTarget, isDir)
|
|
2251
|
+
});
|
|
1892
2252
|
const v = validateIr(raw);
|
|
1893
2253
|
if (!v.ok) {
|
|
1894
2254
|
throw new PptwiseError(
|
|
@@ -1902,11 +2262,12 @@ ${formatIssues(v.errors)}`
|
|
|
1902
2262
|
async function runRender(irPath, opts) {
|
|
1903
2263
|
const cwd = opts.cwd ?? process.cwd();
|
|
1904
2264
|
const [projectHit, userHit] = await Promise.all([findConfig(cwd), findUserConfig()]);
|
|
1905
|
-
const { raw, baseDir, isDir, resolvedTarget, workspaceAssetsDir } = await loadDeckTarget(irPath, cwd, projectHit, userHit);
|
|
2265
|
+
const { raw, baseDir, isDir, resolvedTarget, workspaceAssetsDir, specTheme, specPath } = await loadDeckTarget(irPath, cwd, projectHit, userHit);
|
|
1906
2266
|
await applyDeckConfig(raw, {
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
2267
|
+
specTheme,
|
|
2268
|
+
specPath,
|
|
2269
|
+
fromDeckDir: isDir,
|
|
2270
|
+
deckDir: deckLookupDir(resolvedTarget, isDir),
|
|
1910
2271
|
cwd,
|
|
1911
2272
|
projectHit,
|
|
1912
2273
|
userHit
|
|
@@ -1922,8 +2283,8 @@ ${formatIssues(v.errors)}`);
|
|
|
1922
2283
|
const extraNotes = [];
|
|
1923
2284
|
let output;
|
|
1924
2285
|
if (opts.output !== void 0) {
|
|
1925
|
-
output =
|
|
1926
|
-
await
|
|
2286
|
+
output = resolve7(cwd, opts.output);
|
|
2287
|
+
await mkdir5(dirname5(output), { recursive: true });
|
|
1927
2288
|
} else {
|
|
1928
2289
|
const location = resolveWorkspaceLocation({
|
|
1929
2290
|
cwd,
|
|
@@ -1933,9 +2294,9 @@ ${formatIssues(v.errors)}`);
|
|
|
1933
2294
|
isDir
|
|
1934
2295
|
});
|
|
1935
2296
|
extraNotes.push(...await prepareWorkspaceDir(location, { gitIgnore: opts.gitIgnore, runGit: opts.runGit }));
|
|
1936
|
-
output =
|
|
2297
|
+
output = join7(location.dir, `${location.slug}.pptx`);
|
|
1937
2298
|
}
|
|
1938
|
-
await
|
|
2299
|
+
await writeFile4(output, bytes);
|
|
1939
2300
|
const ok = `wrote ${output} (${v.ir.slides.length} slides, ${bytes.length} bytes)`;
|
|
1940
2301
|
const notes = [...extraNotes, warningsNote(v.warnings), normalizedNote(v.normalized)].filter(
|
|
1941
2302
|
(n) => n !== void 0
|
|
@@ -1959,10 +2320,18 @@ function placeholderNote(ir) {
|
|
|
1959
2320
|
const refs = placeholders.map(({ slide, page }) => slide.id ? `${slide.id} (page ${page})` : `page ${page}`).join(", ");
|
|
1960
2321
|
return `note: ${placeholders.length} unfilled placeholder page${placeholders.length === 1 ? "" : "s"}: ${refs}`;
|
|
1961
2322
|
}
|
|
1962
|
-
async function runValidate(irPath, cwd = process.cwd()
|
|
2323
|
+
async function runValidate(irPath, cwd = process.cwd()) {
|
|
1963
2324
|
const [projectHit, userHit] = await Promise.all([findConfig(cwd), findUserConfig()]);
|
|
1964
|
-
const { raw, baseDir, isDir, workspaceAssetsDir } = await loadDeckTarget(irPath, cwd, projectHit, userHit);
|
|
1965
|
-
await applyDeckConfig(raw, {
|
|
2325
|
+
const { raw, baseDir, isDir, resolvedTarget, workspaceAssetsDir, specTheme, specPath } = await loadDeckTarget(irPath, cwd, projectHit, userHit);
|
|
2326
|
+
await applyDeckConfig(raw, {
|
|
2327
|
+
specTheme,
|
|
2328
|
+
specPath,
|
|
2329
|
+
fromDeckDir: isDir,
|
|
2330
|
+
deckDir: deckLookupDir(resolvedTarget, isDir),
|
|
2331
|
+
cwd,
|
|
2332
|
+
projectHit,
|
|
2333
|
+
userHit
|
|
2334
|
+
});
|
|
1966
2335
|
const v = validateIr(raw);
|
|
1967
2336
|
if (!v.ok)
|
|
1968
2337
|
throw new PptwiseError(
|
|
@@ -2002,8 +2371,16 @@ function formatAuditReport(report, ir) {
|
|
|
2002
2371
|
async function runAudit(target, opts = {}) {
|
|
2003
2372
|
const cwd = opts.cwd ?? process.cwd();
|
|
2004
2373
|
const [projectHit, userHit] = await Promise.all([findConfig(cwd), findUserConfig()]);
|
|
2005
|
-
const { raw, baseDir, workspaceAssetsDir } = await loadDeckTarget(target, cwd, projectHit, userHit);
|
|
2006
|
-
await applyDeckConfig(raw, {
|
|
2374
|
+
const { raw, baseDir, workspaceAssetsDir, isDir, resolvedTarget, specTheme, specPath } = await loadDeckTarget(target, cwd, projectHit, userHit);
|
|
2375
|
+
await applyDeckConfig(raw, {
|
|
2376
|
+
specTheme,
|
|
2377
|
+
specPath,
|
|
2378
|
+
fromDeckDir: isDir,
|
|
2379
|
+
deckDir: deckLookupDir(resolvedTarget, isDir),
|
|
2380
|
+
cwd,
|
|
2381
|
+
projectHit,
|
|
2382
|
+
userHit
|
|
2383
|
+
});
|
|
2007
2384
|
const v = validateIr(raw);
|
|
2008
2385
|
if (!v.ok) {
|
|
2009
2386
|
throw new PptwiseError(
|
|
@@ -2047,8 +2424,16 @@ function formatAssetBriefReport(brief) {
|
|
|
2047
2424
|
async function runAssetBrief(target, opts = {}) {
|
|
2048
2425
|
const cwd = opts.cwd ?? process.cwd();
|
|
2049
2426
|
const [projectHit, userHit] = await Promise.all([findConfig(cwd), findUserConfig()]);
|
|
2050
|
-
const { raw, baseDir, workspaceAssetsDir } = await loadDeckTarget(target, cwd, projectHit, userHit);
|
|
2051
|
-
await applyDeckConfig(raw, {
|
|
2427
|
+
const { raw, baseDir, workspaceAssetsDir, isDir, resolvedTarget, specTheme, specPath } = await loadDeckTarget(target, cwd, projectHit, userHit);
|
|
2428
|
+
await applyDeckConfig(raw, {
|
|
2429
|
+
specTheme,
|
|
2430
|
+
specPath,
|
|
2431
|
+
fromDeckDir: isDir,
|
|
2432
|
+
deckDir: deckLookupDir(resolvedTarget, isDir),
|
|
2433
|
+
cwd,
|
|
2434
|
+
projectHit,
|
|
2435
|
+
userHit
|
|
2436
|
+
});
|
|
2052
2437
|
const v = validateIr(raw);
|
|
2053
2438
|
if (!v.ok) {
|
|
2054
2439
|
throw new PptwiseError(
|
|
@@ -2062,7 +2447,8 @@ ${formatIssues(v.errors)}`
|
|
|
2062
2447
|
}
|
|
2063
2448
|
async function runSpecValidate(specPath) {
|
|
2064
2449
|
const raw = await loadIrFile(specPath, "spec");
|
|
2065
|
-
|
|
2450
|
+
const specDir = dirname5(resolve7(specPath));
|
|
2451
|
+
await registerThemeSelection(themeNameFromUnknown(raw), { startDir: specDir, deckDir: specDir });
|
|
2066
2452
|
const v = validateSpec(raw);
|
|
2067
2453
|
if (!v.ok) {
|
|
2068
2454
|
throw new PptwiseError(formatInvalidSpecError(v.errors));
|
|
@@ -2070,21 +2456,121 @@ async function runSpecValidate(specPath) {
|
|
|
2070
2456
|
const spec2 = v.spec;
|
|
2071
2457
|
const axes = resolveNarrative(spec2.narrative);
|
|
2072
2458
|
const ok = `OK \u2014 ${spec2.pages.length} pages, narrative ${axes.strategy}/${axes.pacing}/${axes.audience}, theme "${resolveSpecThemeId(spec2)}"`;
|
|
2459
|
+
const lines = [ok];
|
|
2460
|
+
for (const w of v.warnings ?? []) {
|
|
2461
|
+
lines.push(`warning: ${w.path ? `${w.path}: ` : ""}${w.message}`);
|
|
2462
|
+
}
|
|
2073
2463
|
const aliasNote = normalizedNote(v.normalized);
|
|
2074
|
-
|
|
2075
|
-
|
|
2464
|
+
if (aliasNote) lines.push(aliasNote);
|
|
2465
|
+
return lines.join("\n");
|
|
2076
2466
|
}
|
|
2077
2467
|
function runSchema(mode) {
|
|
2078
|
-
const schema = mode === "
|
|
2468
|
+
const schema = mode === "spec" ? specJsonSchema() : irJsonSchema();
|
|
2079
2469
|
return JSON.stringify(schema, null, 2);
|
|
2080
2470
|
}
|
|
2081
2471
|
function runThemes(asJson) {
|
|
2082
2472
|
const themes = listThemes();
|
|
2083
|
-
if (asJson)
|
|
2473
|
+
if (asJson) {
|
|
2474
|
+
return JSON.stringify(
|
|
2475
|
+
themes.map((t) => {
|
|
2476
|
+
const rec = Object.hasOwn(THEME_OCCASIONS, t.id) ? THEME_OCCASIONS[t.id] : void 0;
|
|
2477
|
+
return {
|
|
2478
|
+
id: t.id,
|
|
2479
|
+
label: t.label,
|
|
2480
|
+
colors: t.colors,
|
|
2481
|
+
occasions: rec?.occasions ?? [],
|
|
2482
|
+
identity: rec?.identity ?? null
|
|
2483
|
+
};
|
|
2484
|
+
}),
|
|
2485
|
+
null,
|
|
2486
|
+
2
|
|
2487
|
+
);
|
|
2488
|
+
}
|
|
2084
2489
|
return themes.map((t) => `${t.id.padEnd(12)} ${t.label}`).join("\n");
|
|
2085
2490
|
}
|
|
2491
|
+
function layoutCapacity(slots) {
|
|
2492
|
+
let sum = 0;
|
|
2493
|
+
let any = false;
|
|
2494
|
+
for (const slot of slots) {
|
|
2495
|
+
if (slot.capacity !== void 0) {
|
|
2496
|
+
sum += slot.capacity;
|
|
2497
|
+
any = true;
|
|
2498
|
+
}
|
|
2499
|
+
}
|
|
2500
|
+
return any ? sum : void 0;
|
|
2501
|
+
}
|
|
2502
|
+
function listLayouts() {
|
|
2503
|
+
return Object.values(LAYOUT_REGISTRY).map((layout) => {
|
|
2504
|
+
const capacity = layoutCapacity(layout.slots);
|
|
2505
|
+
const row = {
|
|
2506
|
+
id: layout.id,
|
|
2507
|
+
slideTypes: layout.slideTypes,
|
|
2508
|
+
slots: layout.slots.map((slot) => {
|
|
2509
|
+
const compact = { name: slot.name, accepts: slot.accepts };
|
|
2510
|
+
if (slot.capacity !== void 0) compact.capacity = slot.capacity;
|
|
2511
|
+
return compact;
|
|
2512
|
+
})
|
|
2513
|
+
};
|
|
2514
|
+
if (capacity !== void 0) row.capacity = capacity;
|
|
2515
|
+
return row;
|
|
2516
|
+
});
|
|
2517
|
+
}
|
|
2518
|
+
function runLayouts(asJson) {
|
|
2519
|
+
const layouts = listLayouts();
|
|
2520
|
+
if (asJson) return JSON.stringify(layouts, null, 2);
|
|
2521
|
+
const rows = layouts.map((l) => ({
|
|
2522
|
+
id: l.id,
|
|
2523
|
+
types: l.slideTypes.join(","),
|
|
2524
|
+
cap: l.capacity !== void 0 ? String(l.capacity) : "-",
|
|
2525
|
+
slots: l.slots.map((s) => s.name).join(", ")
|
|
2526
|
+
}));
|
|
2527
|
+
const idWidth = Math.max(...rows.map((r) => r.id.length));
|
|
2528
|
+
const typesWidth = Math.max(...rows.map((r) => r.types.length));
|
|
2529
|
+
const capWidth = Math.max(...rows.map((r) => r.cap.length));
|
|
2530
|
+
return rows.map(
|
|
2531
|
+
(r) => `${r.id.padEnd(idWidth + 2)}${r.types.padEnd(typesWidth + 2)}${r.cap.padEnd(capWidth + 2)}${r.slots}`
|
|
2532
|
+
).join("\n");
|
|
2533
|
+
}
|
|
2086
2534
|
function defaultThemeIdFor(output) {
|
|
2087
|
-
return slugify(
|
|
2535
|
+
return slugify(basename5(output).replace(/\.theme\.json$|\.json$/i, ""));
|
|
2536
|
+
}
|
|
2537
|
+
async function defaultThemeOutputPath(id, cwd) {
|
|
2538
|
+
assertThemeId(id);
|
|
2539
|
+
const project = await findConfig(cwd);
|
|
2540
|
+
const root = project !== null ? dirname5(project.path) : resolve7(cwd);
|
|
2541
|
+
return join7(root, WORKSPACE_THEMES_DIRNAME, `${id}.theme.json`);
|
|
2542
|
+
}
|
|
2543
|
+
async function resolveThemeWriteTarget(opts) {
|
|
2544
|
+
if (opts.output !== void 0) {
|
|
2545
|
+
const output = resolve7(opts.cwd, opts.output);
|
|
2546
|
+
const id = opts.id ?? defaultThemeIdFor(output);
|
|
2547
|
+
assertThemeId(id);
|
|
2548
|
+
return { id, output };
|
|
2549
|
+
}
|
|
2550
|
+
if (opts.id !== void 0) {
|
|
2551
|
+
assertThemeId(opts.id);
|
|
2552
|
+
return { id: opts.id, output: await defaultThemeOutputPath(opts.id, opts.cwd) };
|
|
2553
|
+
}
|
|
2554
|
+
throw new PptwiseError("pass --id or -o so the theme file has a name");
|
|
2555
|
+
}
|
|
2556
|
+
async function themeFileFromResolved(resolved, identity) {
|
|
2557
|
+
if (resolved.kind === "builtin") {
|
|
2558
|
+
return themeFileFromPreset(resolved.id, identity);
|
|
2559
|
+
}
|
|
2560
|
+
const copy = structuredClone(resolved.file);
|
|
2561
|
+
copy.id = identity.id;
|
|
2562
|
+
copy.style = { ...copy.style, id: identity.id };
|
|
2563
|
+
if (identity.label !== void 0) copy.label = identity.label;
|
|
2564
|
+
return ThemeFileSchema.parse(copy);
|
|
2565
|
+
}
|
|
2566
|
+
function rethrowBrandColorError(error, id) {
|
|
2567
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2568
|
+
if (/colors\.|contrast ratio|calibrate/i.test(message)) {
|
|
2569
|
+
throw new PptwiseError(
|
|
2570
|
+
`${message}. Run \`pptwise theme new --from <preset> --id ${id}\`, adjust the copied theme colors by hand, then bind that theme`
|
|
2571
|
+
);
|
|
2572
|
+
}
|
|
2573
|
+
throw error;
|
|
2088
2574
|
}
|
|
2089
2575
|
async function runBrandExtract(file, opts) {
|
|
2090
2576
|
let bytes;
|
|
@@ -2094,31 +2580,123 @@ async function runBrandExtract(file, opts) {
|
|
|
2094
2580
|
throw new PptwiseError(`cannot read template file: ${file}`);
|
|
2095
2581
|
}
|
|
2096
2582
|
const id = opts.id ?? defaultThemeIdFor(opts.output);
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2583
|
+
assertThemeId(id);
|
|
2584
|
+
let extracted;
|
|
2585
|
+
try {
|
|
2586
|
+
extracted = await extractBrandTheme(bytes, { id, label: opts.label });
|
|
2587
|
+
} catch (error) {
|
|
2588
|
+
rethrowBrandColorError(error, id);
|
|
2589
|
+
}
|
|
2590
|
+
const from = opts.from ?? "consulting";
|
|
2591
|
+
const cwd = dirname5(resolve7(opts.output));
|
|
2592
|
+
const donorResolved = await resolveThemeByName(from, { startDir: cwd });
|
|
2593
|
+
const donor = await themeFileFromResolved(donorResolved, { id: extracted.id, label: extracted.label });
|
|
2594
|
+
let theme2;
|
|
2595
|
+
try {
|
|
2596
|
+
theme2 = forkTheme(
|
|
2597
|
+
donor,
|
|
2598
|
+
{
|
|
2599
|
+
primary: extracted.style.colors.primary,
|
|
2600
|
+
accent: extracted.style.colors.accent,
|
|
2601
|
+
bg: extracted.style.colors.bg,
|
|
2602
|
+
text: extracted.style.colors.text,
|
|
2603
|
+
surface: extracted.style.colors.surface,
|
|
2604
|
+
chartPalette: extracted.style.colors.chartPalette
|
|
2605
|
+
},
|
|
2606
|
+
{ id: extracted.id, label: extracted.label, fonts: extracted.style.fonts }
|
|
2100
2607
|
);
|
|
2608
|
+
} catch (error) {
|
|
2609
|
+
rethrowBrandColorError(error, id);
|
|
2101
2610
|
}
|
|
2102
|
-
const
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
await writeFile3(outPath, JSON.stringify(theme, null, 2) + "\n");
|
|
2106
|
-
const c = theme.style.colors;
|
|
2611
|
+
const outPath = resolve7(opts.output);
|
|
2612
|
+
await writeThemeFile(outPath, theme2, opts.force === true);
|
|
2613
|
+
const c = theme2.style.colors;
|
|
2107
2614
|
const lines = [
|
|
2108
|
-
`wrote ${opts.output} (theme "${
|
|
2615
|
+
`wrote ${opts.output} (theme "${theme2.id}", label "${theme2.label}")`,
|
|
2109
2616
|
` colors: bg ${c.bg}, text ${c.text}, primary ${c.primary}, accent ${c.accent}, muted ${c.muted} (derived), ${c.chartPalette.length} chart colors`,
|
|
2110
|
-
` fonts: heading "${
|
|
2111
|
-
`
|
|
2617
|
+
` fonts: heading "${theme2.style.fonts.heading[0]}", body "${theme2.style.fonts.body[0]}"`,
|
|
2618
|
+
`Drop the file in a deck ${THEME_FILENAME} or workspace themes/ and set spec.theme to "${theme2.id}".`
|
|
2112
2619
|
];
|
|
2113
|
-
try {
|
|
2114
|
-
assertContrastFloor(theme.id, theme.style);
|
|
2115
|
-
} catch (e) {
|
|
2116
|
-
lines.push(
|
|
2117
|
-
`warning: this theme will be refused at load time \u2014 ${e instanceof Error ? e.message : String(e)}. Edit the written file's colors (darker text, or a lighter bg) before using it`
|
|
2118
|
-
);
|
|
2119
|
-
}
|
|
2120
2620
|
return lines.join("\n");
|
|
2121
2621
|
}
|
|
2622
|
+
async function runThemeNew(opts) {
|
|
2623
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
2624
|
+
const { id, output } = await resolveThemeWriteTarget({ id: opts.id, output: opts.output, cwd });
|
|
2625
|
+
const resolved = await resolveThemeByName(opts.from, { startDir: cwd });
|
|
2626
|
+
const file = await themeFileFromResolved(resolved, { id, label: opts.label });
|
|
2627
|
+
await writeThemeFile(output, file, opts.force === true);
|
|
2628
|
+
return `wrote ${output} (theme "${file.id}"). Set spec.theme to "${file.id}".`;
|
|
2629
|
+
}
|
|
2630
|
+
async function runThemeFork(name, opts) {
|
|
2631
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
2632
|
+
const { id, output } = await resolveThemeWriteTarget({ id: opts.id, output: opts.output, cwd });
|
|
2633
|
+
const resolved = await resolveThemeByName(name, { startDir: cwd });
|
|
2634
|
+
const source = await themeFileFromResolved(resolved, { id, label: opts.label });
|
|
2635
|
+
const file = forkTheme(
|
|
2636
|
+
source,
|
|
2637
|
+
{ primary: opts.primary, bg: opts.bg, accent: opts.accent, text: opts.text, surface: opts.surface },
|
|
2638
|
+
{ id, label: opts.label ?? source.label }
|
|
2639
|
+
);
|
|
2640
|
+
await writeThemeFile(output, file, opts.force === true);
|
|
2641
|
+
return `wrote ${output} (theme "${file.id}"). Set spec.theme to "${file.id}".`;
|
|
2642
|
+
}
|
|
2643
|
+
function parseThemeTryIds(raw) {
|
|
2644
|
+
const ids = raw.split(",").map((part) => part.trim()).filter((part) => part.length > 0);
|
|
2645
|
+
if (ids.length < 2 || ids.length > 4) {
|
|
2646
|
+
throw new PptwiseError(`pptwise theme try expects 2-4 theme ids, got ${ids.length}`);
|
|
2647
|
+
}
|
|
2648
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2649
|
+
for (const id of ids) {
|
|
2650
|
+
if (seen.has(id)) throw new PptwiseError(`pptwise theme try has duplicate theme id "${id}"`);
|
|
2651
|
+
seen.add(id);
|
|
2652
|
+
}
|
|
2653
|
+
return ids;
|
|
2654
|
+
}
|
|
2655
|
+
function contactSheetSlidesFromIr(ir, svgs) {
|
|
2656
|
+
return ir.slides.map((slide, index) => {
|
|
2657
|
+
const type = slide.type ?? "content";
|
|
2658
|
+
const label = type === "content" && typeof slide.kind === "string" ? slide.kind : type;
|
|
2659
|
+
return { type, label, svg: svgs[index] };
|
|
2660
|
+
});
|
|
2661
|
+
}
|
|
2662
|
+
async function runThemeTry(idsRaw, opts = {}) {
|
|
2663
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
2664
|
+
const ids = parseThemeTryIds(idsRaw);
|
|
2665
|
+
const columns = [];
|
|
2666
|
+
for (const name of ids) {
|
|
2667
|
+
const resolved = await resolveThemeByName(name, { startDir: cwd });
|
|
2668
|
+
const raw = structuredClone(THEME_TRY_SAMPLE_IR);
|
|
2669
|
+
raw.theme = { id: resolved.id };
|
|
2670
|
+
const v = validateIr(raw);
|
|
2671
|
+
if (!v.ok) throw new PptwiseError(`invalid IR:
|
|
2672
|
+
${formatIssues(v.errors)}`);
|
|
2673
|
+
const ir = v.ir;
|
|
2674
|
+
const svgs = ir.slides.map((_, index) => renderSlideSvg(ir, index));
|
|
2675
|
+
columns.push({ id: resolved.id, slides: contactSheetSlidesFromIr(ir, svgs) });
|
|
2676
|
+
}
|
|
2677
|
+
let resolvedOut;
|
|
2678
|
+
const extraNotes = [];
|
|
2679
|
+
if (opts.output !== void 0) {
|
|
2680
|
+
resolvedOut = resolve7(cwd, opts.output);
|
|
2681
|
+
await mkdir5(resolvedOut, { recursive: true });
|
|
2682
|
+
} else {
|
|
2683
|
+
const projectHit = await findConfig(cwd);
|
|
2684
|
+
const location = resolveWorkspaceLocation({
|
|
2685
|
+
cwd,
|
|
2686
|
+
projectConfigPath: projectHit?.path,
|
|
2687
|
+
outDir: projectHit?.config.outDir,
|
|
2688
|
+
target: "theme-try",
|
|
2689
|
+
isDir: true
|
|
2690
|
+
});
|
|
2691
|
+
extraNotes.push(...await prepareWorkspaceDir(location, { gitIgnore: opts.gitIgnore, runGit: opts.runGit }));
|
|
2692
|
+
resolvedOut = location.dir;
|
|
2693
|
+
}
|
|
2694
|
+
const htmlPath = join7(resolvedOut, "contact-sheet.html");
|
|
2695
|
+
await writeFile4(htmlPath, buildContactSheetHtml({ title: "theme try", themes: columns }));
|
|
2696
|
+
const ok = `wrote contact sheet to ${htmlPath}`;
|
|
2697
|
+
return extraNotes.length > 0 ? `${ok}
|
|
2698
|
+
${extraNotes.join("\n")}` : ok;
|
|
2699
|
+
}
|
|
2122
2700
|
function runNarratives(asJson) {
|
|
2123
2701
|
if (asJson) {
|
|
2124
2702
|
return JSON.stringify(
|
|
@@ -2141,29 +2719,32 @@ function runNarratives(asJson) {
|
|
|
2141
2719
|
const axesWidth = Math.max(...rows.map((r) => r.axes.length));
|
|
2142
2720
|
return rows.map((r) => `${r.id.padEnd(idWidth + 2)}${r.axes.padEnd(axesWidth + 2)}${r.themes}`).join("\n");
|
|
2143
2721
|
}
|
|
2144
|
-
var CONFIG_TEMPLATE = {
|
|
2145
|
-
theme: "consulting",
|
|
2146
|
-
style: {
|
|
2147
|
-
colors: { primary: "#0B5FFF", accent: "#FF6A00" }
|
|
2148
|
-
}
|
|
2149
|
-
};
|
|
2722
|
+
var CONFIG_TEMPLATE = {};
|
|
2150
2723
|
async function runInit(cwd = process.cwd()) {
|
|
2151
|
-
const target =
|
|
2724
|
+
const target = join7(cwd, CONFIG_FILENAME);
|
|
2152
2725
|
try {
|
|
2153
|
-
await
|
|
2726
|
+
await writeFile4(target, JSON.stringify(CONFIG_TEMPLATE, null, 2) + "\n", { flag: "wx" });
|
|
2154
2727
|
} catch (e) {
|
|
2155
2728
|
if (e.code === "EEXIST") {
|
|
2156
2729
|
throw new PptwiseError(`${target} already exists \u2014 edit it instead`);
|
|
2157
2730
|
}
|
|
2158
2731
|
throw e;
|
|
2159
2732
|
}
|
|
2160
|
-
return `wrote ${target} \u2014 themes: \`pptwise themes
|
|
2733
|
+
return `wrote ${target} \u2014 themes: \`pptwise themes\``;
|
|
2161
2734
|
}
|
|
2162
2735
|
async function renderDeckSlides(target, opts = {}) {
|
|
2163
2736
|
const cwd = opts.cwd ?? process.cwd();
|
|
2164
2737
|
const [projectHit, userHit] = await Promise.all([findConfig(cwd), findUserConfig()]);
|
|
2165
|
-
const { raw, baseDir, isDir, resolvedTarget, workspaceAssetsDir } = await loadDeckTarget(target, cwd, projectHit, userHit);
|
|
2166
|
-
await applyDeckConfig(raw, {
|
|
2738
|
+
const { raw, baseDir, isDir, resolvedTarget, workspaceAssetsDir, specTheme, specPath } = await loadDeckTarget(target, cwd, projectHit, userHit);
|
|
2739
|
+
await applyDeckConfig(raw, {
|
|
2740
|
+
specTheme,
|
|
2741
|
+
specPath,
|
|
2742
|
+
fromDeckDir: isDir,
|
|
2743
|
+
deckDir: deckLookupDir(resolvedTarget, isDir),
|
|
2744
|
+
cwd,
|
|
2745
|
+
projectHit,
|
|
2746
|
+
userHit
|
|
2747
|
+
});
|
|
2167
2748
|
const v = validateIr(raw);
|
|
2168
2749
|
if (!v.ok) throw new PptwiseError(`invalid IR:
|
|
2169
2750
|
${formatIssues(v.errors)}`);
|
|
@@ -2196,35 +2777,35 @@ async function buildDeckPreview(target, opts = {}) {
|
|
|
2196
2777
|
const { html, findings, checks } = buildDeckAuditAndHtml(rendered.ir, rendered.svgs);
|
|
2197
2778
|
return { ...rendered, html, findings, checks };
|
|
2198
2779
|
}
|
|
2780
|
+
async function resolvePreviewOutDir(cwd, outDir, resolvedTarget, isDir, opts) {
|
|
2781
|
+
if (outDir !== void 0) {
|
|
2782
|
+
const resolvedOut = resolve7(cwd, outDir);
|
|
2783
|
+
await mkdir5(resolvedOut, { recursive: true });
|
|
2784
|
+
return { resolvedOut, extraNotes: [] };
|
|
2785
|
+
}
|
|
2786
|
+
const projectHit = await findConfig(cwd);
|
|
2787
|
+
const location = resolveWorkspaceLocation({
|
|
2788
|
+
cwd,
|
|
2789
|
+
projectConfigPath: projectHit?.path,
|
|
2790
|
+
outDir: projectHit?.config.outDir,
|
|
2791
|
+
target: resolvedTarget,
|
|
2792
|
+
isDir
|
|
2793
|
+
});
|
|
2794
|
+
const extraNotes = await prepareWorkspaceDir(location, { gitIgnore: opts.gitIgnore, runGit: opts.runGit });
|
|
2795
|
+
await pruneRenderedSvgs(location.dir);
|
|
2796
|
+
return { resolvedOut: location.dir, extraNotes };
|
|
2797
|
+
}
|
|
2199
2798
|
async function runPreview(irPath, outDir, opts = {}) {
|
|
2200
2799
|
const cwd = opts.cwd ?? process.cwd();
|
|
2201
2800
|
const { ir, svgs, normalized, isDir, resolvedTarget } = await renderDeckSlides(irPath, {
|
|
2202
|
-
cwd
|
|
2203
|
-
themeFilePath: opts.themeFilePath
|
|
2801
|
+
cwd
|
|
2204
2802
|
});
|
|
2205
|
-
const extraNotes =
|
|
2206
|
-
let resolvedOut;
|
|
2207
|
-
if (outDir !== void 0) {
|
|
2208
|
-
resolvedOut = resolve6(cwd, outDir);
|
|
2209
|
-
await mkdir4(resolvedOut, { recursive: true });
|
|
2210
|
-
} else {
|
|
2211
|
-
const projectHit = await findConfig(cwd);
|
|
2212
|
-
const location = resolveWorkspaceLocation({
|
|
2213
|
-
cwd,
|
|
2214
|
-
projectConfigPath: projectHit?.path,
|
|
2215
|
-
outDir: projectHit?.config.outDir,
|
|
2216
|
-
target: resolvedTarget,
|
|
2217
|
-
isDir
|
|
2218
|
-
});
|
|
2219
|
-
extraNotes.push(...await prepareWorkspaceDir(location, { gitIgnore: opts.gitIgnore, runGit: opts.runGit }));
|
|
2220
|
-
await pruneRenderedSvgs(location.dir);
|
|
2221
|
-
resolvedOut = location.dir;
|
|
2222
|
-
}
|
|
2803
|
+
const { resolvedOut, extraNotes } = await resolvePreviewOutDir(cwd, outDir, resolvedTarget, isDir, opts);
|
|
2223
2804
|
const svgNames = [];
|
|
2224
2805
|
for (let i = 0; i < ir.slides.length; i++) {
|
|
2225
2806
|
const name = `${String(i + 1).padStart(3, "0")}-${ir.slides[i].type}.svg`;
|
|
2226
2807
|
svgNames.push(name);
|
|
2227
|
-
await
|
|
2808
|
+
await writeFile4(join7(resolvedOut, name), svgs[i]);
|
|
2228
2809
|
}
|
|
2229
2810
|
const ok = `wrote ${ir.slides.length} SVG files to ${resolvedOut}`;
|
|
2230
2811
|
const notes = [...extraNotes];
|
|
@@ -2232,8 +2813,8 @@ async function runPreview(irPath, outDir, opts = {}) {
|
|
|
2232
2813
|
if (aliasNote) notes.push(aliasNote);
|
|
2233
2814
|
if (opts.htmlOut) {
|
|
2234
2815
|
const { html, findings, checks } = buildDeckAuditAndHtml(ir, svgs);
|
|
2235
|
-
const htmlPath =
|
|
2236
|
-
await
|
|
2816
|
+
const htmlPath = join7(resolvedOut, "preview.html");
|
|
2817
|
+
await writeFile4(htmlPath, html);
|
|
2237
2818
|
const hasPlaceholder = ir.slides.some((s) => s.placeholder);
|
|
2238
2819
|
const manifest = buildPreviewManifest({
|
|
2239
2820
|
title: ir.filename,
|
|
@@ -2251,8 +2832,8 @@ async function runPreview(irPath, outDir, opts = {}) {
|
|
|
2251
2832
|
checks,
|
|
2252
2833
|
auditNote: hasPlaceholder ? "audit skipped \u2014 deck has unfilled placeholder pages" : void 0
|
|
2253
2834
|
});
|
|
2254
|
-
const manifestPath =
|
|
2255
|
-
await
|
|
2835
|
+
const manifestPath = join7(resolvedOut, "manifest.json");
|
|
2836
|
+
await writeFile4(manifestPath, `${JSON.stringify(manifest, null, 2)}
|
|
2256
2837
|
`);
|
|
2257
2838
|
notes.push(`note: wrote self-contained preview to ${htmlPath}`);
|
|
2258
2839
|
notes.push(`note: wrote machine-readable page manifest to ${manifestPath}`);
|
|
@@ -2267,7 +2848,7 @@ function withRewrittenAssetPaths(ir, deckDir, outDir) {
|
|
|
2267
2848
|
const images2 = Object.fromEntries(
|
|
2268
2849
|
Object.entries(ir.assets.images).map(([id, asset]) => {
|
|
2269
2850
|
if (asset.src.startsWith("data:") || /^https?:\/\//.test(asset.src)) return [id, asset];
|
|
2270
|
-
return [id, { ...asset, src: relative2(outDir,
|
|
2851
|
+
return [id, { ...asset, src: relative2(outDir, join7(deckDir, asset.src)) }];
|
|
2271
2852
|
})
|
|
2272
2853
|
);
|
|
2273
2854
|
return { ...ir, assets: { images: images2 } };
|
|
@@ -2279,25 +2860,18 @@ async function runAssemble(target, opts = {}) {
|
|
|
2279
2860
|
if (await pathExists(dir) && !await isDeckDirectory(dir)) {
|
|
2280
2861
|
throw new PptwiseError(`expected a deck project directory: ${dir}`);
|
|
2281
2862
|
}
|
|
2282
|
-
if (await isDeckDirectory(dir))
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
const
|
|
2863
|
+
if (await isDeckDirectory(dir)) {
|
|
2864
|
+
await registerThemesFromSpecSource(join7(dir, SPEC_FILENAME), { startDir: cwd, deckDir: dir });
|
|
2865
|
+
}
|
|
2866
|
+
const { ir, deckDir } = await readDeckDir(dir);
|
|
2867
|
+
const outPath = opts.output ? resolve7(cwd, opts.output) : join7(deckDir, "deck.json");
|
|
2868
|
+
const outDir = dirname5(outPath);
|
|
2286
2869
|
const outIr = outDir === deckDir ? ir : withRewrittenAssetPaths(ir, deckDir, outDir);
|
|
2287
|
-
await
|
|
2288
|
-
await
|
|
2870
|
+
await mkdir5(outDir, { recursive: true });
|
|
2871
|
+
await writeFile4(outPath, JSON.stringify(outIr, null, 2) + "\n");
|
|
2289
2872
|
const placeholderCount = outIr.slides.filter((s) => s.placeholder).length;
|
|
2290
2873
|
const summary = `wrote ${outPath} (${outIr.slides.length} slides, ${placeholderCount} placeholder${placeholderCount === 1 ? "" : "s"})`;
|
|
2291
|
-
|
|
2292
|
-
if (generatedSeed !== void 0) {
|
|
2293
|
-
notes.push(`note: generated seed ${generatedSeed} \u2014 add "seed": ${generatedSeed} to deck.spec.json for revision stability`);
|
|
2294
|
-
}
|
|
2295
|
-
if (materializedLayoutCount !== void 0) {
|
|
2296
|
-
notes.push(
|
|
2297
|
-
`note: ${materializedLayoutCount} layout${materializedLayoutCount === 1 ? "" : "s"} auto-selected into deck.json \u2014 pin "layout" in a page file to lock one`
|
|
2298
|
-
);
|
|
2299
|
-
}
|
|
2300
|
-
return [summary, ...notes].join("\n");
|
|
2874
|
+
return summary;
|
|
2301
2875
|
}
|
|
2302
2876
|
async function runDisassemble(irPath, outDir) {
|
|
2303
2877
|
const raw = await loadIrFile(irPath);
|
|
@@ -2307,203 +2881,41 @@ ${formatIssues(v.errors)}`);
|
|
|
2307
2881
|
const { spec: spec2, pages } = disassembleDeck(v.ir);
|
|
2308
2882
|
const ids = Object.keys(pages);
|
|
2309
2883
|
for (const id of ids) assertSafeFileSegment(id, "slide id");
|
|
2310
|
-
const specPath =
|
|
2311
|
-
await
|
|
2884
|
+
const specPath = join7(outDir, "deck.spec.json");
|
|
2885
|
+
await mkdir5(outDir, { recursive: true });
|
|
2312
2886
|
try {
|
|
2313
|
-
await
|
|
2887
|
+
await writeFile4(specPath, JSON.stringify(spec2, null, 2) + "\n", { flag: "wx" });
|
|
2314
2888
|
} catch (e) {
|
|
2315
2889
|
if (e.code === "EEXIST") {
|
|
2316
2890
|
throw new PptwiseError(`${specPath} already exists \u2014 refusing to overwrite an existing deck project`);
|
|
2317
2891
|
}
|
|
2318
2892
|
throw e;
|
|
2319
2893
|
}
|
|
2320
|
-
const pagesDir =
|
|
2894
|
+
const pagesDir = join7(outDir, "pages");
|
|
2321
2895
|
try {
|
|
2322
2896
|
if (ids.length > 0) {
|
|
2323
|
-
await
|
|
2897
|
+
await mkdir5(pagesDir, { recursive: true });
|
|
2324
2898
|
await Promise.all(
|
|
2325
2899
|
ids.map((id) => {
|
|
2326
2900
|
const content = pages[id];
|
|
2327
|
-
return
|
|
2901
|
+
return writeFile4(join7(pagesDir, `${id}.json`), JSON.stringify(content, null, 2) + "\n");
|
|
2328
2902
|
})
|
|
2329
2903
|
);
|
|
2330
2904
|
}
|
|
2331
2905
|
const { count: assetCount, assetsDir } = await writeDeckAssets(
|
|
2332
2906
|
v.ir.assets.images,
|
|
2333
2907
|
outDir,
|
|
2334
|
-
|
|
2908
|
+
dirname5(resolve7(irPath))
|
|
2335
2909
|
);
|
|
2336
2910
|
const pagesNote = ids.length > 0 ? `${ids.length} page file${ids.length === 1 ? "" : "s"} to ${pagesDir}` : "no pages (every slide was a placeholder)";
|
|
2337
2911
|
const assetsNote = assetCount > 0 ? `, and ${assetCount} asset file${assetCount === 1 ? "" : "s"} to ${assetsDir}` : "";
|
|
2338
2912
|
return `wrote ${specPath}, ${pagesNote}${assetsNote}`;
|
|
2339
2913
|
} catch (e) {
|
|
2340
|
-
await
|
|
2914
|
+
await rm2(specPath, { force: true }).catch(() => {
|
|
2341
2915
|
});
|
|
2342
2916
|
throw e;
|
|
2343
2917
|
}
|
|
2344
2918
|
}
|
|
2345
|
-
async function runMigrate(input, output, cwd = process.cwd()) {
|
|
2346
|
-
const resolvedInput = resolve6(cwd, input);
|
|
2347
|
-
if (await isDeckDirectory(resolvedInput)) {
|
|
2348
|
-
return runMigrateDeckDir(resolvedInput, output, cwd);
|
|
2349
|
-
}
|
|
2350
|
-
return runMigrateIrFile(resolvedInput, output, cwd);
|
|
2351
|
-
}
|
|
2352
|
-
function isPlainRecord(value) {
|
|
2353
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2354
|
-
}
|
|
2355
|
-
function needsChromeRewrite(raw) {
|
|
2356
|
-
return Object.hasOwn(raw, "chrome");
|
|
2357
|
-
}
|
|
2358
|
-
function needsBloomRewrite(raw) {
|
|
2359
|
-
const theme = raw.theme;
|
|
2360
|
-
if (theme === "bloom") return true;
|
|
2361
|
-
return isPlainRecord(theme) && theme.id === "bloom";
|
|
2362
|
-
}
|
|
2363
|
-
function componentListHasLogoWall(components) {
|
|
2364
|
-
return Array.isArray(components) && components.some((component) => isPlainRecord(component) && component.type === "logo_wall");
|
|
2365
|
-
}
|
|
2366
|
-
function needsLogoWallRewrite(raw) {
|
|
2367
|
-
if (componentListHasLogoWall(raw.components)) return true;
|
|
2368
|
-
if (!Array.isArray(raw.slides)) return false;
|
|
2369
|
-
return raw.slides.some((slide) => isPlainRecord(slide) && componentListHasLogoWall(slide.components));
|
|
2370
|
-
}
|
|
2371
|
-
function recordHasBannerHeadingPin(obj) {
|
|
2372
|
-
return obj.layout === "banner-heading" || obj.focus === "banner-heading";
|
|
2373
|
-
}
|
|
2374
|
-
function needsBannerHeadingRewrite(raw) {
|
|
2375
|
-
if (recordHasBannerHeadingPin(raw)) return true;
|
|
2376
|
-
if (Array.isArray(raw.slides) && raw.slides.some((slide) => isPlainRecord(slide) && recordHasBannerHeadingPin(slide))) {
|
|
2377
|
-
return true;
|
|
2378
|
-
}
|
|
2379
|
-
if (Array.isArray(raw.pages) && raw.pages.some((page) => isPlainRecord(page) && recordHasBannerHeadingPin(page))) {
|
|
2380
|
-
return true;
|
|
2381
|
-
}
|
|
2382
|
-
return false;
|
|
2383
|
-
}
|
|
2384
|
-
function migrateRewriteNote(chrome, bloom, logoWall = false, bannerHeading = false) {
|
|
2385
|
-
const parts = [];
|
|
2386
|
-
if (chrome) parts.push("renamed chrome \u2192 branding");
|
|
2387
|
-
if (bloom) parts.push("relocated bloom \u2192 classroom");
|
|
2388
|
-
if (logoWall) parts.push("rewrote logo_wall \u2192 image_grid");
|
|
2389
|
-
if (bannerHeading) parts.push("rewrote banner-heading \u2192 two-column");
|
|
2390
|
-
return parts.join(", ");
|
|
2391
|
-
}
|
|
2392
|
-
function applyV4LeftoverRewrites(raw) {
|
|
2393
|
-
return migrateBannerHeadingToTwoColumn(
|
|
2394
|
-
migrateLogoWallToImageGrid(migrateBloomToClassroom(migrateChromeToBranding(raw)))
|
|
2395
|
-
);
|
|
2396
|
-
}
|
|
2397
|
-
async function listPageJsonNames(dir) {
|
|
2398
|
-
const pagesDir = join5(dir, PAGES_DIRNAME);
|
|
2399
|
-
try {
|
|
2400
|
-
const entries = await readdir3(pagesDir);
|
|
2401
|
-
return entries.filter((name) => name.endsWith(".json")).sort();
|
|
2402
|
-
} catch (e) {
|
|
2403
|
-
if (e.code === "ENOENT") return [];
|
|
2404
|
-
throw e;
|
|
2405
|
-
}
|
|
2406
|
-
}
|
|
2407
|
-
async function rewriteLeftoverPages(dir, outDir) {
|
|
2408
|
-
const names = await listPageJsonNames(dir);
|
|
2409
|
-
const written = [];
|
|
2410
|
-
let logoWall = false;
|
|
2411
|
-
let bannerHeading = false;
|
|
2412
|
-
for (const name of names) {
|
|
2413
|
-
const src = join5(dir, PAGES_DIRNAME, name);
|
|
2414
|
-
const raw = await loadIrFile(src, "page");
|
|
2415
|
-
if (!isPlainRecord(raw)) continue;
|
|
2416
|
-
const hasLogo = needsLogoWallRewrite(raw);
|
|
2417
|
-
const hasBanner = needsBannerHeadingRewrite(raw);
|
|
2418
|
-
if (!hasLogo && !hasBanner) continue;
|
|
2419
|
-
if (hasLogo) logoWall = true;
|
|
2420
|
-
if (hasBanner) bannerHeading = true;
|
|
2421
|
-
const dest = join5(outDir, PAGES_DIRNAME, name);
|
|
2422
|
-
await writeMigratedJson(dest, migrateBannerHeadingToTwoColumn(migrateLogoWallToImageGrid(raw)));
|
|
2423
|
-
written.push(dest);
|
|
2424
|
-
}
|
|
2425
|
-
return { paths: written, logoWall, bannerHeading };
|
|
2426
|
-
}
|
|
2427
|
-
async function writeMigratedJson(outPath, data) {
|
|
2428
|
-
await mkdir4(dirname3(outPath), { recursive: true });
|
|
2429
|
-
try {
|
|
2430
|
-
await writeFile3(outPath, JSON.stringify(data, null, 2) + "\n", { flag: "wx" });
|
|
2431
|
-
} catch (e) {
|
|
2432
|
-
if (e.code === "EEXIST") {
|
|
2433
|
-
throw new PptwiseError(`${outPath} already exists \u2014 refusing to overwrite, delete it first or choose a different -o`);
|
|
2434
|
-
}
|
|
2435
|
-
throw e;
|
|
2436
|
-
}
|
|
2437
|
-
}
|
|
2438
|
-
async function runMigrateDeckDir(dir, output, cwd) {
|
|
2439
|
-
const planPath = join5(dir, PLAN_FILENAME);
|
|
2440
|
-
const sourceSpecPath = join5(dir, SPEC_FILENAME);
|
|
2441
|
-
const outDir = resolve6(cwd, output);
|
|
2442
|
-
const specPath = join5(outDir, SPEC_FILENAME);
|
|
2443
|
-
if (!await pathExists(planPath) && await pathExists(sourceSpecPath)) {
|
|
2444
|
-
const raw2 = await loadIrFile(sourceSpecPath, "spec");
|
|
2445
|
-
const specNeeds = isPlainRecord(raw2) && (needsChromeRewrite(raw2) || needsBloomRewrite(raw2) || needsBannerHeadingRewrite(raw2));
|
|
2446
|
-
const pages2 = await rewriteLeftoverPages(dir, outDir);
|
|
2447
|
-
if (specNeeds && isPlainRecord(raw2)) {
|
|
2448
|
-
const chrome = needsChromeRewrite(raw2);
|
|
2449
|
-
const bloom = needsBloomRewrite(raw2);
|
|
2450
|
-
const bannerHeading = needsBannerHeadingRewrite(raw2);
|
|
2451
|
-
const migrated2 = applyV4LeftoverRewrites(raw2);
|
|
2452
|
-
await writeMigratedJson(specPath, migrated2);
|
|
2453
|
-
const specNote2 = `wrote ${specPath} (${migrateRewriteNote(chrome, bloom, false, bannerHeading)})`;
|
|
2454
|
-
if (pages2.paths.length === 0) return specNote2;
|
|
2455
|
-
return `${specNote2}, wrote ${pages2.paths.length === 1 ? pages2.paths[0] : join5(outDir, PAGES_DIRNAME)} (${migrateRewriteNote(false, false, pages2.logoWall, pages2.bannerHeading)})`;
|
|
2456
|
-
}
|
|
2457
|
-
if (pages2.paths.length > 0) {
|
|
2458
|
-
const target = pages2.paths.length === 1 ? pages2.paths[0] : join5(outDir, PAGES_DIRNAME);
|
|
2459
|
-
return `wrote ${target} (${migrateRewriteNote(false, false, pages2.logoWall, pages2.bannerHeading)})`;
|
|
2460
|
-
}
|
|
2461
|
-
throw new PptwiseError(
|
|
2462
|
-
`${dir} has ${SPEC_FILENAME} but no ${PLAN_FILENAME} \u2014 this deck project is already migrated, nothing to do`
|
|
2463
|
-
);
|
|
2464
|
-
}
|
|
2465
|
-
const raw = await loadIrFile(planPath, "plan");
|
|
2466
|
-
const migrated = migrateDeckPlanToSpec(raw);
|
|
2467
|
-
await writeMigratedJson(specPath, migrated);
|
|
2468
|
-
const pages = await rewriteLeftoverPages(dir, outDir);
|
|
2469
|
-
const specNote = `wrote ${specPath} \u2014 run \`pptwise spec validate ${specPath}\` to confirm it, then delete ${planPath} (a directory with both files present is rejected)`;
|
|
2470
|
-
if (pages.paths.length === 0) return specNote;
|
|
2471
|
-
return `${specNote}, wrote ${pages.paths.length === 1 ? pages.paths[0] : join5(outDir, PAGES_DIRNAME)} (${migrateRewriteNote(false, false, pages.logoWall, pages.bannerHeading)})`;
|
|
2472
|
-
}
|
|
2473
|
-
async function runMigrateIrFile(filePath, output, cwd) {
|
|
2474
|
-
const raw = await loadIrFile(filePath);
|
|
2475
|
-
const version = typeof raw === "object" && raw !== null ? raw.version : void 0;
|
|
2476
|
-
if (version === "2") {
|
|
2477
|
-
throw new PptwiseError(
|
|
2478
|
-
"pptwise migrate does not support IR v2 (spec \xA715.3: v2 has no real users) \u2014 run `pptwise validate` on the v2 file to see the full v2\u2192v4 combined field mapping and rewrite it by hand"
|
|
2479
|
-
);
|
|
2480
|
-
}
|
|
2481
|
-
const outPath = resolve6(cwd, output);
|
|
2482
|
-
if (version === "3") {
|
|
2483
|
-
const pre = isPlainRecord(raw) ? migrateLogoWallToImageGrid(raw) : raw;
|
|
2484
|
-
const parsed = PptxIRV3Schema.safeParse(pre);
|
|
2485
|
-
if (!parsed.success) {
|
|
2486
|
-
const detail = parsed.error.issues.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`).join("\n");
|
|
2487
|
-
throw new PptwiseError(`invalid IR v3 file ${filePath}:
|
|
2488
|
-
${detail}`);
|
|
2489
|
-
}
|
|
2490
|
-
const migrated = migrateIrV3ToV4(parsed.data);
|
|
2491
|
-
await writeMigratedJson(outPath, migrated);
|
|
2492
|
-
return `wrote ${outPath} (migrated IR v3 \u2192 v4)`;
|
|
2493
|
-
}
|
|
2494
|
-
if (isPlainRecord(raw) && (needsChromeRewrite(raw) || needsBloomRewrite(raw) || needsLogoWallRewrite(raw) || needsBannerHeadingRewrite(raw))) {
|
|
2495
|
-
const chrome = needsChromeRewrite(raw);
|
|
2496
|
-
const bloom = needsBloomRewrite(raw);
|
|
2497
|
-
const logoWall = needsLogoWallRewrite(raw);
|
|
2498
|
-
const bannerHeading = needsBannerHeadingRewrite(raw);
|
|
2499
|
-
const migrated = applyV4LeftoverRewrites(raw);
|
|
2500
|
-
await writeMigratedJson(outPath, migrated);
|
|
2501
|
-
return `wrote ${outPath} (${migrateRewriteNote(chrome, bloom, logoWall, bannerHeading)})`;
|
|
2502
|
-
}
|
|
2503
|
-
throw new PptwiseError(
|
|
2504
|
-
`pptwise migrate converts an IR v3 file (version: "3"), a v4 IR or deck spec still carrying the old chrome field (renamed to branding), the removed bloom theme id (relocated to classroom), a leftover logo_wall component (rewritten to image_grid), or a leftover banner-heading layout pin (rewritten to two-column), or a deck project directory containing ${PLAN_FILENAME} \u2014 got version ${JSON.stringify(version)} in ${filePath} with nothing to migrate`
|
|
2505
|
-
);
|
|
2506
|
-
}
|
|
2507
2919
|
|
|
2508
2920
|
// src/cli/secret-input.ts
|
|
2509
2921
|
import readline from "readline";
|
|
@@ -2518,7 +2930,7 @@ async function readSecret(prompt, io = { stdin: process.stdin, stderr: process.s
|
|
|
2518
2930
|
return await readHiddenTty(prompt, stdin, stderr);
|
|
2519
2931
|
}
|
|
2520
2932
|
function readFirstLine(stdin) {
|
|
2521
|
-
return new Promise((
|
|
2933
|
+
return new Promise((resolve9, reject) => {
|
|
2522
2934
|
const encoding = stdin.readableEncoding ?? "utf8";
|
|
2523
2935
|
let buf = "";
|
|
2524
2936
|
const onData = (chunk) => {
|
|
@@ -2526,11 +2938,11 @@ function readFirstLine(stdin) {
|
|
|
2526
2938
|
const nl = buf.indexOf("\n");
|
|
2527
2939
|
if (nl === -1) return;
|
|
2528
2940
|
cleanup();
|
|
2529
|
-
|
|
2941
|
+
resolve9(buf.slice(0, nl).replace(/\r$/, "").trim());
|
|
2530
2942
|
};
|
|
2531
2943
|
const onEnd = () => {
|
|
2532
2944
|
cleanup();
|
|
2533
|
-
|
|
2945
|
+
resolve9(buf.replace(/\r$/, "").trim());
|
|
2534
2946
|
};
|
|
2535
2947
|
const onError = (e) => {
|
|
2536
2948
|
cleanup();
|
|
@@ -2547,7 +2959,7 @@ function readFirstLine(stdin) {
|
|
|
2547
2959
|
});
|
|
2548
2960
|
}
|
|
2549
2961
|
function readHiddenTty(prompt, stdin, stderr) {
|
|
2550
|
-
return new Promise((
|
|
2962
|
+
return new Promise((resolve9, reject) => {
|
|
2551
2963
|
const rl = readline.createInterface({ input: stdin, output: stderr, terminal: true });
|
|
2552
2964
|
const mutable = rl;
|
|
2553
2965
|
mutable._writeToOutput = () => {
|
|
@@ -2565,7 +2977,7 @@ function readHiddenTty(prompt, stdin, stderr) {
|
|
|
2565
2977
|
stderr.write("\n");
|
|
2566
2978
|
const value = answer.trim();
|
|
2567
2979
|
if (value === "") reject(new PptwiseError("API key cannot be empty"));
|
|
2568
|
-
else
|
|
2980
|
+
else resolve9(value);
|
|
2569
2981
|
});
|
|
2570
2982
|
});
|
|
2571
2983
|
rl.on("SIGINT", () => {
|
|
@@ -2634,13 +3046,13 @@ async function runConfigShow(opts = {}) {
|
|
|
2634
3046
|
|
|
2635
3047
|
// src/cli/doctor.ts
|
|
2636
3048
|
import { lstatSync as lstatSync2 } from "fs";
|
|
2637
|
-
import { access, readFile as readFile7, readdir as
|
|
3049
|
+
import { access, readFile as readFile7, readdir as readdir4 } from "fs/promises";
|
|
2638
3050
|
import { homedir } from "os";
|
|
2639
|
-
import { basename as
|
|
3051
|
+
import { basename as basename6, join as join9 } from "path";
|
|
2640
3052
|
|
|
2641
3053
|
// src/cli/image-generators.ts
|
|
2642
|
-
import { copyFile as copyFile2, readdir as
|
|
2643
|
-
import { join as
|
|
3054
|
+
import { copyFile as copyFile2, readdir as readdir3, stat as stat3 } from "fs/promises";
|
|
3055
|
+
import { join as join8 } from "path";
|
|
2644
3056
|
var GENERATOR_PROBE_TIMEOUT_MS = 2e3;
|
|
2645
3057
|
var BIN_NAMES = {
|
|
2646
3058
|
grok: ["grok"],
|
|
@@ -2713,12 +3125,12 @@ async function isSniffedImage(path2) {
|
|
|
2713
3125
|
async function collectFiles(dir, acc) {
|
|
2714
3126
|
let entries;
|
|
2715
3127
|
try {
|
|
2716
|
-
entries = await
|
|
3128
|
+
entries = await readdir3(dir, { withFileTypes: true });
|
|
2717
3129
|
} catch {
|
|
2718
3130
|
return;
|
|
2719
3131
|
}
|
|
2720
3132
|
for (const entry of entries) {
|
|
2721
|
-
const full =
|
|
3133
|
+
const full = join8(dir, entry.name);
|
|
2722
3134
|
if (entry.isDirectory()) {
|
|
2723
3135
|
await collectFiles(full, acc);
|
|
2724
3136
|
} else if (entry.isFile()) {
|
|
@@ -2902,18 +3314,19 @@ var SKILL_COPY_FILES = [
|
|
|
2902
3314
|
"references/validate.zh-CN.md"
|
|
2903
3315
|
];
|
|
2904
3316
|
var SKILL_ROOTS = [
|
|
2905
|
-
{ harness: "Claude Code", relative:
|
|
2906
|
-
{ harness: "Codex", relative:
|
|
2907
|
-
{ harness: "Pi, OpenCode", relative:
|
|
3317
|
+
{ harness: "Claude Code", relative: join9(".claude", "skills") },
|
|
3318
|
+
{ harness: "Codex", relative: join9(".codex", "skills") },
|
|
3319
|
+
{ harness: "Pi, OpenCode", relative: join9(".agents", "skills") }
|
|
2908
3320
|
];
|
|
2909
3321
|
var SELF_TEST_DECK = {
|
|
2910
|
-
version: "
|
|
3322
|
+
version: "5",
|
|
2911
3323
|
filename: "pptwise-doctor-self-test.pptx",
|
|
2912
3324
|
theme: { id: "consulting" },
|
|
2913
3325
|
slides: [
|
|
2914
3326
|
{ type: "cover", heading: "pptwise doctor", subheading: "self-test render" },
|
|
2915
3327
|
{
|
|
2916
3328
|
type: "content",
|
|
3329
|
+
kind: "points",
|
|
2917
3330
|
heading: "Core chain",
|
|
2918
3331
|
components: [{ type: "bullets", items: ["validate the IR", "render a slide to SVG", "generate the .pptx bytes"] }]
|
|
2919
3332
|
}
|
|
@@ -2941,15 +3354,15 @@ async function scanSkillCopies(home, currentVersion) {
|
|
|
2941
3354
|
const scanned = [];
|
|
2942
3355
|
const copies = [];
|
|
2943
3356
|
for (const { harness, relative: relative3 } of SKILL_ROOTS) {
|
|
2944
|
-
const root =
|
|
3357
|
+
const root = join9(home, relative3);
|
|
2945
3358
|
scanned.push(root);
|
|
2946
3359
|
for (const { dirName, legacy } of [
|
|
2947
3360
|
{ dirName: SKILL_DIR_NAME, legacy: false },
|
|
2948
3361
|
...LEGACY_SKILL_DIR_NAMES.map((dirName2) => ({ dirName: dirName2, legacy: true }))
|
|
2949
3362
|
]) {
|
|
2950
|
-
const copyDir =
|
|
3363
|
+
const copyDir = join9(root, dirName);
|
|
2951
3364
|
if (!await pathExists2(copyDir)) continue;
|
|
2952
|
-
const launcherPath =
|
|
3365
|
+
const launcherPath = join9(copyDir, "scripts", "run.sh");
|
|
2953
3366
|
let pinned = null;
|
|
2954
3367
|
let launcher = null;
|
|
2955
3368
|
try {
|
|
@@ -2959,7 +3372,7 @@ async function scanSkillCopies(home, currentVersion) {
|
|
|
2959
3372
|
}
|
|
2960
3373
|
const missing = [];
|
|
2961
3374
|
for (const rel of SKILL_COPY_FILES) {
|
|
2962
|
-
if (!await pathExists2(
|
|
3375
|
+
if (!await pathExists2(join9(copyDir, rel))) missing.push(rel);
|
|
2963
3376
|
}
|
|
2964
3377
|
copies.push({
|
|
2965
3378
|
harness,
|
|
@@ -2976,7 +3389,7 @@ async function scanSkillCopies(home, currentVersion) {
|
|
|
2976
3389
|
return { scanned, copies };
|
|
2977
3390
|
}
|
|
2978
3391
|
async function readInstalledPluginVersion(profileDir) {
|
|
2979
|
-
const pkgPath =
|
|
3392
|
+
const pkgPath = join9(profileDir, "node_modules", ...PACKAGE_NAME.split("/"), "package.json");
|
|
2980
3393
|
try {
|
|
2981
3394
|
const pkg = JSON.parse(await readFile7(pkgPath, "utf8"));
|
|
2982
3395
|
return typeof pkg.version === "string" ? pkg.version : null;
|
|
@@ -2985,22 +3398,22 @@ async function readInstalledPluginVersion(profileDir) {
|
|
|
2985
3398
|
}
|
|
2986
3399
|
}
|
|
2987
3400
|
async function inspectDsh(home, currentVersion) {
|
|
2988
|
-
const dshHome =
|
|
3401
|
+
const dshHome = join9(home, ".dsh");
|
|
2989
3402
|
if (!await pathExists2(dshHome)) return { applicable: false, home: dshHome, profiles: [] };
|
|
2990
|
-
const profilesDir =
|
|
3403
|
+
const profilesDir = join9(dshHome, "profiles");
|
|
2991
3404
|
let entries;
|
|
2992
3405
|
try {
|
|
2993
|
-
const dirents = await
|
|
3406
|
+
const dirents = await readdir4(profilesDir, { withFileTypes: true });
|
|
2994
3407
|
entries = dirents.filter((d) => d.isDirectory() && d.name !== "node_modules").map((d) => d.name);
|
|
2995
3408
|
} catch {
|
|
2996
3409
|
return { applicable: true, home: dshHome, profiles: [] };
|
|
2997
3410
|
}
|
|
2998
3411
|
const profiles = [];
|
|
2999
3412
|
for (const name of entries.sort()) {
|
|
3000
|
-
const profileDir =
|
|
3413
|
+
const profileDir = join9(profilesDir, name);
|
|
3001
3414
|
let declared;
|
|
3002
3415
|
try {
|
|
3003
|
-
const pkg = JSON.parse(await readFile7(
|
|
3416
|
+
const pkg = JSON.parse(await readFile7(join9(profileDir, "package.json"), "utf8"));
|
|
3004
3417
|
declared = pkg.dependencies?.[PACKAGE_NAME];
|
|
3005
3418
|
} catch {
|
|
3006
3419
|
continue;
|
|
@@ -3171,8 +3584,8 @@ async function buildDoctorReport(input = {}) {
|
|
|
3171
3584
|
if (copy.legacy) {
|
|
3172
3585
|
warnings.push({
|
|
3173
3586
|
check: "skill copy",
|
|
3174
|
-
message: `${copy.path} is a leftover ${
|
|
3175
|
-
fix: `remove it and install the pptwise skill in ${
|
|
3587
|
+
message: `${copy.path} is a leftover ${basename6(copy.path)} skill copy`,
|
|
3588
|
+
fix: `remove it and install the pptwise skill in ${join9(copy.root, SKILL_DIR_NAME)}`
|
|
3176
3589
|
});
|
|
3177
3590
|
} else if (copy.stale) {
|
|
3178
3591
|
warnings.push({
|
|
@@ -3234,10 +3647,10 @@ function renderDoctorReport(report) {
|
|
|
3234
3647
|
} else {
|
|
3235
3648
|
for (const copy of report.skills.copies) {
|
|
3236
3649
|
const state = copy.legacy || copy.stale || copy.pinned === null || copy.missing.length > 0 ? "warn" : "ok";
|
|
3237
|
-
const pin = copy.legacy ? `leftover ${
|
|
3650
|
+
const pin = copy.legacy ? `leftover ${basename6(copy.path)} copy` : copy.pinned === null ? "version unknown" : `pins ${copy.pinned}`;
|
|
3238
3651
|
lines.push(` ${mark(state)} ${copy.harness}: ${copy.path} \u2014 ${pin}${copy.stale && !copy.legacy ? " (stale)" : ""}`);
|
|
3239
3652
|
if (copy.legacy) {
|
|
3240
|
-
lines.push(` fix: remove it and install the pptwise skill in ${
|
|
3653
|
+
lines.push(` fix: remove it and install the pptwise skill in ${join9(copy.root, SKILL_DIR_NAME)}`);
|
|
3241
3654
|
} else if (copy.stale) {
|
|
3242
3655
|
lines.push(` fix: ${skillRefreshCommand(copy.path)}`);
|
|
3243
3656
|
} else if (copy.pinned === null) {
|
|
@@ -3353,9 +3766,9 @@ async function runDoctor(opts = {}) {
|
|
|
3353
3766
|
}
|
|
3354
3767
|
|
|
3355
3768
|
// src/cli/images.ts
|
|
3356
|
-
import { mkdir as
|
|
3769
|
+
import { mkdir as mkdir6, mkdtemp, readFile as readFile8, readdir as readdir5, rename as rename2, rm as rm3, unlink as unlink2, writeFile as writeFile5 } from "fs/promises";
|
|
3357
3770
|
import { tmpdir } from "os";
|
|
3358
|
-
import { dirname as
|
|
3771
|
+
import { dirname as dirname6, join as join10, resolve as resolve8 } from "path";
|
|
3359
3772
|
|
|
3360
3773
|
// src/cli/redact.ts
|
|
3361
3774
|
var KEY_QUERY = /([?&](?:api_)?key=)[^&\s"'<>]*/gi;
|
|
@@ -3376,7 +3789,7 @@ var OPENVERSE_SEARCH_URL = "https://api.openverse.org/v1/images/";
|
|
|
3376
3789
|
var OPENVERSE_TOKEN_URL = "https://api.openverse.org/v1/auth_tokens/token/";
|
|
3377
3790
|
var OPENVERSE_PAGE_SIZE = 8;
|
|
3378
3791
|
var TOKEN_SKEW_MS = 3e4;
|
|
3379
|
-
var defaultSleep = (ms) => new Promise((
|
|
3792
|
+
var defaultSleep = (ms) => new Promise((resolve9) => setTimeout(resolve9, ms));
|
|
3380
3793
|
var tokenCache = /* @__PURE__ */ new Map();
|
|
3381
3794
|
function userAgent() {
|
|
3382
3795
|
return `pptwise/${VERSION} (+https://pptwise.com)`;
|
|
@@ -3933,7 +4346,7 @@ function parsePhotoRef(ref) {
|
|
|
3933
4346
|
}
|
|
3934
4347
|
async function resolveDeckWorkspace(deckArg, cwd) {
|
|
3935
4348
|
const [projectHit, userHit] = await Promise.all([findConfig(cwd), findUserConfig()]);
|
|
3936
|
-
const decksDirSource = projectHit?.config.decksDir !== void 0 ? { decksDir:
|
|
4349
|
+
const decksDirSource = projectHit?.config.decksDir !== void 0 ? { decksDir: resolve8(dirname6(projectHit.path), projectHit.config.decksDir) } : userHit?.config;
|
|
3937
4350
|
const target = await resolveDeckTarget(deckArg, decksDirSource, cwd);
|
|
3938
4351
|
const isDir = await isDeckDirectory(target);
|
|
3939
4352
|
const location = resolveWorkspaceLocation({
|
|
@@ -3943,7 +4356,7 @@ async function resolveDeckWorkspace(deckArg, cwd) {
|
|
|
3943
4356
|
target,
|
|
3944
4357
|
isDir
|
|
3945
4358
|
});
|
|
3946
|
-
return { location, assetsDir:
|
|
4359
|
+
return { location, assetsDir: join10(location.dir, ASSETS_DIRNAME) };
|
|
3947
4360
|
}
|
|
3948
4361
|
function assertSafeDownloadUrl(url) {
|
|
3949
4362
|
let parsed;
|
|
@@ -4099,8 +4512,8 @@ async function runImagesFetch(ref, opts) {
|
|
|
4099
4512
|
if (!apiKey) throw missingKeysError(provider);
|
|
4100
4513
|
}
|
|
4101
4514
|
const { assetsDir } = await resolveDeckWorkspace(opts.deck, cwd);
|
|
4102
|
-
const jpgPath =
|
|
4103
|
-
const jsonPath =
|
|
4515
|
+
const jpgPath = join10(assetsDir, `${opts.as}.jpg`);
|
|
4516
|
+
const jsonPath = join10(assetsDir, `${opts.as}.json`);
|
|
4104
4517
|
if (await pathExists(jpgPath) && await pathExists(jsonPath)) {
|
|
4105
4518
|
const existing = await readSidecar(jsonPath);
|
|
4106
4519
|
if (existing && existing.photo_id === photoId && existing.provider === provider) {
|
|
@@ -4150,8 +4563,8 @@ async function runImagesFetch(ref, opts) {
|
|
|
4150
4563
|
const apiLong = meta.width !== void 0 && meta.height !== void 0 ? Math.max(meta.width, meta.height) : void 0;
|
|
4151
4564
|
const resize = opts.resizeToJpeg ?? defaultResizeToJpeg;
|
|
4152
4565
|
const jpeg = await toJpeg(bytes, apiLong, resize);
|
|
4153
|
-
await
|
|
4154
|
-
await
|
|
4566
|
+
await mkdir6(assetsDir, { recursive: true });
|
|
4567
|
+
await writeFile5(jpgPath, jpeg);
|
|
4155
4568
|
const sidecar = {
|
|
4156
4569
|
provider,
|
|
4157
4570
|
photo_id: photoId,
|
|
@@ -4167,7 +4580,7 @@ async function runImagesFetch(ref, opts) {
|
|
|
4167
4580
|
if (/"apiKey"\s*:/.test(json) || /"key"\s*:/.test(json) || /"clientSecret"\s*:/.test(json)) {
|
|
4168
4581
|
throw new PptwiseError("internal error: sidecar would have contained a key field");
|
|
4169
4582
|
}
|
|
4170
|
-
await
|
|
4583
|
+
await writeFile5(jsonPath, json);
|
|
4171
4584
|
return `pinned ${provider}:${photoId} as ${opts.as} \u2192 ${jpgPath}`;
|
|
4172
4585
|
}
|
|
4173
4586
|
async function runImagesList(opts) {
|
|
@@ -4175,14 +4588,14 @@ async function runImagesList(opts) {
|
|
|
4175
4588
|
const { assetsDir } = await resolveDeckWorkspace(opts.deck, cwd);
|
|
4176
4589
|
let names;
|
|
4177
4590
|
try {
|
|
4178
|
-
names = (await
|
|
4591
|
+
names = (await readdir5(assetsDir)).filter((n) => n.endsWith(".json") && !n.startsWith(".")).sort();
|
|
4179
4592
|
} catch (e) {
|
|
4180
4593
|
if (e.code === "ENOENT") return "No pinned stock photos.";
|
|
4181
4594
|
throw e;
|
|
4182
4595
|
}
|
|
4183
4596
|
const lines = [];
|
|
4184
4597
|
for (const name of names) {
|
|
4185
|
-
const sidecar = await readSidecar(
|
|
4598
|
+
const sidecar = await readSidecar(join10(assetsDir, name));
|
|
4186
4599
|
if (!sidecar) continue;
|
|
4187
4600
|
const assetId = name.slice(0, -".json".length);
|
|
4188
4601
|
const id = sidecar.photo_id ? `${sidecar.provider}:${sidecar.photo_id}` : sidecar.provider;
|
|
@@ -4192,20 +4605,20 @@ async function runImagesList(opts) {
|
|
|
4192
4605
|
return lines.length === 0 ? "No pinned stock photos." : lines.join("\n");
|
|
4193
4606
|
}
|
|
4194
4607
|
async function writePinnedAsset(assetsDir, assetId, jpeg, sidecar) {
|
|
4195
|
-
await
|
|
4196
|
-
const jpgPath =
|
|
4197
|
-
const jsonPath =
|
|
4198
|
-
const tmpJpg =
|
|
4199
|
-
const tmpJson =
|
|
4608
|
+
await mkdir6(assetsDir, { recursive: true });
|
|
4609
|
+
const jpgPath = join10(assetsDir, `${assetId}.jpg`);
|
|
4610
|
+
const jsonPath = join10(assetsDir, `${assetId}.json`);
|
|
4611
|
+
const tmpJpg = join10(assetsDir, `.${assetId}.jpg.tmp`);
|
|
4612
|
+
const tmpJson = join10(assetsDir, `.${assetId}.json.tmp`);
|
|
4200
4613
|
const json = JSON.stringify(sidecar, null, 2) + "\n";
|
|
4201
4614
|
if (/"apiKey"\s*:/.test(json) || /"key"\s*:/.test(json) || /"clientSecret"\s*:/.test(json)) {
|
|
4202
4615
|
throw new PptwiseError("internal error: sidecar would have contained a key field");
|
|
4203
4616
|
}
|
|
4204
4617
|
try {
|
|
4205
|
-
await
|
|
4206
|
-
await
|
|
4207
|
-
await
|
|
4208
|
-
await
|
|
4618
|
+
await writeFile5(tmpJpg, jpeg);
|
|
4619
|
+
await writeFile5(tmpJson, json);
|
|
4620
|
+
await rename2(tmpJpg, jpgPath);
|
|
4621
|
+
await rename2(tmpJson, jsonPath);
|
|
4209
4622
|
} catch (e) {
|
|
4210
4623
|
await unlink2(tmpJpg).catch(() => void 0);
|
|
4211
4624
|
await unlink2(tmpJson).catch(() => void 0);
|
|
@@ -4247,8 +4660,8 @@ async function runImagesGenerate(opts) {
|
|
|
4247
4660
|
throw new PptwiseError(`No image generator is enabled. Found but disabled: ${listed}`);
|
|
4248
4661
|
}
|
|
4249
4662
|
const { assetsDir } = await resolveDeckWorkspace(opts.deck, cwd);
|
|
4250
|
-
const workdir = await mkdtemp(
|
|
4251
|
-
const dest =
|
|
4663
|
+
const workdir = await mkdtemp(join10(tmpdir(), "pptwise-gen-"));
|
|
4664
|
+
const dest = join10(workdir, "generated.jpg");
|
|
4252
4665
|
const run = opts.run ?? defaultProcessRunner;
|
|
4253
4666
|
const attempts = [];
|
|
4254
4667
|
try {
|
|
@@ -4290,7 +4703,7 @@ async function runImagesGenerate(opts) {
|
|
|
4290
4703
|
}
|
|
4291
4704
|
throw new PptwiseError(`All image generators failed: ${attempts.join("; ")}`);
|
|
4292
4705
|
} finally {
|
|
4293
|
-
await
|
|
4706
|
+
await rm3(workdir, { recursive: true, force: true });
|
|
4294
4707
|
}
|
|
4295
4708
|
}
|
|
4296
4709
|
|
|
@@ -4298,12 +4711,17 @@ async function runImagesGenerate(opts) {
|
|
|
4298
4711
|
import { watch } from "fs";
|
|
4299
4712
|
import { createServer } from "http";
|
|
4300
4713
|
import { platform as osPlatform } from "os";
|
|
4301
|
-
import { join as
|
|
4714
|
+
import { dirname as dirname7, join as join11 } from "path";
|
|
4302
4715
|
var DEFAULT_PORT = 4400;
|
|
4303
4716
|
var DEBOUNCE_MS = 200;
|
|
4304
4717
|
var HEARTBEAT_MS = 3e4;
|
|
4305
4718
|
function watchRoots(resolvedTarget, isDir, extra = []) {
|
|
4306
|
-
const roots = isDir ? [
|
|
4719
|
+
const roots = isDir ? [
|
|
4720
|
+
join11(resolvedTarget, SPEC_FILENAME),
|
|
4721
|
+
join11(resolvedTarget, PAGES_DIRNAME),
|
|
4722
|
+
join11(resolvedTarget, ASSETS_DIRNAME),
|
|
4723
|
+
join11(resolvedTarget, THEME_FILENAME)
|
|
4724
|
+
] : [resolvedTarget];
|
|
4307
4725
|
return [...roots, ...extra];
|
|
4308
4726
|
}
|
|
4309
4727
|
var SERVE_CLIENT_SCRIPT_ID = "pptwise-serve-client";
|
|
@@ -4366,7 +4784,7 @@ async function createServeServer(options) {
|
|
|
4366
4784
|
if (!Number.isInteger(requestedPort) || requestedPort < 0 || requestedPort > 65535) {
|
|
4367
4785
|
throw new PptwiseError(`invalid port ${requestedPort} \u2014 expected an integer between 0 and 65535`);
|
|
4368
4786
|
}
|
|
4369
|
-
const initial = await buildDeckPreview(options.target, { cwd
|
|
4787
|
+
const initial = await buildDeckPreview(options.target, { cwd });
|
|
4370
4788
|
let cachedHtml = injectServeClient(initial.html);
|
|
4371
4789
|
const sseClients = /* @__PURE__ */ new Set();
|
|
4372
4790
|
function writeToAll(chunk) {
|
|
@@ -4385,7 +4803,7 @@ data: ${JSON.stringify(data)}
|
|
|
4385
4803
|
}
|
|
4386
4804
|
async function rebuild() {
|
|
4387
4805
|
try {
|
|
4388
|
-
const result = await buildDeckPreview(options.target, { cwd
|
|
4806
|
+
const result = await buildDeckPreview(options.target, { cwd });
|
|
4389
4807
|
cachedHtml = injectServeClient(result.html);
|
|
4390
4808
|
broadcast("reload", {});
|
|
4391
4809
|
} catch (e) {
|
|
@@ -4424,7 +4842,7 @@ data: ${JSON.stringify(data)}
|
|
|
4424
4842
|
}, DEBOUNCE_MS);
|
|
4425
4843
|
}
|
|
4426
4844
|
const projectHit = await findConfig(cwd);
|
|
4427
|
-
const workspaceAssets =
|
|
4845
|
+
const workspaceAssets = join11(
|
|
4428
4846
|
resolveWorkspaceLocation({
|
|
4429
4847
|
cwd,
|
|
4430
4848
|
projectConfigPath: projectHit?.path,
|
|
@@ -4434,8 +4852,17 @@ data: ${JSON.stringify(data)}
|
|
|
4434
4852
|
}).dir,
|
|
4435
4853
|
ASSETS_DIRNAME
|
|
4436
4854
|
);
|
|
4855
|
+
const extraWatch = [workspaceAssets];
|
|
4856
|
+
try {
|
|
4857
|
+
const resolvedTheme = await resolveThemeByName(initial.ir.theme.id, {
|
|
4858
|
+
startDir: cwd,
|
|
4859
|
+
deckDir: initial.isDir ? initial.resolvedTarget : dirname7(initial.resolvedTarget)
|
|
4860
|
+
});
|
|
4861
|
+
if (resolvedTheme.kind === "file") extraWatch.push(resolvedTheme.path);
|
|
4862
|
+
} catch {
|
|
4863
|
+
}
|
|
4437
4864
|
const watchers = [];
|
|
4438
|
-
for (const path2 of watchRoots(initial.resolvedTarget, initial.isDir,
|
|
4865
|
+
for (const path2 of watchRoots(initial.resolvedTarget, initial.isDir, extraWatch)) {
|
|
4439
4866
|
try {
|
|
4440
4867
|
watchers.push(watch(path2, () => scheduleRebuild()));
|
|
4441
4868
|
} catch (e) {
|
|
@@ -4496,7 +4923,7 @@ function openBrowser(url) {
|
|
|
4496
4923
|
}
|
|
4497
4924
|
}
|
|
4498
4925
|
async function runServe(target, opts = {}) {
|
|
4499
|
-
const handle = await createServeServer({ target, port: opts.port, cwd: opts.cwd
|
|
4926
|
+
const handle = await createServeServer({ target, port: opts.port, cwd: opts.cwd });
|
|
4500
4927
|
console.log(`pptwise serve: ${handle.url} (Ctrl+C to stop)`);
|
|
4501
4928
|
if (opts.open !== false) openBrowser(handle.url);
|
|
4502
4929
|
process.on("SIGINT", () => {
|
|
@@ -4515,7 +4942,7 @@ function fail(e) {
|
|
|
4515
4942
|
console.error(e instanceof Error ? e.message : String(e));
|
|
4516
4943
|
process.exit(1);
|
|
4517
4944
|
}
|
|
4518
|
-
program.command("render").description("Render an IR JSON file, deck project directory, or bare deck name to a .pptx").argument("<target>", "IR JSON file, deck project directory, or bare name under ~/.pptwise/decks").option("-o, --output <file>", "output .pptx path (default: .pptwise/<deck>/<deck>.pptx under the project root)").option("--
|
|
4945
|
+
program.command("render").description("Render an IR JSON file, deck project directory, or bare deck name to a .pptx").argument("<target>", "IR JSON file, deck project directory, or bare name under ~/.pptwise/decks").option("-o, --output <file>", "output .pptx path (default: .pptwise/<deck>/<deck>.pptx under the project root)").option("--draft", "allow unfilled placeholder pages (skip the draft gate)").option(
|
|
4519
4946
|
"--allow-dropped-content",
|
|
4520
4947
|
"export anyway when a page holds more than fits and the layout drops blocks (skip the content-drop gate)"
|
|
4521
4948
|
).option("--no-git-ignore", "do not add .pptwise/ to this repository's local exclude file").action(
|
|
@@ -4524,9 +4951,6 @@ program.command("render").description("Render an IR JSON file, deck project dire
|
|
|
4524
4951
|
console.log(
|
|
4525
4952
|
await runRender(target, {
|
|
4526
4953
|
output: opts.output,
|
|
4527
|
-
theme: opts.theme,
|
|
4528
|
-
themeFilePath: opts.themeFile,
|
|
4529
|
-
stylePath: opts.style,
|
|
4530
4954
|
draft: opts.draft,
|
|
4531
4955
|
allowDroppedContent: opts.allowDroppedContent,
|
|
4532
4956
|
gitIgnore: opts.gitIgnore,
|
|
@@ -4538,21 +4962,20 @@ program.command("render").description("Render an IR JSON file, deck project dire
|
|
|
4538
4962
|
}
|
|
4539
4963
|
}
|
|
4540
4964
|
);
|
|
4541
|
-
program.command("validate").description("Validate an IR JSON file, deck project directory, or bare deck name against the schema").argument("<target>", "IR JSON file, deck project directory, or bare name under ~/.pptwise/decks").
|
|
4965
|
+
program.command("validate").description("Validate an IR JSON file, deck project directory, or bare deck name against the schema").argument("<target>", "IR JSON file, deck project directory, or bare name under ~/.pptwise/decks").action(async (target) => {
|
|
4542
4966
|
try {
|
|
4543
|
-
console.log(await runValidate(target, process.cwd()
|
|
4967
|
+
console.log(await runValidate(target, process.cwd()));
|
|
4544
4968
|
} catch (e) {
|
|
4545
4969
|
fail(e);
|
|
4546
4970
|
}
|
|
4547
4971
|
});
|
|
4548
4972
|
program.command("audit").description(
|
|
4549
|
-
"Deterministic geometry audit (overflow, out-of-bounds, low-contrast, overlap, content-truncated, content-dropped), plus an optional --pixels contrast pass \u2014 exits 1 when it finds anything"
|
|
4550
|
-
).argument("<target>", "IR JSON file, deck project directory, or bare name under ~/.pptwise/decks").option("--json", "machine-readable output (the full AuditReport)").option("--pixels", "also run the optional pixel-contrast pass over image-backed text (requires sharp)").
|
|
4973
|
+
"Deterministic geometry audit (overflow, out-of-bounds, low-contrast, overlap, content-truncated, content-dropped, monotony), plus an optional --pixels contrast pass \u2014 exits 1 when it finds anything"
|
|
4974
|
+
).argument("<target>", "IR JSON file, deck project directory, or bare name under ~/.pptwise/decks").option("--json", "machine-readable output (the full AuditReport)").option("--pixels", "also run the optional pixel-contrast pass over image-backed text (requires sharp)").action(async (target, opts) => {
|
|
4551
4975
|
try {
|
|
4552
4976
|
const { output, hasFindings } = await runAudit(target, {
|
|
4553
4977
|
json: opts.json,
|
|
4554
|
-
pixels: opts.pixels
|
|
4555
|
-
themeFilePath: opts.themeFile
|
|
4978
|
+
pixels: opts.pixels
|
|
4556
4979
|
});
|
|
4557
4980
|
console.log(output);
|
|
4558
4981
|
if (hasFindings) process.exit(1);
|
|
@@ -4569,15 +4992,8 @@ program.command("asset-brief").description(
|
|
|
4569
4992
|
fail(e);
|
|
4570
4993
|
}
|
|
4571
4994
|
});
|
|
4572
|
-
program.command("schema").description("Print the IR JSON Schema (feed this to a model before it writes IR)").option("--
|
|
4573
|
-
|
|
4574
|
-
fail(new Error("`pptwise schema --plan` has been renamed to `pptwise schema --spec` \u2014 run `pptwise schema --spec` instead"));
|
|
4575
|
-
}
|
|
4576
|
-
console.log(runSchema(opts.spec ? "spec" : opts.style ? "style" : void 0));
|
|
4577
|
-
});
|
|
4578
|
-
var plan = program.command("plan").description("Removed \u2014 use `pptwise spec` instead");
|
|
4579
|
-
plan.command("validate").description("Removed \u2014 use `pptwise spec validate` instead").argument("<file>").action(() => {
|
|
4580
|
-
fail(new Error("`pptwise plan validate` has been renamed to `pptwise spec validate` \u2014 run `pptwise spec validate <file>` instead"));
|
|
4995
|
+
program.command("schema").description("Print the IR JSON Schema (feed this to a model before it writes IR)").option("--spec", "print the deck spec schema instead").action((opts) => {
|
|
4996
|
+
console.log(runSchema(opts.spec ? "spec" : void 0));
|
|
4581
4997
|
});
|
|
4582
4998
|
var spec = program.command("spec").description("Deck spec commands (spec \xA76)");
|
|
4583
4999
|
spec.command("validate").description("Validate a deck spec JSON file against the schema and strategy-aware hard gates").argument("<spec.json>").action(async (specPath) => {
|
|
@@ -4601,28 +5017,62 @@ program.command("disassemble").description("Split an IR JSON file into a deck pr
|
|
|
4601
5017
|
fail(e);
|
|
4602
5018
|
}
|
|
4603
5019
|
});
|
|
4604
|
-
program.command("
|
|
5020
|
+
program.command("themes").description("List built-in themes").option("--json", "machine-readable output").action((opts) => console.log(runThemes(Boolean(opts.json))));
|
|
5021
|
+
var theme = program.command("theme").description("Copy, fork, and compare themes");
|
|
5022
|
+
theme.command("new").description("Copy a preset or named theme into a self-contained v2 theme file").requiredOption("--from <preset>", "preset or theme name to copy").option("-o, --output <path>", "output theme JSON path").option("--id <id>", "theme id (default: slug of the output filename)").option("--label <label>", "human-readable theme label").option("--force", "overwrite an existing theme file").addHelpText("after", "\nExample:\n $ pptwise theme new --from consulting -o themes/acme.theme.json").action(async (opts) => {
|
|
4605
5023
|
try {
|
|
4606
|
-
console.log(await
|
|
5024
|
+
console.log(await runThemeNew({ from: opts.from, output: opts.output, id: opts.id, label: opts.label, force: opts.force, cwd: process.cwd() }));
|
|
4607
5025
|
} catch (e) {
|
|
4608
5026
|
fail(e);
|
|
4609
5027
|
}
|
|
4610
5028
|
});
|
|
4611
|
-
|
|
5029
|
+
theme.command("fork").description("Copy a theme and rederive tokens around new anchor colors").argument("<name>", "preset or theme name to fork").requiredOption("--primary <hex>", "new primary color").option("--bg <hex>", "new background color").option("--accent <hex>", "new accent color").option("--text <hex>", "new text color").option("--surface <hex>", "new surface color").option("-o, --output <path>", "output theme JSON path").option("--id <id>", "theme id (default: slug of the output filename)").option("--label <label>", "human-readable theme label").option("--force", "overwrite an existing theme file").addHelpText("after", "\nExample:\n $ pptwise theme fork acme --primary #0B5FFF -o themes/acme-blue.theme.json").action(
|
|
5030
|
+
async (name, opts) => {
|
|
5031
|
+
try {
|
|
5032
|
+
console.log(
|
|
5033
|
+
await runThemeFork(name, {
|
|
5034
|
+
primary: opts.primary,
|
|
5035
|
+
bg: opts.bg,
|
|
5036
|
+
accent: opts.accent,
|
|
5037
|
+
text: opts.text,
|
|
5038
|
+
surface: opts.surface,
|
|
5039
|
+
output: opts.output,
|
|
5040
|
+
id: opts.id,
|
|
5041
|
+
label: opts.label,
|
|
5042
|
+
force: opts.force,
|
|
5043
|
+
cwd: process.cwd()
|
|
5044
|
+
})
|
|
5045
|
+
);
|
|
5046
|
+
} catch (e) {
|
|
5047
|
+
fail(e);
|
|
5048
|
+
}
|
|
5049
|
+
}
|
|
5050
|
+
);
|
|
5051
|
+
theme.command("try").description("Render the fitting-room sample across 2-4 themes into a contact sheet").argument("<ids>", "comma-separated theme ids (2-4)").option("-o, --output <dir>", "output directory (default: .pptwise/theme-try/)").addHelpText("after", "\nExample:\n $ pptwise theme try consulting,swiss,memo").action(async (ids, opts) => {
|
|
5052
|
+
try {
|
|
5053
|
+
console.log(await runThemeTry(ids, { output: opts.output, cwd: process.cwd() }));
|
|
5054
|
+
} catch (e) {
|
|
5055
|
+
fail(e);
|
|
5056
|
+
}
|
|
5057
|
+
});
|
|
5058
|
+
program.command("layouts").description("List registered layouts").option("--json", "machine-readable output").action((opts) => console.log(runLayouts(Boolean(opts.json))));
|
|
4612
5059
|
var brand = program.command("brand").description("Brand asset commands \u2014 extract your company's colors/fonts from an Office template");
|
|
4613
5060
|
brand.command("extract").description(
|
|
4614
5061
|
"Extract brand colors and fonts from a .thmx/.potx/.pptx file into a pptwise theme file \u2014 runs entirely locally, the file never leaves your machine"
|
|
4615
|
-
).argument("<file>", "a .thmx theme, .potx template, or .pptx presentation").requiredOption("-o, --output <file>", "output theme JSON path (e.g. my-brand.theme.json)").option("--id <id>", "theme id to register under (default: slug of the output filename)").option("--label <label>", "human-readable theme label (default: the source theme's color-scheme name)").action(async (file, opts) => {
|
|
5062
|
+
).argument("<file>", "a .thmx theme, .potx template, or .pptx presentation").requiredOption("-o, --output <file>", "output theme JSON path (e.g. my-brand.theme.json)").option("--id <id>", "theme id to register under (default: slug of the output filename)").option("--label <label>", "human-readable theme label (default: the source theme's color-scheme name)").option("--from <preset>", "donor preset whose menu is copied (default: consulting)").option("--force", "overwrite an existing theme file").addHelpText("after", "\nExample:\n $ pptwise brand extract corp.pptx -o themes/acme.theme.json --from consulting").action(async (file, opts) => {
|
|
4616
5063
|
try {
|
|
4617
|
-
console.log(await runBrandExtract(file, {
|
|
5064
|
+
console.log(await runBrandExtract(file, {
|
|
5065
|
+
output: opts.output,
|
|
5066
|
+
id: opts.id,
|
|
5067
|
+
label: opts.label,
|
|
5068
|
+
from: opts.from,
|
|
5069
|
+
force: opts.force
|
|
5070
|
+
}));
|
|
4618
5071
|
} catch (e) {
|
|
4619
5072
|
fail(e);
|
|
4620
5073
|
}
|
|
4621
5074
|
});
|
|
4622
5075
|
program.command("narratives").description("List named narrative presets (strategy/pacing/audience axes + theme recommendations)").option("--json", "machine-readable output").action((opts) => console.log(runNarratives(Boolean(opts.json))));
|
|
4623
|
-
program.command("scenarios").description("Removed \u2014 use `pptwise narratives` instead").action(() => {
|
|
4624
|
-
fail(new Error("`pptwise scenarios` has been renamed to `pptwise narratives` \u2014 run `pptwise narratives` instead"));
|
|
4625
|
-
});
|
|
4626
5076
|
var config = program.command("config").description("User-level settings (API keys for optional stock-photo search)");
|
|
4627
5077
|
config.command("set <key> [value]").description("Set a user config value. Omit the value for an apiKey or clientSecret to enter it at a hidden prompt").action(async (key, value) => {
|
|
4628
5078
|
try {
|
|
@@ -4693,12 +5143,11 @@ program.command("init").description("Scaffold a pptwise.config.json in the curre
|
|
|
4693
5143
|
fail(e);
|
|
4694
5144
|
}
|
|
4695
5145
|
});
|
|
4696
|
-
program.command("preview").description("Render each slide to an SVG file for visual self-check").argument("<target>", "IR JSON file, deck project directory, or bare name under ~/.pptwise/decks").option("-o, --output <dir>", "output directory (default: .pptwise/<deck>/ under the project root)").option("--html", "also write a self-contained preview.html (all slides inlined \u2014 thumbnail strip, keyboard navigation) for human review").option("--
|
|
5146
|
+
program.command("preview").description("Render each slide to an SVG file for visual self-check").argument("<target>", "IR JSON file, deck project directory, or bare name under ~/.pptwise/decks").option("-o, --output <dir>", "output directory (default: .pptwise/<deck>/ under the project root)").option("--html", "also write a self-contained preview.html (all slides inlined \u2014 thumbnail strip, keyboard navigation) for human review").option("--no-git-ignore", "do not add .pptwise/ to this repository's local exclude file").action(async (target, opts) => {
|
|
4697
5147
|
try {
|
|
4698
5148
|
console.log(
|
|
4699
5149
|
await runPreview(target, opts.output, {
|
|
4700
5150
|
htmlOut: opts.html,
|
|
4701
|
-
themeFilePath: opts.themeFile,
|
|
4702
5151
|
gitIgnore: opts.gitIgnore,
|
|
4703
5152
|
cwd: process.cwd()
|
|
4704
5153
|
})
|
|
@@ -4707,7 +5156,7 @@ program.command("preview").description("Render each slide to an SVG file for vis
|
|
|
4707
5156
|
fail(e);
|
|
4708
5157
|
}
|
|
4709
5158
|
});
|
|
4710
|
-
program.command("serve").description("Serve a live-reloading HTML preview of an IR JSON file, deck project directory, or bare deck name over HTTP").argument("<target>", "IR JSON file, deck project directory, or bare name under ~/.pptwise/decks").option("--port <number>", `port to listen on (default ${DEFAULT_PORT})`).option("--no-open", "do not open the URL in a browser after starting").
|
|
5159
|
+
program.command("serve").description("Serve a live-reloading HTML preview of an IR JSON file, deck project directory, or bare deck name over HTTP").argument("<target>", "IR JSON file, deck project directory, or bare name under ~/.pptwise/decks").option("--port <number>", `port to listen on (default ${DEFAULT_PORT})`).option("--no-open", "do not open the URL in a browser after starting").action(async (target, opts) => {
|
|
4711
5160
|
try {
|
|
4712
5161
|
let port;
|
|
4713
5162
|
if (opts.port !== void 0) {
|
|
@@ -4716,7 +5165,7 @@ program.command("serve").description("Serve a live-reloading HTML preview of an
|
|
|
4716
5165
|
fail(new Error(`invalid --port value "${opts.port}" \u2014 expected an integer`));
|
|
4717
5166
|
}
|
|
4718
5167
|
}
|
|
4719
|
-
await runServe(target, { port, open: opts.open
|
|
5168
|
+
await runServe(target, { port, open: opts.open });
|
|
4720
5169
|
} catch (e) {
|
|
4721
5170
|
fail(e);
|
|
4722
5171
|
}
|