stego-cli 0.4.2 → 0.5.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 +68 -22
- package/dist/shared/src/domain/frontmatter/validators.js +20 -2
- package/dist/shared/src/domain/images/index.js +1 -0
- package/dist/shared/src/domain/images/style.js +185 -0
- package/dist/shared/src/index.js +1 -0
- package/dist/stego-cli/src/app/command-registry.js +93 -1
- package/dist/stego-cli/src/app/create-cli-app.js +3 -0
- package/dist/stego-cli/src/app/error-boundary.js +11 -1
- package/dist/stego-cli/src/modules/comments/commands/comments-add.js +0 -1
- package/dist/stego-cli/src/modules/comments/commands/comments-clear-resolved.js +0 -1
- package/dist/stego-cli/src/modules/comments/commands/comments-delete.js +0 -1
- package/dist/stego-cli/src/modules/comments/commands/comments-read.js +0 -1
- package/dist/stego-cli/src/modules/comments/commands/comments-reply.js +0 -1
- package/dist/stego-cli/src/modules/comments/commands/comments-set-status.js +0 -1
- package/dist/stego-cli/src/modules/comments/commands/comments-sync-anchors.js +0 -1
- package/dist/stego-cli/src/modules/compile/application/compile-manuscript.js +12 -1
- package/dist/stego-cli/src/modules/compile/commands/build.js +1 -2
- package/dist/stego-cli/src/modules/compile/domain/compile-structure.js +0 -3
- package/dist/stego-cli/src/modules/compile/domain/image-settings.js +394 -0
- package/dist/stego-cli/src/modules/export/application/run-export.js +22 -1
- package/dist/stego-cli/src/modules/export/commands/export.js +1 -2
- package/dist/stego-cli/src/modules/export/infra/pandoc-exporter.js +29 -2
- package/dist/stego-cli/src/modules/manuscript/commands/new-manuscript.js +1 -1
- package/dist/stego-cli/src/modules/project/application/create-project.js +41 -1
- package/dist/stego-cli/src/modules/project/application/infer-project.js +1 -1
- package/dist/stego-cli/src/modules/project/commands/new-project.js +1 -1
- package/dist/stego-cli/src/modules/quality/application/inspect-project.js +147 -112
- package/dist/stego-cli/src/modules/quality/commands/check-stage.js +1 -2
- package/dist/stego-cli/src/modules/quality/commands/lint.js +1 -2
- package/dist/stego-cli/src/modules/quality/commands/validate.js +1 -2
- package/dist/stego-cli/src/modules/scaffold/commands/init.js +2 -2
- package/dist/stego-cli/src/modules/scaffold/domain/templates.js +15 -14
- package/dist/stego-cli/src/modules/spine/commands/spine-deprecated-aliases.js +2 -2
- package/dist/stego-cli/src/modules/spine/commands/spine-new-category.js +1 -2
- package/dist/stego-cli/src/modules/spine/commands/spine-new-entry.js +1 -2
- package/dist/stego-cli/src/modules/spine/commands/spine-read.js +1 -2
- package/filters/image-layout.css +23 -0
- package/filters/image-layout.lua +170 -0
- package/package.json +4 -2
- package/projects/fiction-example/assets/README.md +31 -0
- package/projects/stego-docs/assets/README.md +31 -0
- package/projects/stego-docs/manuscript/500-project-configuration.md +37 -0
- package/projects/stego-docs/manuscript/800-build-export-and-release-outputs.md +4 -0
- package/dist/stego-cli/src/modules/manuscript/domain/manuscript.js +0 -1
- package/dist/stego-cli/src/modules/project/domain/project.js +0 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { parseCommentAppendix } from "../../../../../shared/src/domain/comments/index.js";
|
|
4
|
+
import { parseMarkdownDocument } from "../../../../../shared/src/domain/frontmatter/index.js";
|
|
4
5
|
import { isStageName } from "../../../../../shared/src/domain/stages/index.js";
|
|
5
6
|
import { readSpineCatalogForProject } from "../../spine/index.js";
|
|
6
7
|
export function inspectProject(project, options = {}) {
|
|
@@ -12,6 +13,7 @@ export function inspectProject(project, options = {}) {
|
|
|
12
13
|
const compileStructureState = resolveCompileStructure(project);
|
|
13
14
|
issues.push(...requiredMetadataState.issues);
|
|
14
15
|
issues.push(...compileStructureState.issues);
|
|
16
|
+
issues.push(...validateProjectImagesConfiguration(project));
|
|
15
17
|
if (project.meta.spineCategories !== undefined) {
|
|
16
18
|
issues.push(makeIssue("error", "metadata", "Legacy 'spineCategories' in stego-project.json is no longer supported. Use spine/ category directories and files.", path.relative(repoRoot, path.join(project.root, "stego-project.json"))));
|
|
17
19
|
}
|
|
@@ -232,7 +234,8 @@ function parseChapter(chapterPath, project, requiredMetadata, spineCategories, c
|
|
|
232
234
|
}
|
|
233
235
|
const referenceValidation = extractReferenceKeysByCategory(metadata, relativePath, spineCategories);
|
|
234
236
|
chapterIssues.push(...referenceValidation.issues);
|
|
235
|
-
chapterIssues.push(...
|
|
237
|
+
chapterIssues.push(...validateImagesMetadata(metadata, relativePath));
|
|
238
|
+
chapterIssues.push(...validateMarkdownBody(body, chapterPath, repoRoot, project.root));
|
|
236
239
|
return {
|
|
237
240
|
path: chapterPath,
|
|
238
241
|
relativePath,
|
|
@@ -251,7 +254,7 @@ function normalizeGroupingValue(rawValue, relativePath, issues, key) {
|
|
|
251
254
|
if (rawValue == null || rawValue === "") {
|
|
252
255
|
return undefined;
|
|
253
256
|
}
|
|
254
|
-
if (Array.isArray(rawValue)) {
|
|
257
|
+
if (Array.isArray(rawValue) || isPlainObject(rawValue)) {
|
|
255
258
|
issues.push(makeIssue("error", "metadata", `Metadata '${key}' must be a scalar value.`, relativePath));
|
|
256
259
|
return undefined;
|
|
257
260
|
}
|
|
@@ -316,101 +319,135 @@ function extractReferenceKeysByCategory(metadata, relativePath, spineCategories)
|
|
|
316
319
|
}
|
|
317
320
|
return { referencesByCategory, issues };
|
|
318
321
|
}
|
|
322
|
+
function validateImagesMetadata(metadata, relativePath) {
|
|
323
|
+
const issues = [];
|
|
324
|
+
const rawImages = metadata.images;
|
|
325
|
+
if (rawImages == null) {
|
|
326
|
+
return issues;
|
|
327
|
+
}
|
|
328
|
+
if (!isPlainObject(rawImages)) {
|
|
329
|
+
issues.push(makeIssue("warning", "metadata", "Metadata 'images' must be an object.", relativePath));
|
|
330
|
+
return issues;
|
|
331
|
+
}
|
|
332
|
+
const reservedGlobalKeys = new Set(["width", "height", "classes", "id", "attrs", "layout", "align"]);
|
|
333
|
+
for (const [key, value] of Object.entries(rawImages)) {
|
|
334
|
+
if (reservedGlobalKeys.has(key)) {
|
|
335
|
+
issues.push(makeIssue("warning", "metadata", `Manuscript frontmatter 'images.${key}' is reserved for project defaults. Put defaults in stego-project.json 'images.${key}'.`, relativePath));
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
if (!isPlainObject(value)) {
|
|
339
|
+
issues.push(makeIssue("warning", "metadata", `Metadata 'images.${key}' must be an object of style keys (width, height, classes, id, attrs, layout, align).`, relativePath));
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
for (const [styleKey, styleValue] of Object.entries(value)) {
|
|
343
|
+
issues.push(...validateImageStyleField(styleValue, styleKey, `images.${key}.${styleKey}`, relativePath));
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
return issues;
|
|
347
|
+
}
|
|
348
|
+
function validateProjectImagesConfiguration(project) {
|
|
349
|
+
const issues = [];
|
|
350
|
+
const rawImages = project.meta.images;
|
|
351
|
+
const projectConfigPath = path.relative(project.workspace.repoRoot, path.join(project.root, "stego-project.json"));
|
|
352
|
+
if (rawImages == null) {
|
|
353
|
+
return issues;
|
|
354
|
+
}
|
|
355
|
+
if (!isPlainObject(rawImages)) {
|
|
356
|
+
issues.push(makeIssue("warning", "metadata", "Project 'images' must be an object.", projectConfigPath));
|
|
357
|
+
return issues;
|
|
358
|
+
}
|
|
359
|
+
const reservedGlobalKeys = new Set(["width", "height", "classes", "id", "attrs", "layout", "align"]);
|
|
360
|
+
for (const [key, value] of Object.entries(rawImages)) {
|
|
361
|
+
if (reservedGlobalKeys.has(key)) {
|
|
362
|
+
issues.push(...validateImageStyleField(value, key, `images.${key}`, projectConfigPath));
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
365
|
+
issues.push(makeIssue("warning", "metadata", `Project image defaults do not support key 'images.${key}'. Use only width, height, classes, id, attrs, layout, align in stego-project.json.`, projectConfigPath));
|
|
366
|
+
}
|
|
367
|
+
return issues;
|
|
368
|
+
}
|
|
369
|
+
function validateImageStyleField(value, key, metadataPath, relativePath) {
|
|
370
|
+
const issues = [];
|
|
371
|
+
if (key === "width" || key === "height" || key === "id") {
|
|
372
|
+
if (!isStyleScalar(value)) {
|
|
373
|
+
issues.push(makeIssue("warning", "metadata", `Metadata '${metadataPath}' must be a scalar value.`, relativePath));
|
|
374
|
+
}
|
|
375
|
+
return issues;
|
|
376
|
+
}
|
|
377
|
+
if (key === "classes") {
|
|
378
|
+
if (typeof value === "string") {
|
|
379
|
+
return issues;
|
|
380
|
+
}
|
|
381
|
+
if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string" || entry.trim().length === 0)) {
|
|
382
|
+
issues.push(makeIssue("warning", "metadata", `Metadata '${metadataPath}' must be a string or array of strings.`, relativePath));
|
|
383
|
+
}
|
|
384
|
+
return issues;
|
|
385
|
+
}
|
|
386
|
+
if (key === "attrs") {
|
|
387
|
+
if (!isPlainObject(value)) {
|
|
388
|
+
issues.push(makeIssue("warning", "metadata", `Metadata '${metadataPath}' must be an object of scalar values.`, relativePath));
|
|
389
|
+
return issues;
|
|
390
|
+
}
|
|
391
|
+
for (const [attrKey, attrValue] of Object.entries(value)) {
|
|
392
|
+
if (!isStyleScalar(attrValue)) {
|
|
393
|
+
issues.push(makeIssue("warning", "metadata", `Metadata '${metadataPath}.${attrKey}' must be a scalar value.`, relativePath));
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
return issues;
|
|
397
|
+
}
|
|
398
|
+
if (key === "layout") {
|
|
399
|
+
if (value !== "block" && value !== "inline") {
|
|
400
|
+
issues.push(makeIssue("warning", "metadata", `Metadata '${metadataPath}' must be either 'block' or 'inline'.`, relativePath));
|
|
401
|
+
}
|
|
402
|
+
return issues;
|
|
403
|
+
}
|
|
404
|
+
if (key === "align") {
|
|
405
|
+
if (value !== "left" && value !== "center" && value !== "right") {
|
|
406
|
+
issues.push(makeIssue("warning", "metadata", `Metadata '${metadataPath}' must be one of: left, center, right.`, relativePath));
|
|
407
|
+
}
|
|
408
|
+
return issues;
|
|
409
|
+
}
|
|
410
|
+
issues.push(makeIssue("warning", "metadata", `Unsupported image style key '${key}' in '${metadataPath}'. Allowed keys: width, height, classes, id, attrs, layout, align.`, relativePath));
|
|
411
|
+
return issues;
|
|
412
|
+
}
|
|
413
|
+
function isStyleScalar(value) {
|
|
414
|
+
return typeof value === "string" || typeof value === "number" || typeof value === "boolean";
|
|
415
|
+
}
|
|
319
416
|
function parseMetadata(raw, chapterPath, repoRoot, required) {
|
|
320
417
|
const relativePath = path.relative(repoRoot, chapterPath);
|
|
321
418
|
const issues = [];
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
if (!required) {
|
|
325
|
-
return {
|
|
326
|
-
metadata: {},
|
|
327
|
-
body: commentsResult.bodyWithoutComments,
|
|
328
|
-
comments: commentsResult.comments,
|
|
329
|
-
issues: commentsResult.issues
|
|
330
|
-
};
|
|
331
|
-
}
|
|
419
|
+
const frontmatterMatch = raw.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/);
|
|
420
|
+
if ((raw.startsWith("---\n") || raw.startsWith("---\r\n")) && !frontmatterMatch) {
|
|
332
421
|
return {
|
|
333
422
|
metadata: {},
|
|
334
|
-
body:
|
|
335
|
-
comments:
|
|
336
|
-
issues: [makeIssue("error", "metadata", "
|
|
423
|
+
body: raw,
|
|
424
|
+
comments: [],
|
|
425
|
+
issues: [makeIssue("error", "metadata", "Metadata opening delimiter found, but closing delimiter is missing.", relativePath)]
|
|
337
426
|
};
|
|
338
427
|
}
|
|
339
|
-
|
|
340
|
-
|
|
428
|
+
let parsed;
|
|
429
|
+
try {
|
|
430
|
+
parsed = parseMarkdownDocument(raw);
|
|
431
|
+
}
|
|
432
|
+
catch (error) {
|
|
433
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
341
434
|
return {
|
|
342
435
|
metadata: {},
|
|
343
436
|
body: raw,
|
|
344
437
|
comments: [],
|
|
345
|
-
issues: [makeIssue("error", "metadata",
|
|
438
|
+
issues: [makeIssue("error", "metadata", `Could not parse frontmatter: ${message}`, relativePath)]
|
|
346
439
|
};
|
|
347
440
|
}
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
const metadata = {};
|
|
351
|
-
const lines = metadataText.split(/\r?\n/);
|
|
352
|
-
for (let i = 0; i < lines.length; i += 1) {
|
|
353
|
-
const line = lines[i].trim();
|
|
354
|
-
if (!line || line.startsWith("#")) {
|
|
355
|
-
continue;
|
|
356
|
-
}
|
|
357
|
-
const separatorIndex = line.indexOf(":");
|
|
358
|
-
if (separatorIndex === -1) {
|
|
359
|
-
issues.push(makeIssue("error", "metadata", `Invalid metadata line '${line}'. Expected 'key: value' format.`, relativePath, i + 1));
|
|
360
|
-
continue;
|
|
361
|
-
}
|
|
362
|
-
const key = line.slice(0, separatorIndex).trim();
|
|
363
|
-
const value = line.slice(separatorIndex + 1).trim();
|
|
364
|
-
if (!value) {
|
|
365
|
-
let lookahead = i + 1;
|
|
366
|
-
while (lookahead < lines.length) {
|
|
367
|
-
const nextTrimmed = lines[lookahead].trim();
|
|
368
|
-
if (!nextTrimmed || nextTrimmed.startsWith("#")) {
|
|
369
|
-
lookahead += 1;
|
|
370
|
-
continue;
|
|
371
|
-
}
|
|
372
|
-
break;
|
|
373
|
-
}
|
|
374
|
-
if (lookahead < lines.length) {
|
|
375
|
-
const firstValueLine = lines[lookahead];
|
|
376
|
-
const firstValueTrimmed = firstValueLine.trim();
|
|
377
|
-
const firstValueIndent = firstValueLine.length - firstValueLine.trimStart().length;
|
|
378
|
-
if (firstValueIndent > 0 && firstValueTrimmed.startsWith("- ")) {
|
|
379
|
-
const items = [];
|
|
380
|
-
let j = lookahead;
|
|
381
|
-
while (j < lines.length) {
|
|
382
|
-
const candidateRaw = lines[j];
|
|
383
|
-
const candidateTrimmed = candidateRaw.trim();
|
|
384
|
-
if (!candidateTrimmed || candidateTrimmed.startsWith("#")) {
|
|
385
|
-
j += 1;
|
|
386
|
-
continue;
|
|
387
|
-
}
|
|
388
|
-
const indent = candidateRaw.length - candidateRaw.trimStart().length;
|
|
389
|
-
if (indent === 0) {
|
|
390
|
-
break;
|
|
391
|
-
}
|
|
392
|
-
if (!candidateTrimmed.startsWith("- ")) {
|
|
393
|
-
issues.push(makeIssue("error", "metadata", `Unsupported metadata list line '${candidateTrimmed}'. Expected '- value'.`, relativePath, j + 1));
|
|
394
|
-
j += 1;
|
|
395
|
-
continue;
|
|
396
|
-
}
|
|
397
|
-
const itemValue = candidateTrimmed.slice(2).trim().replace(/^['"]|['"]$/g, "");
|
|
398
|
-
items.push(itemValue);
|
|
399
|
-
j += 1;
|
|
400
|
-
}
|
|
401
|
-
metadata[key] = items;
|
|
402
|
-
i = j - 1;
|
|
403
|
-
continue;
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
metadata[key] = coerceMetadataValue(value);
|
|
441
|
+
if (!parsed.hasFrontmatter && required) {
|
|
442
|
+
issues.push(makeIssue("error", "metadata", "Missing metadata block at top of file.", relativePath));
|
|
408
443
|
}
|
|
409
|
-
const bodyStartLine =
|
|
410
|
-
|
|
444
|
+
const bodyStartLine = frontmatterMatch
|
|
445
|
+
? frontmatterMatch[0].split(/\r?\n/).length
|
|
446
|
+
: 1;
|
|
447
|
+
const commentsResult = parseStegoCommentsAppendix(parsed.body, relativePath, bodyStartLine);
|
|
411
448
|
issues.push(...commentsResult.issues);
|
|
412
449
|
return {
|
|
413
|
-
metadata,
|
|
450
|
+
metadata: parsed.frontmatter,
|
|
414
451
|
body: commentsResult.bodyWithoutComments,
|
|
415
452
|
comments: commentsResult.comments,
|
|
416
453
|
issues
|
|
@@ -439,32 +476,7 @@ function parseCommentIssueFromParserError(error, relativePath, bodyStartLine) {
|
|
|
439
476
|
const absoluteLine = Number.isFinite(relativeLine) ? bodyStartLine + relativeLine - 1 : undefined;
|
|
440
477
|
return makeIssue("error", "comments", lineMatch[2], relativePath, absoluteLine);
|
|
441
478
|
}
|
|
442
|
-
function
|
|
443
|
-
if (!value) {
|
|
444
|
-
return "";
|
|
445
|
-
}
|
|
446
|
-
if ((value.startsWith("\"") && value.endsWith("\"")) || (value.startsWith("'") && value.endsWith("'"))) {
|
|
447
|
-
return value.slice(1, -1);
|
|
448
|
-
}
|
|
449
|
-
if (value.startsWith("[") && value.endsWith("]")) {
|
|
450
|
-
const inner = value.slice(1, -1).trim();
|
|
451
|
-
if (!inner) {
|
|
452
|
-
return [];
|
|
453
|
-
}
|
|
454
|
-
return inner.split(",").map((entry) => entry.trim().replace(/^['\"]|['\"]$/g, ""));
|
|
455
|
-
}
|
|
456
|
-
if (/^-?\d+$/.test(value)) {
|
|
457
|
-
return Number(value);
|
|
458
|
-
}
|
|
459
|
-
if (value === "true") {
|
|
460
|
-
return true;
|
|
461
|
-
}
|
|
462
|
-
if (value === "false") {
|
|
463
|
-
return false;
|
|
464
|
-
}
|
|
465
|
-
return value;
|
|
466
|
-
}
|
|
467
|
-
function validateMarkdownBody(body, chapterPath, repoRoot) {
|
|
479
|
+
function validateMarkdownBody(body, chapterPath, repoRoot, projectRoot) {
|
|
468
480
|
const relativePath = path.relative(repoRoot, chapterPath);
|
|
469
481
|
const issues = [];
|
|
470
482
|
const lines = body.split(/\r?\n/);
|
|
@@ -499,17 +511,20 @@ function validateMarkdownBody(body, chapterPath, repoRoot) {
|
|
|
499
511
|
if (openFence) {
|
|
500
512
|
issues.push(makeIssue("error", "structure", `Unclosed code fence opened at line ${openFence.line}.`, relativePath, openFence.line));
|
|
501
513
|
}
|
|
502
|
-
issues.push(...checkLocalMarkdownLinks(body, chapterPath, repoRoot));
|
|
514
|
+
issues.push(...checkLocalMarkdownLinks(body, chapterPath, repoRoot, projectRoot));
|
|
503
515
|
issues.push(...runStyleHeuristics(body, relativePath));
|
|
504
516
|
return issues;
|
|
505
517
|
}
|
|
506
|
-
function checkLocalMarkdownLinks(body, chapterPath, repoRoot) {
|
|
518
|
+
function checkLocalMarkdownLinks(body, chapterPath, repoRoot, projectRoot) {
|
|
507
519
|
const relativePath = path.relative(repoRoot, chapterPath);
|
|
508
520
|
const issues = [];
|
|
509
|
-
const
|
|
521
|
+
const assetsDir = path.resolve(projectRoot, "assets");
|
|
522
|
+
const warnedOutsideAssets = new Set();
|
|
523
|
+
const linkRegex = /(!?)\[[^\]]*\]\(([^)]+)\)/g;
|
|
510
524
|
let match = null;
|
|
511
525
|
while ((match = linkRegex.exec(body)) !== null) {
|
|
512
|
-
|
|
526
|
+
const isImage = match[1] === "!";
|
|
527
|
+
let target = match[2].trim();
|
|
513
528
|
if (!target) {
|
|
514
529
|
continue;
|
|
515
530
|
}
|
|
@@ -520,7 +535,7 @@ function checkLocalMarkdownLinks(body, chapterPath, repoRoot) {
|
|
|
520
535
|
if (isExternalTarget(target) || target.startsWith("#")) {
|
|
521
536
|
continue;
|
|
522
537
|
}
|
|
523
|
-
const cleanTarget = target.split("#")[0];
|
|
538
|
+
const cleanTarget = target.split("#")[0].split("?")[0];
|
|
524
539
|
if (!cleanTarget) {
|
|
525
540
|
continue;
|
|
526
541
|
}
|
|
@@ -528,15 +543,35 @@ function checkLocalMarkdownLinks(body, chapterPath, repoRoot) {
|
|
|
528
543
|
if (!fs.existsSync(resolved)) {
|
|
529
544
|
issues.push(makeIssue("warning", "links", `Broken local link/image target '${cleanTarget}'.`, relativePath));
|
|
530
545
|
}
|
|
546
|
+
if (!isImage) {
|
|
547
|
+
continue;
|
|
548
|
+
}
|
|
549
|
+
if (isPathInside(resolved, assetsDir)) {
|
|
550
|
+
continue;
|
|
551
|
+
}
|
|
552
|
+
const warningKey = `${relativePath}|${cleanTarget}`;
|
|
553
|
+
if (warnedOutsideAssets.has(warningKey)) {
|
|
554
|
+
continue;
|
|
555
|
+
}
|
|
556
|
+
warnedOutsideAssets.add(warningKey);
|
|
557
|
+
issues.push(makeIssue("warning", "assets", `Local image target '${cleanTarget}' is outside project assets/. Store manuscript images under 'assets/'.`, relativePath));
|
|
531
558
|
}
|
|
532
559
|
return issues;
|
|
533
560
|
}
|
|
534
561
|
function isExternalTarget(target) {
|
|
535
562
|
return (target.startsWith("http://")
|
|
536
563
|
|| target.startsWith("https://")
|
|
564
|
+
|| target.startsWith("data:")
|
|
537
565
|
|| target.startsWith("mailto:")
|
|
538
566
|
|| target.startsWith("tel:"));
|
|
539
567
|
}
|
|
568
|
+
function isPathInside(candidatePath, parentPath) {
|
|
569
|
+
const relative = path.relative(path.resolve(parentPath), path.resolve(candidatePath));
|
|
570
|
+
if (!relative) {
|
|
571
|
+
return true;
|
|
572
|
+
}
|
|
573
|
+
return !relative.startsWith("..") && !path.isAbsolute(relative);
|
|
574
|
+
}
|
|
540
575
|
function runStyleHeuristics(body, relativePath) {
|
|
541
576
|
const issues = [];
|
|
542
577
|
const prose = body
|
|
@@ -7,9 +7,8 @@ export function registerCheckStageCommand(registry) {
|
|
|
7
7
|
registry.register({
|
|
8
8
|
name: "check-stage",
|
|
9
9
|
description: "Run stage-specific quality gates",
|
|
10
|
-
allowUnknownOptions: true,
|
|
11
10
|
options: [
|
|
12
|
-
{ flags: "--project <project-id>", description: "Project id" },
|
|
11
|
+
{ flags: "-p, --project <project-id>", description: "Project id" },
|
|
13
12
|
{ flags: "--stage <stage>", description: "draft|revise|line-edit|proof|final" },
|
|
14
13
|
{ flags: "--file <path>", description: "Project-relative manuscript path" },
|
|
15
14
|
{ flags: "--root <path>", description: "Workspace root path" }
|
|
@@ -7,9 +7,8 @@ export function registerLintCommand(registry) {
|
|
|
7
7
|
registry.register({
|
|
8
8
|
name: "lint",
|
|
9
9
|
description: "Run markdown and spelling checks",
|
|
10
|
-
allowUnknownOptions: true,
|
|
11
10
|
options: [
|
|
12
|
-
{ flags: "--project <project-id>", description: "Project id" },
|
|
11
|
+
{ flags: "-p, --project <project-id>", description: "Project id" },
|
|
13
12
|
{ flags: "--manuscript", description: "Lint manuscript files only" },
|
|
14
13
|
{ flags: "--spine", description: "Lint spine/notes files only" },
|
|
15
14
|
{ flags: "--root <path>", description: "Workspace root path" }
|
|
@@ -6,9 +6,8 @@ export function registerValidateCommand(registry) {
|
|
|
6
6
|
registry.register({
|
|
7
7
|
name: "validate",
|
|
8
8
|
description: "Validate manuscript and project state",
|
|
9
|
-
allowUnknownOptions: true,
|
|
10
9
|
options: [
|
|
11
|
-
{ flags: "--project <project-id>", description: "Project id" },
|
|
10
|
+
{ flags: "-p, --project <project-id>", description: "Project id" },
|
|
12
11
|
{ flags: "--file <path>", description: "Project-relative manuscript path" },
|
|
13
12
|
{ flags: "--root <path>", description: "Workspace root path" }
|
|
14
13
|
],
|
|
@@ -24,8 +24,8 @@ export function registerInitCommand(registry) {
|
|
|
24
24
|
writeText("Next steps:");
|
|
25
25
|
writeText(" npm install");
|
|
26
26
|
writeText(" stego list-projects");
|
|
27
|
-
writeText(" stego validate
|
|
28
|
-
writeText(" stego build
|
|
27
|
+
writeText(" stego validate -p fiction-example");
|
|
28
|
+
writeText(" stego build -p fiction-example");
|
|
29
29
|
}
|
|
30
30
|
});
|
|
31
31
|
}
|
|
@@ -17,6 +17,7 @@ This directory is a Stego writing workspace (a monorepo for one or more writing
|
|
|
17
17
|
|
|
18
18
|
- \`stego.config.json\` workspace configuration
|
|
19
19
|
- \`projects/\` demo projects (\`stego-docs\` and \`fiction-example\`)
|
|
20
|
+
- per-project \`assets/\` directories for manuscript images
|
|
20
21
|
- root \`package.json\` scripts for Stego commands
|
|
21
22
|
- root \`.vscode/tasks.json\` tasks for common workflows
|
|
22
23
|
|
|
@@ -32,11 +33,11 @@ stego list-projects
|
|
|
32
33
|
## Run commands for a specific project (from workspace root)
|
|
33
34
|
|
|
34
35
|
\`\`\`bash
|
|
35
|
-
stego validate
|
|
36
|
-
stego build
|
|
37
|
-
stego check-stage
|
|
38
|
-
stego export
|
|
39
|
-
stego new
|
|
36
|
+
stego validate -p fiction-example
|
|
37
|
+
stego build -p fiction-example
|
|
38
|
+
stego check-stage -p fiction-example --stage revise
|
|
39
|
+
stego export -p fiction-example --format md
|
|
40
|
+
stego new -p fiction-example
|
|
40
41
|
\`\`\`
|
|
41
42
|
|
|
42
43
|
## Work inside one project
|
|
@@ -58,13 +59,13 @@ This keeps your editor context focused and applies the project's recommended ext
|
|
|
58
59
|
## Create a new project
|
|
59
60
|
|
|
60
61
|
\`\`\`bash
|
|
61
|
-
stego new-project
|
|
62
|
+
stego new-project -p my-book --title "My Book"
|
|
62
63
|
\`\`\`
|
|
63
64
|
|
|
64
65
|
## Add a new manuscript file
|
|
65
66
|
|
|
66
67
|
\`\`\`bash
|
|
67
|
-
stego new
|
|
68
|
+
stego new -p fiction-example
|
|
68
69
|
\`\`\`
|
|
69
70
|
`;
|
|
70
71
|
export const SCAFFOLD_AGENTS_CONTENT = `# AGENTS.md
|
|
@@ -90,7 +91,7 @@ This workspace is designed to be AI-friendly for writing workflows.
|
|
|
90
91
|
|
|
91
92
|
1. Confirm workspace root contains \`stego.config.json\`.
|
|
92
93
|
2. Run \`stego list-projects\`.
|
|
93
|
-
3. Use explicit \`--project <id>\` for project-scoped commands.
|
|
94
|
+
3. Use explicit \`--project/-p <id>\` for project-scoped commands.
|
|
94
95
|
|
|
95
96
|
## CLI-First Policy (Required)
|
|
96
97
|
|
|
@@ -123,7 +124,7 @@ Preferred commands include:
|
|
|
123
124
|
|
|
124
125
|
1. Read current state first (\`metadata read\`, \`spine read\`, \`comments read\`).
|
|
125
126
|
2. Mutate via CLI commands.
|
|
126
|
-
3. Verify after writes (\`stego validate --project <id>\` and relevant read commands).
|
|
127
|
+
3. Verify after writes (\`stego validate --project/-p <id>\` and relevant read commands).
|
|
127
128
|
|
|
128
129
|
## Manual Edit Fallback
|
|
129
130
|
|
|
@@ -146,7 +147,7 @@ When CLI fails:
|
|
|
146
147
|
|
|
147
148
|
## Validation Expectations
|
|
148
149
|
|
|
149
|
-
After mutations, run relevant checks when feasible (for example \`stego validate --project <id>\`) and report results.
|
|
150
|
+
After mutations, run relevant checks when feasible (for example \`stego validate --project/-p <id>\`) and report results.
|
|
150
151
|
|
|
151
152
|
## Scope Guardrails
|
|
152
153
|
|
|
@@ -155,10 +156,10 @@ After mutations, run relevant checks when feasible (for example \`stego validate
|
|
|
155
156
|
|
|
156
157
|
## Task To Command Quick Map
|
|
157
158
|
|
|
158
|
-
- New manuscript: \`stego new --project <id> [--filename <name>]\`
|
|
159
|
-
- Read spine: \`stego spine read --project <id> --format json\`
|
|
160
|
-
- New spine category: \`stego spine new-category --project <id> --key <category>\`
|
|
161
|
-
- New spine entry: \`stego spine new --project <id> --category <category> [--filename <path>]\`
|
|
159
|
+
- New manuscript: \`stego new --project/-p <id> [--filename <name>]\`
|
|
160
|
+
- Read spine: \`stego spine read --project/-p <id> --format json\`
|
|
161
|
+
- New spine category: \`stego spine new-category --project/-p <id> --key <category>\`
|
|
162
|
+
- New spine entry: \`stego spine new --project/-p <id> --category <category> [--filename <path>]\`
|
|
162
163
|
- Read metadata: \`stego metadata read <markdown-path> --format json\`
|
|
163
164
|
- Apply metadata: \`stego metadata apply <markdown-path> --input <path|-> --format json\`
|
|
164
165
|
- Read comments: \`stego comments read <manuscript> --format json\`
|
|
@@ -4,7 +4,7 @@ export function registerSpineDeprecatedAliases(registry) {
|
|
|
4
4
|
description: "Deprecated alias for spine new-category",
|
|
5
5
|
allowUnknownOptions: true,
|
|
6
6
|
options: [
|
|
7
|
-
{ flags: "--project <project-id>", description: "Project id" },
|
|
7
|
+
{ flags: "-p, --project <project-id>", description: "Project id" },
|
|
8
8
|
{ flags: "--key <category>", description: "Category key" },
|
|
9
9
|
{ flags: "--label <label>", description: "Category display label" },
|
|
10
10
|
{ flags: "--require-metadata", description: "Append key to required metadata" },
|
|
@@ -20,7 +20,7 @@ export function registerSpineDeprecatedAliases(registry) {
|
|
|
20
20
|
description: "Deprecated alias for spine new",
|
|
21
21
|
allowUnknownOptions: true,
|
|
22
22
|
options: [
|
|
23
|
-
{ flags: "--project <project-id>", description: "Project id" },
|
|
23
|
+
{ flags: "-p, --project <project-id>", description: "Project id" },
|
|
24
24
|
{ flags: "--category <category>", description: "Category key" },
|
|
25
25
|
{ flags: "--filename <path>", description: "Relative entry path" },
|
|
26
26
|
{ flags: "--format <format>", description: "text|json" },
|
|
@@ -8,9 +8,8 @@ export function registerSpineNewCategoryCommand(registry) {
|
|
|
8
8
|
registry.register({
|
|
9
9
|
name: "spine new-category",
|
|
10
10
|
description: "Create a new spine category",
|
|
11
|
-
allowUnknownOptions: true,
|
|
12
11
|
options: [
|
|
13
|
-
{ flags: "--project <project-id>", description: "Project id" },
|
|
12
|
+
{ flags: "-p, --project <project-id>", description: "Project id" },
|
|
14
13
|
{ flags: "--key <category>", description: "Category key" },
|
|
15
14
|
{ flags: "--label <label>", description: "Category display label" },
|
|
16
15
|
{ flags: "--require-metadata", description: "Append key to required metadata" },
|
|
@@ -8,9 +8,8 @@ export function registerSpineNewEntryCommand(registry) {
|
|
|
8
8
|
registry.register({
|
|
9
9
|
name: "spine new",
|
|
10
10
|
description: "Create a new spine entry",
|
|
11
|
-
allowUnknownOptions: true,
|
|
12
11
|
options: [
|
|
13
|
-
{ flags: "--project <project-id>", description: "Project id" },
|
|
12
|
+
{ flags: "-p, --project <project-id>", description: "Project id" },
|
|
14
13
|
{ flags: "--category <category>", description: "Category key" },
|
|
15
14
|
{ flags: "--filename <path>", description: "Relative entry path" },
|
|
16
15
|
{ flags: "--format <format>", description: "text|json" },
|
|
@@ -7,9 +7,8 @@ export function registerSpineReadCommand(registry) {
|
|
|
7
7
|
registry.register({
|
|
8
8
|
name: "spine read",
|
|
9
9
|
description: "Read spine catalog",
|
|
10
|
-
allowUnknownOptions: true,
|
|
11
10
|
options: [
|
|
12
|
-
{ flags: "--project <project-id>", description: "Project id" },
|
|
11
|
+
{ flags: "-p, --project <project-id>", description: "Project id" },
|
|
13
12
|
{ flags: "--format <format>", description: "text|json" },
|
|
14
13
|
{ flags: "--root <path>", description: "Workspace root path" }
|
|
15
14
|
],
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
img[data-layout="inline"] {
|
|
2
|
+
display: inline;
|
|
3
|
+
vertical-align: baseline;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
img[data-layout="block"] {
|
|
7
|
+
display: block;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
img[data-layout="block"][data-align="left"] {
|
|
11
|
+
margin-left: 0;
|
|
12
|
+
margin-right: auto;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
img[data-layout="block"][data-align="center"] {
|
|
16
|
+
margin-left: auto;
|
|
17
|
+
margin-right: auto;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
img[data-layout="block"][data-align="right"] {
|
|
21
|
+
margin-left: auto;
|
|
22
|
+
margin-right: 0;
|
|
23
|
+
}
|