@easysociable/cli 0.1.1 → 0.1.3
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 +29 -18
- package/content-engine/fixtures/golden-run-en/draft.md +13 -0
- package/content-engine/fixtures/golden-run-en/idea.md +26 -0
- package/content-engine/fixtures/golden-run-en/pack-tiktok.json +11 -0
- package/content-engine/scripts/contracts.mjs +183 -0
- package/content-engine/scripts/export-contracts.mjs +76 -0
- package/content-engine/scripts/fixture.test.mjs +24 -0
- package/content-engine/scripts/starter.mjs +55 -0
- package/content-engine/scripts/validate-export.mjs +42 -0
- package/content-engine/scripts/validate-pack.mjs +48 -0
- package/content-engine/scripts/validate-run.mjs +100 -0
- package/content-engine/scripts/validate.test.mjs +101 -0
- package/content-engine/scripts/workbench.mjs +135 -0
- package/content-engine/starter/CLAUDE.md +756 -0
- package/content-engine/starter/accounts/README.md +9 -0
- package/content-engine/starter/accounts/_index.md +31 -0
- package/content-engine/starter/accounts/overrides/README.md +3 -0
- package/content-engine/starter/discovery/README.md +18 -0
- package/content-engine/starter/discovery/_index.md +27 -0
- package/content-engine/starter/discovery/_template.md +41 -0
- package/content-engine/starter/engine.json +19 -0
- package/content-engine/starter/exports/README.md +28 -0
- package/content-engine/starter/inbox/README.md +9 -0
- package/content-engine/starter/inbox/_index.md +24 -0
- package/content-engine/starter/inbox/entries/README.md +3 -0
- package/content-engine/starter/platforms/README.md +19 -0
- package/content-engine/starter/platforms/ig.md +69 -0
- package/content-engine/starter/platforms/instagram.md +5 -0
- package/content-engine/starter/platforms/linkedin.md +72 -0
- package/content-engine/starter/platforms/threads.md +69 -0
- package/content-engine/starter/platforms/tiktok.md +71 -0
- package/content-engine/starter/platforms/x.md +78 -0
- package/content-engine/starter/platforms/xiaohongshu.md +19 -0
- package/content-engine/starter/profiles/_index.md +13 -0
- package/content-engine/starter/profiles/default/README.md +11 -0
- package/content-engine/starter/profiles/default/audience.md +38 -0
- package/content-engine/starter/profiles/default/pillars.md +33 -0
- package/content-engine/starter/profiles/default/voice.md +24 -0
- package/content-engine/starter/published/_index.md +20 -0
- package/content-engine/starter/raw/README.md +4 -0
- package/content-engine/starter/raw/_index.md +12 -0
- package/content-engine/starter/runs/README.md +22 -0
- package/content-engine/starter/runs/_index.md +41 -0
- package/content-engine/starter/runs/_template/brief.md +66 -0
- package/content-engine/starter/runs/_template/draft.md +11 -0
- package/content-engine/starter/runs/_template/editor.md +18 -0
- package/content-engine/starter/runs/_template/feedback.md +15 -0
- package/content-engine/starter/runs/_template/idea.md +27 -0
- package/content-engine/starter/runs/_template/pack.md +47 -0
- package/content-engine/starter/runs/_template/packet.md +25 -0
- package/content-engine/starter/runs/_template/rubric.md +19 -0
- package/content-engine/starter/swipe/_index.md +27 -0
- package/content-engine/starter/swipe/_template.md +34 -0
- package/content-engine/starter/topics/README.md +25 -0
- package/content-engine/starter/topics/WATCHLIST.md +65 -0
- package/content-engine/starter/topics/_index.md +44 -0
- package/content-engine/starter/topics/_template-item.md +90 -0
- package/content-engine/starter/topics/items/README.md +4 -0
- package/content-engine/starter/topics/scoring.md +201 -0
- package/content-engine/starter/topics/watchlists/README.md +10 -0
- package/content-engine/starter/topics/watchlists/_shared.md +14 -0
- package/content-engine/starter/topics/watchlists/default.md +6 -0
- package/content-engine/starter/wiki/README.md +13 -0
- package/content-engine/starter/wiki/atoms/_index.md +23 -0
- package/content-engine/starter/wiki/atoms/_template.md +31 -0
- package/content-engine/starter/wiki/claims/_index.md +26 -0
- package/content-engine/starter/wiki/claims/_template.md +38 -0
- package/content-engine/starter/workbench/README.md +17 -0
- package/content-engine/workbench/build.py +1074 -0
- package/content-engine/workbench/serve.py +53 -0
- package/content-engine/workbench/template.html +2119 -0
- package/dist/easysociable.js +804 -2903
- package/dist/easysociable.js.map +1 -1
- package/package.json +19 -13
- package/skills/ONBOARDING.md +20 -14
- package/skills/easysociable-content/SKILL.md +38 -102
- package/skills/easysociable-slideshow/SKILL.md +24 -16
- package/skills/easysociable-slideshow/references/VAULT.md +13 -11
- package/skills/easysociable-template/SKILL.md +26 -24
- package/skills/easysociable-template/references/CATALOG_METADATA.md +2 -2
- package/skills/easysociable-template/references/DESIGN_V2_1.md +53 -2
- package/skills/easysociable-template/references/GATEWAY.md +7 -2
- package/skills/easysociable-template/references/PLATFORM_SPECS.md +11 -3
- package/skills/easysociable-template/references/THEME_TOKENS.md +1 -1
- package/skills/easysociable-template/references/fixtures/image-reference.design-plan.json +74 -0
- package/skills/easysociable-template/references/fixtures/image-reference.visual-qa.json +28 -0
- package/skills/easysociable-template/scripts/generate-theme-from-hex.mjs +0 -0
- package/skills/easysociable-template/scripts/validate-design-v2.mjs +97 -0
- package/skills/easysociable-template/workflows/ASSET_PLAN.md +2 -1
- package/skills/easysociable-template/workflows/AUTHORING_FROM_BRIEF.md +4 -4
- package/skills/easysociable-template/workflows/AUTHORING_FROM_IMAGE.md +66 -16
- package/skills/easysociable-template/workflows/AUTHORING_STUDIO.md +28 -15
- package/skills/easysociable-template/workflows/DESIGN_BRIEF.md +4 -3
- package/skills/easysociable-template/workflows/MULTI_PLATFORM_FAMILY.md +81 -0
- package/skills/easysociable-template/workflows/PUBLISH_FROM_DRAFT.md +1 -1
- package/skills/easysociable-template/workflows/VISUAL_QA.md +10 -0
- package/skills/easysociable-content/platforms/claude.md +0 -5
- package/skills/easysociable-content/platforms/codex.md +0 -5
- package/skills/easysociable-content/platforms/hermes.md +0 -5
- package/skills/easysociable-content/platforms/openclaw.md +0 -5
- package/skills/easysociable-content/references/CLI.md +0 -27
- package/skills/easysociable-content/references/MCP.md +0 -20
- package/skills/easysociable-content/references/PAGE_PACK.md +0 -6
- package/skills/easysociable-content/references/TROUBLESHOOTING.md +0 -11
- package/skills/easysociable-content/references/VAULT.md +0 -20
- package/skills/easysociable-content/scripts/setup.mjs +0 -156
- package/skills/easysociable-content/scripts/setup.test.mjs +0 -71
- package/skills/easysociable-content/scripts/starter.mjs +0 -53
- package/skills/easysociable-content/workflows/ANALYSIS_AND_COVERAGE.md +0 -8
- package/skills/easysociable-content/workflows/CONTENT_EXPORT.md +0 -19
- package/skills/easysociable-content/workflows/DEVICE_TASKS.md +0 -5
- package/skills/easysociable-content/workflows/EXPERIMENTS.md +0 -6
- package/skills/easysociable-content/workflows/MCP_FIRST.md +0 -10
- package/skills/easysociable-content/workflows/PUBLICATION.md +0 -5
- package/skills/easysociable-content/workflows/TOPIC_DRAFT.md +0 -5
- package/skills/easysociable-content/workflows/WORKING_DRAFT.md +0 -10
package/README.md
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# EasySociable CLI
|
|
2
2
|
|
|
3
|
-
Agent-first CLI
|
|
4
|
-
Gateway-only: no Local API, SQLite, or local renderer.
|
|
3
|
+
Agent-first CLI for EasySociable: local Content Engine, Template, Slideshow, and MCP.
|
|
5
4
|
|
|
6
5
|
```bash
|
|
7
6
|
npm i -g @easysociable/cli
|
|
@@ -9,25 +8,41 @@ npm i -g @easysociable/cli
|
|
|
9
8
|
curl -fsSL https://easysociable.com/install.sh | bash
|
|
10
9
|
```
|
|
11
10
|
|
|
12
|
-
Requires Node.js 20+.
|
|
11
|
+
Requires Node.js 20+. Content Engine workbench also needs `python3`.
|
|
13
12
|
|
|
14
13
|
## Quick start
|
|
15
14
|
|
|
16
15
|
```bash
|
|
16
|
+
# Visual skills + MCP for Codex / Claude Code
|
|
17
17
|
easysociable install --yes
|
|
18
|
-
|
|
19
|
-
#
|
|
18
|
+
|
|
19
|
+
# Local content engine (markdown + workbench) — no Agent host required
|
|
20
|
+
easysociable engine init --root /workspace --yes
|
|
21
|
+
easysociable engine serve --engine /workspace/Content Engine --no-open
|
|
20
22
|
```
|
|
21
23
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
+
Then tell the Agent to follow `Content Engine/CLAUDE.md`.
|
|
25
|
+
|
|
26
|
+
Optional pointer skill for hosts that discover work via SKILL.md:
|
|
24
27
|
|
|
25
28
|
```bash
|
|
26
|
-
easysociable install --skills content --yes
|
|
29
|
+
easysociable install --skills content,template,slideshow --yes
|
|
27
30
|
```
|
|
28
31
|
|
|
29
|
-
Lazy-auth guidance
|
|
30
|
-
|
|
32
|
+
Lazy-auth guidance lives in the package at `skills/ONBOARDING.md`.
|
|
33
|
+
|
|
34
|
+
## Content Engine
|
|
35
|
+
|
|
36
|
+
| Command | Purpose |
|
|
37
|
+
|---------|---------|
|
|
38
|
+
| `easysociable engine init [--root] [--yes] [--serve]` | Scaffold starter (never overwrites) |
|
|
39
|
+
| `easysociable engine serve [--engine] [--no-open]` | Build + start workbench on `:8765` |
|
|
40
|
+
| `easysociable engine build [--engine]` | Rebuild `workbench/index.html` |
|
|
41
|
+
| `easysociable engine detect` | Find engines under search roots |
|
|
42
|
+
| `easysociable engine validate --root …` | Starter completeness |
|
|
43
|
+
| `easysociable engine validate --run …` | Run / pack checks before render |
|
|
44
|
+
|
|
45
|
+
Default `--root` / `--engine` parent is `/workspace` when that directory exists, otherwise the current working directory.
|
|
31
46
|
|
|
32
47
|
## Authentication
|
|
33
48
|
|
|
@@ -38,23 +53,19 @@ easysociable templates list --json
|
|
|
38
53
|
easysociable mcp serve --transport stdio
|
|
39
54
|
```
|
|
40
55
|
|
|
41
|
-
|
|
42
|
-
versioned JSON envelope.
|
|
43
|
-
|
|
44
|
-
## Common commands
|
|
56
|
+
## Other commands
|
|
45
57
|
|
|
46
58
|
| Command | Purpose |
|
|
47
59
|
|---------|---------|
|
|
48
60
|
| `easysociable install [--yes] [--skills list]` | Install Skills + MCP |
|
|
49
61
|
| `easysociable update [--yes] [--skills list]` | Update Skills + MCP |
|
|
50
|
-
| `easysociable auth login --api-key <key>` | Store Gateway credentials |
|
|
51
62
|
| `easysociable templates …` | Page Template drafts / catalog |
|
|
52
|
-
| `easysociable
|
|
63
|
+
| `easysociable brands …` | Workspace Brands |
|
|
64
|
+
| `easysociable slideshows …` | Slideshow Runs |
|
|
53
65
|
| `easysociable renders …` | Single-page final render jobs |
|
|
54
66
|
| `easysociable mcp serve --transport stdio` | Unified MCP server |
|
|
55
67
|
|
|
56
|
-
`--skills` accepts
|
|
57
|
-
`slideshow` (default: `template,slideshow`).
|
|
68
|
+
`--skills` accepts: `content`, `template`, `slideshow` (default: `template,slideshow`).
|
|
58
69
|
|
|
59
70
|
## License
|
|
60
71
|
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Draft
|
|
2
|
+
|
|
3
|
+
## Page 1 (hook)
|
|
4
|
+
Stop scrolling — this changed my mornings.
|
|
5
|
+
|
|
6
|
+
## Page 2 (value)
|
|
7
|
+
Wake at the same time, even on weekends.
|
|
8
|
+
|
|
9
|
+
## Page 3 (value)
|
|
10
|
+
No phone for the first 20 minutes after waking.
|
|
11
|
+
|
|
12
|
+
## Page 4 (cta)
|
|
13
|
+
Save this and try it tomorrow.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
---
|
|
2
|
+
run_id: RUN-20260823-morning-routine-tips
|
|
3
|
+
status: pack-ready
|
|
4
|
+
created: 2026-08-23
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Idea
|
|
8
|
+
|
|
9
|
+
- **one_liner:** Three morning habits that calm the start of the day
|
|
10
|
+
- **account:** your-handle
|
|
11
|
+
- **profile:** default
|
|
12
|
+
- **pillar:** P1
|
|
13
|
+
- **primary_platform:** tiktok
|
|
14
|
+
- **platforms:** tiktok
|
|
15
|
+
- **topic_id:** none
|
|
16
|
+
- **capture_ids:** none
|
|
17
|
+
- **swipe_id:** none
|
|
18
|
+
- **atoms:** none
|
|
19
|
+
- **claim_id:** none
|
|
20
|
+
- **hypothesis:** none
|
|
21
|
+
|
|
22
|
+
## Platform (this run)
|
|
23
|
+
|
|
24
|
+
| platform | copy | images (pack) | publish status |
|
|
25
|
+
|----------|------|---------------|----------------|
|
|
26
|
+
| tiktok | yes | yes | pending |
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": "content-pack.v1",
|
|
3
|
+
"platform": "tiktok",
|
|
4
|
+
"title": "Three morning habits",
|
|
5
|
+
"pages": [
|
|
6
|
+
{ "role": "hook", "pageTemplateId": "tpl_hook_001", "slots": { "text_0": "Stop scrolling — this changed my mornings" } },
|
|
7
|
+
{ "role": "value", "pageTemplateId": "tpl_value_001", "slots": { "text_0": "Wake at the same time", "text_1": "Even on weekends" } },
|
|
8
|
+
{ "role": "value", "pageTemplateId": "tpl_value_001", "slots": { "text_0": "No phone for 20 minutes", "text_1": "Before email or feeds" } },
|
|
9
|
+
{ "role": "cta", "pageTemplateId": "tpl_cta_001", "slots": { "text_0": "Save this for tomorrow" } }
|
|
10
|
+
]
|
|
11
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/** ponytail: mirrors @easysociable/template-public-contracts — upgrade by diffing that package */
|
|
2
|
+
|
|
3
|
+
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs'
|
|
4
|
+
import { join, basename } from 'node:path'
|
|
5
|
+
|
|
6
|
+
export const PAGE_ROLES = [
|
|
7
|
+
'cover', 'hook', 'setup', 'context', 'value', 'point', 'checklist', 'proof',
|
|
8
|
+
'evidence', 'compare', 'screenshot', 'metric', 'quote', 'steps', 'cta', 'follow', 'recap',
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
export const PACK_PLATFORMS = ['tiktok', 'xiaohongshu', 'instagram', 'linkedin', 'threads']
|
|
12
|
+
|
|
13
|
+
export const SLIDESHOW_PLATFORMS = ['tiktok', 'instagram', 'linkedin', 'threads']
|
|
14
|
+
|
|
15
|
+
export const PLATFORM_CAPS = {
|
|
16
|
+
tiktok: { hardMin: 4, hardMax: 35 },
|
|
17
|
+
xiaohongshu: { hardMin: 2, hardMax: 18 },
|
|
18
|
+
instagram: { hardMin: 2, hardMax: 20 },
|
|
19
|
+
linkedin: { hardMin: 2, hardMax: 300 },
|
|
20
|
+
threads: { hardMin: 2, hardMax: 20 },
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const FORBIDDEN_PACK_KEYS = ['deck_template_id', 'deckTemplateId']
|
|
24
|
+
|
|
25
|
+
const PACK_KEYS = new Set(['schemaVersion', 'platform', 'title', 'pages'])
|
|
26
|
+
const PAGE_KEYS = new Set(['pageTemplateId', 'role', 'slots'])
|
|
27
|
+
export const RUN_FILE_STATUSES = [
|
|
28
|
+
'RUN_OPEN', 'BRIEF_WAIT', 'DRAFTED', 'EDIT_WAIT', 'SCORED',
|
|
29
|
+
'READY_TO_POST', 'PUBLISHED', 'HOLD', 'KILLED', 'SUPERSEDED',
|
|
30
|
+
]
|
|
31
|
+
export const RUN_INDEX_STATUSES = [
|
|
32
|
+
'open', 'brief', 'draft', 'scored', 'ready', 'published', 'hold', 'killed', 'superseded', 'partial',
|
|
33
|
+
]
|
|
34
|
+
const RUN_STATUSES = [
|
|
35
|
+
...RUN_FILE_STATUSES,
|
|
36
|
+
...RUN_INDEX_STATUSES,
|
|
37
|
+
'template-ready', 'pack-ready', 'handed-off', 'exported',
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
function isRecord(value) {
|
|
41
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function pushForbidden(errors, value, path) {
|
|
45
|
+
const text = JSON.stringify(value).toLowerCase()
|
|
46
|
+
for (const key of FORBIDDEN_PACK_KEYS) {
|
|
47
|
+
if (text.includes(key.toLowerCase())) errors.push(`${path}: forbidden field ${key}`)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function pushUnknownKeys(errors, value, allowed, path) {
|
|
52
|
+
for (const key of Object.keys(value)) {
|
|
53
|
+
if (!allowed.has(key)) errors.push(`${path}: unknown field ${key}`)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function isPublicSlotValue(value) {
|
|
58
|
+
return value === null || typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean'
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function validateContentPack(pack, options = {}) {
|
|
62
|
+
const { platform: expectedPlatform } = options
|
|
63
|
+
const errors = []
|
|
64
|
+
const warnings = []
|
|
65
|
+
|
|
66
|
+
if (!isRecord(pack)) {
|
|
67
|
+
errors.push('pack: must be a JSON object')
|
|
68
|
+
return { valid: false, errors, warnings }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
pushForbidden(errors, pack, 'pack')
|
|
72
|
+
pushUnknownKeys(errors, pack, PACK_KEYS, 'pack')
|
|
73
|
+
|
|
74
|
+
if (pack.schemaVersion !== 'content-pack.v1') errors.push('schemaVersion: must be content-pack.v1')
|
|
75
|
+
if (!PACK_PLATFORMS.includes(pack.platform)) {
|
|
76
|
+
errors.push(`platform: must be one of ${PACK_PLATFORMS.join(', ')}`)
|
|
77
|
+
}
|
|
78
|
+
if (expectedPlatform && pack.platform !== expectedPlatform) {
|
|
79
|
+
errors.push(`platform: expected ${expectedPlatform}, got ${pack.platform}`)
|
|
80
|
+
}
|
|
81
|
+
if (pack.title !== undefined && typeof pack.title !== 'string') errors.push('title: must be a string')
|
|
82
|
+
if (!Array.isArray(pack.pages) || pack.pages.length === 0) {
|
|
83
|
+
errors.push('pages: must be a non-empty array')
|
|
84
|
+
return { valid: errors.length === 0, errors, warnings }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const cap = PLATFORM_CAPS[pack.platform]
|
|
88
|
+
if (cap && (pack.pages.length < cap.hardMin || pack.pages.length > cap.hardMax)) {
|
|
89
|
+
errors.push(`pages: ${pack.platform} requires ${cap.hardMin}-${cap.hardMax} pages, got ${pack.pages.length}`)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
pack.pages.forEach((page, index) => {
|
|
93
|
+
const label = `pages[${index}]`
|
|
94
|
+
if (!isRecord(page)) {
|
|
95
|
+
errors.push(`${label}: must be an object`)
|
|
96
|
+
return
|
|
97
|
+
}
|
|
98
|
+
pushUnknownKeys(errors, page, PAGE_KEYS, label)
|
|
99
|
+
if (!PAGE_ROLES.includes(page.role)) errors.push(`${label}.role: invalid page role`)
|
|
100
|
+
if (typeof page.pageTemplateId !== 'string' || page.pageTemplateId.length === 0) {
|
|
101
|
+
errors.push(`${label}.pageTemplateId: required and must be a non-empty string`)
|
|
102
|
+
}
|
|
103
|
+
if (!isRecord(page.slots)) {
|
|
104
|
+
errors.push(`${label}.slots: must be an object`)
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
for (const [key, value] of Object.entries(page.slots)) {
|
|
108
|
+
if (!isPublicSlotValue(value)) errors.push(`${label}.slots.${key}: must be string, number, boolean or null`)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
if (!SLIDESHOW_PLATFORMS.includes(pack.platform)) {
|
|
114
|
+
warnings.push(`platform ${pack.platform}: not supported by easysociable-slideshow in V1`)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return { valid: errors.length === 0, errors, warnings }
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function parseFrontmatter(text) {
|
|
121
|
+
const match = text.match(/^---\r?\n([\s\S]*?)\r?\n---/)
|
|
122
|
+
if (!match) return {}
|
|
123
|
+
const fields = {}
|
|
124
|
+
for (const line of match[1].split('\n')) {
|
|
125
|
+
const idx = line.indexOf(':')
|
|
126
|
+
if (idx === -1) continue
|
|
127
|
+
const key = line.slice(0, idx).trim()
|
|
128
|
+
const value = line.slice(idx + 1).trim().replace(/^['"]|['"]$/g, '')
|
|
129
|
+
if (key) fields[key] = value
|
|
130
|
+
}
|
|
131
|
+
return fields
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function ideaField(text, name) {
|
|
135
|
+
const match = text.match(new RegExp(`\\*\\*${name}:\\*\\*\\s*(.+)`, 'i'))
|
|
136
|
+
return match ? match[1].trim() : ''
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function validateRunLayout(runDir, options = {}) {
|
|
140
|
+
const { requirePack = false } = options
|
|
141
|
+
const errors = []
|
|
142
|
+
const warnings = []
|
|
143
|
+
const files = { ideaMd: false, draftMd: false, packMd: false, packs: [] }
|
|
144
|
+
const ideaPath = join(runDir, 'idea.md')
|
|
145
|
+
|
|
146
|
+
if (!existsSync(runDir) || !statSync(runDir).isDirectory()) {
|
|
147
|
+
errors.push('run: directory required')
|
|
148
|
+
return { valid: false, errors, warnings, files, frontmatter: {} }
|
|
149
|
+
}
|
|
150
|
+
if (!existsSync(ideaPath)) {
|
|
151
|
+
errors.push('idea.md: required')
|
|
152
|
+
return { valid: false, errors, warnings, files, frontmatter: {} }
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
files.ideaMd = true
|
|
156
|
+
const ideaText = readFileSync(ideaPath, 'utf8')
|
|
157
|
+
const frontmatter = parseFrontmatter(ideaText)
|
|
158
|
+
const platform = (frontmatter.platform || ideaField(ideaText, 'primary_platform') || ideaField(ideaText, 'platforms')).replace(/\s.*/, '')
|
|
159
|
+
frontmatter.platform = platform
|
|
160
|
+
frontmatter.slug = frontmatter.slug || basename(runDir)
|
|
161
|
+
if (!frontmatter.run_id) errors.push('idea.md frontmatter: missing run_id')
|
|
162
|
+
if (!frontmatter.status) errors.push('idea.md frontmatter: missing status')
|
|
163
|
+
if (frontmatter.status && !RUN_STATUSES.includes(frontmatter.status)) {
|
|
164
|
+
errors.push(`idea.md status: must be one of the engine run statuses`)
|
|
165
|
+
}
|
|
166
|
+
if (!platform) warnings.push('idea.md: primary_platform recommended')
|
|
167
|
+
|
|
168
|
+
files.draftMd = existsSync(join(runDir, 'draft.md'))
|
|
169
|
+
files.packMd = existsSync(join(runDir, 'pack.md')) || readdirSync(runDir).some((name) => /^pack-[a-z0-9_-]+\.md$/i.test(name))
|
|
170
|
+
if (!files.draftMd) warnings.push('draft.md: recommended')
|
|
171
|
+
|
|
172
|
+
files.packs = readdirSync(runDir).filter((name) => /^pack-[a-z0-9_-]+\.json$/i.test(name))
|
|
173
|
+
const slideshowRun = SLIDESHOW_PLATFORMS.includes(platform) || platform === 'ig'
|
|
174
|
+
const expectedPack = platform === 'ig' ? 'pack-instagram.json' : (platform ? `pack-${platform}.json` : 'pack-<platform>.json')
|
|
175
|
+
if (requirePack && files.packs.length === 0) {
|
|
176
|
+
errors.push(`${expectedPack}: required for slideshow handoff`)
|
|
177
|
+
}
|
|
178
|
+
if (!slideshowRun && files.packs.length > 0) {
|
|
179
|
+
errors.push('pack-<platform>.json: only slideshow platforms may contain a Content Pack')
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return { valid: errors.length === 0, errors, warnings, files, frontmatter }
|
|
183
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs'
|
|
2
|
+
import { basename, join } from 'node:path'
|
|
3
|
+
import { createHash } from 'node:crypto'
|
|
4
|
+
|
|
5
|
+
function isRecord(value) {
|
|
6
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function sha256(bytes) {
|
|
10
|
+
return createHash('sha256').update(bytes).digest('hex')
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function validateExportDirectory(exportDir, options = {}) {
|
|
14
|
+
const expectedPages = options.expectedPages
|
|
15
|
+
const errors = []
|
|
16
|
+
const warnings = []
|
|
17
|
+
const files = { manifest: false, pngs: [] }
|
|
18
|
+
|
|
19
|
+
if (!existsSync(exportDir) || !statSync(exportDir).isDirectory()) {
|
|
20
|
+
return { valid: false, errors: ['export: directory required'], warnings, files }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const manifestPath = join(exportDir, 'manifest.json')
|
|
24
|
+
if (!existsSync(manifestPath)) {
|
|
25
|
+
errors.push('manifest.json: required')
|
|
26
|
+
return { valid: false, errors, warnings, files }
|
|
27
|
+
}
|
|
28
|
+
files.manifest = true
|
|
29
|
+
|
|
30
|
+
let manifest
|
|
31
|
+
try {
|
|
32
|
+
manifest = JSON.parse(readFileSync(manifestPath, 'utf8'))
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
errors.push(`manifest.json: invalid JSON (${error instanceof Error ? error.message : String(error)})`)
|
|
36
|
+
return { valid: false, errors, warnings, files }
|
|
37
|
+
}
|
|
38
|
+
if (!isRecord(manifest)) errors.push('manifest.json: must contain an object')
|
|
39
|
+
|
|
40
|
+
const entries = Array.isArray(manifest?.pages)
|
|
41
|
+
? manifest.pages
|
|
42
|
+
: Array.isArray(manifest?.artifacts) ? manifest.artifacts : []
|
|
43
|
+
if (entries.length === 0) errors.push('manifest.json: pages or artifacts must be a non-empty array')
|
|
44
|
+
|
|
45
|
+
files.pngs = readdirSync(exportDir).filter(name => /^\d{2}(?:-[^/]+)?\.png$/i.test(name) && statSync(join(exportDir, name)).isFile())
|
|
46
|
+
if (entries.length > 0 && files.pngs.length !== entries.length) {
|
|
47
|
+
errors.push(`export: manifest has ${entries.length} page(s), found ${files.pngs.length} PNG file(s)`)
|
|
48
|
+
}
|
|
49
|
+
if (Number.isInteger(expectedPages) && expectedPages > 0 && entries.length !== expectedPages) {
|
|
50
|
+
errors.push(`export: expected ${expectedPages} page(s), manifest has ${entries.length}`)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const pngByName = new Map(files.pngs.map(name => [name, name]))
|
|
54
|
+
entries.forEach((entry, index) => {
|
|
55
|
+
if (!isRecord(entry)) {
|
|
56
|
+
errors.push(`manifest[${index}]: must be an object`)
|
|
57
|
+
return
|
|
58
|
+
}
|
|
59
|
+
const file = typeof entry.file === 'string' && entry.file.length > 0
|
|
60
|
+
? basename(entry.file)
|
|
61
|
+
: `${String(index + 1).padStart(2, '0')}.png`
|
|
62
|
+
if (file !== entry.file && entry.file !== undefined) errors.push(`manifest[${index}].file: must be a local basename`)
|
|
63
|
+
if (!pngByName.has(file)) {
|
|
64
|
+
errors.push(`manifest[${index}].file: missing ${file}`)
|
|
65
|
+
return
|
|
66
|
+
}
|
|
67
|
+
const bytes = readFileSync(join(exportDir, file))
|
|
68
|
+
if (bytes.length === 0) errors.push(`${file}: empty file`)
|
|
69
|
+
if (bytes.length < 8 || bytes.subarray(0, 8).toString('hex') !== '89504e470d0a1a0a') {
|
|
70
|
+
errors.push(`${file}: not a PNG file`)
|
|
71
|
+
}
|
|
72
|
+
if (entry.sha256 && entry.sha256 !== sha256(bytes)) errors.push(`${file}: sha256 mismatch`)
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
return { valid: errors.length === 0, errors, warnings, files, manifest }
|
|
76
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { readFileSync } from 'node:fs'
|
|
3
|
+
import { join } from 'node:path'
|
|
4
|
+
import { spawnSync } from 'node:child_process'
|
|
5
|
+
|
|
6
|
+
const root = new URL('..', import.meta.url).pathname
|
|
7
|
+
const goldenRun = join(root, 'fixtures/golden-run-en')
|
|
8
|
+
const validateRun = join(root, 'scripts/validate-run.mjs')
|
|
9
|
+
const validatePack = join(root, 'scripts/validate-pack.mjs')
|
|
10
|
+
|
|
11
|
+
const packFile = join(goldenRun, 'pack-tiktok.json')
|
|
12
|
+
const packCli = spawnSync(process.execPath, [validatePack, '--file', packFile, '--platform', 'tiktok', '--json'], { encoding: 'utf8' })
|
|
13
|
+
assert.equal(packCli.status, 0, packCli.stdout + packCli.stderr)
|
|
14
|
+
|
|
15
|
+
const runCli = spawnSync(process.execPath, [validateRun, '--run', goldenRun, '--require-pack', '--json'], { encoding: 'utf8' })
|
|
16
|
+
assert.equal(runCli.status, 0, runCli.stdout + runCli.stderr)
|
|
17
|
+
const runReport = JSON.parse(runCli.stdout)
|
|
18
|
+
assert.equal(runReport.valid, true)
|
|
19
|
+
assert.equal(runReport.layout.frontmatter.status, 'pack-ready')
|
|
20
|
+
|
|
21
|
+
const pack = JSON.parse(readFileSync(packFile, 'utf8'))
|
|
22
|
+
assert.equal(pack.pages.length, 4)
|
|
23
|
+
|
|
24
|
+
console.log('Golden fixture validation passed.')
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs'
|
|
2
|
+
import { join, relative } from 'node:path'
|
|
3
|
+
import { fileURLToPath } from 'node:url'
|
|
4
|
+
|
|
5
|
+
export const ENGINE_ROOT = 'Content Engine'
|
|
6
|
+
export const STARTER_DIR = fileURLToPath(new URL('../starter/', import.meta.url))
|
|
7
|
+
export const WORKBENCH_SRC = fileURLToPath(new URL('../workbench/', import.meta.url))
|
|
8
|
+
export const WORKBENCH_FILES = ['template.html', 'build.py', 'serve.py']
|
|
9
|
+
|
|
10
|
+
export const ENGINE_PATHS = {
|
|
11
|
+
runs: 'runs',
|
|
12
|
+
exports: 'exports',
|
|
13
|
+
topics: 'topics',
|
|
14
|
+
discovery: 'discovery',
|
|
15
|
+
swipe: 'swipe',
|
|
16
|
+
profiles: 'profiles',
|
|
17
|
+
inbox: 'inbox',
|
|
18
|
+
accounts: 'accounts',
|
|
19
|
+
raw: 'raw',
|
|
20
|
+
published: 'published',
|
|
21
|
+
wiki: 'wiki',
|
|
22
|
+
platforms: 'platforms',
|
|
23
|
+
workbench: 'workbench',
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function engineJson() {
|
|
27
|
+
return `${JSON.stringify({
|
|
28
|
+
schemaVersion: 'content-engine.v1',
|
|
29
|
+
locale: 'en',
|
|
30
|
+
root: ENGINE_ROOT,
|
|
31
|
+
paths: ENGINE_PATHS,
|
|
32
|
+
}, null, 2)}\n`
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function walkFiles(dir) {
|
|
36
|
+
const out = []
|
|
37
|
+
if (!existsSync(dir)) return out
|
|
38
|
+
for (const name of readdirSync(dir)) {
|
|
39
|
+
if (name === '.DS_Store') continue
|
|
40
|
+
const path = join(dir, name)
|
|
41
|
+
if (statSync(path).isDirectory()) out.push(...walkFiles(path))
|
|
42
|
+
else out.push(path)
|
|
43
|
+
}
|
|
44
|
+
return out
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function filesFor() {
|
|
48
|
+
const files = {}
|
|
49
|
+
for (const path of walkFiles(STARTER_DIR)) {
|
|
50
|
+
const relativePath = relative(STARTER_DIR, path).split('\\').join('/')
|
|
51
|
+
files[relativePath] = readFileSync(path, 'utf8')
|
|
52
|
+
}
|
|
53
|
+
files['engine.json'] = engineJson()
|
|
54
|
+
return files
|
|
55
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { resolve } from 'node:path'
|
|
4
|
+
import { validateExportDirectory } from './export-contracts.mjs'
|
|
5
|
+
|
|
6
|
+
function parseArgs(argv) {
|
|
7
|
+
const options = { exportDir: '', expectedPages: undefined, json: false }
|
|
8
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
9
|
+
const arg = argv[i]
|
|
10
|
+
if (arg === '--export') options.exportDir = argv[++i] ?? ''
|
|
11
|
+
else if (arg === '--expected-pages') options.expectedPages = Number(argv[++i])
|
|
12
|
+
else if (arg === '--json') options.json = true
|
|
13
|
+
else if (arg === '--help' || arg === '-h') options.help = true
|
|
14
|
+
else throw new Error(`Unknown argument: ${arg}`)
|
|
15
|
+
}
|
|
16
|
+
return options
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function output(value, json) {
|
|
20
|
+
process.stdout.write(json ? `${JSON.stringify(value, null, 2)}\n` : `${value.message ?? value}\n`)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
const options = parseArgs(process.argv.slice(2))
|
|
25
|
+
if (options.help) {
|
|
26
|
+
output({ usage: 'node validate-export.mjs --export <export-dir> [--expected-pages N] [--json]' }, false)
|
|
27
|
+
process.exit(0)
|
|
28
|
+
}
|
|
29
|
+
if (!options.exportDir) {
|
|
30
|
+
output({ code: 'EXPORT_REQUIRED', message: 'Pass --export <export-dir>.', valid: false }, options.json)
|
|
31
|
+
process.exitCode = 2
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
const report = validateExportDirectory(resolve(options.exportDir), { expectedPages: options.expectedPages })
|
|
35
|
+
output({ ...report, action: 'validated', export: resolve(options.exportDir) }, options.json)
|
|
36
|
+
if (!report.valid) process.exitCode = 2
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
output({ code: 'EXPORT_VALIDATE_FAILED', message: error instanceof Error ? error.message : String(error), valid: false }, true)
|
|
41
|
+
process.exitCode = 1
|
|
42
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { readFileSync } from 'node:fs'
|
|
4
|
+
import { resolve } from 'node:path'
|
|
5
|
+
import { validateContentPack } from './contracts.mjs'
|
|
6
|
+
|
|
7
|
+
function parseArgs(argv) {
|
|
8
|
+
const options = { file: '', platform: '', requirePageTemplateId: false, json: false }
|
|
9
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
10
|
+
const arg = argv[i]
|
|
11
|
+
if (arg === '--file') options.file = argv[++i] ?? ''
|
|
12
|
+
else if (arg === '--platform') options.platform = argv[++i] ?? ''
|
|
13
|
+
else if (arg === '--require-page-template-id') options.requirePageTemplateId = true
|
|
14
|
+
else if (arg === '--json') options.json = true
|
|
15
|
+
else if (arg === '--help' || arg === '-h') options.help = true
|
|
16
|
+
else throw new Error(`Unknown argument: ${arg}`)
|
|
17
|
+
}
|
|
18
|
+
return options
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function output(value, json) {
|
|
22
|
+
process.stdout.write(json ? `${JSON.stringify(value, null, 2)}\n` : `${value.message ?? value}\n`)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
const options = parseArgs(process.argv.slice(2))
|
|
27
|
+
if (options.help) {
|
|
28
|
+
output({ usage: 'node validate-pack.mjs --file <pack.json> [--platform tiktok] [--require-page-template-id] [--json]' }, false)
|
|
29
|
+
process.exit(0)
|
|
30
|
+
}
|
|
31
|
+
if (!options.file) {
|
|
32
|
+
output({ code: 'FILE_REQUIRED', message: 'Pass --file <pack.json>.', valid: false }, options.json)
|
|
33
|
+
process.exitCode = 2
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
const pack = JSON.parse(readFileSync(resolve(options.file), 'utf8'))
|
|
37
|
+
const report = validateContentPack(pack, {
|
|
38
|
+
requirePageTemplateId: options.requirePageTemplateId,
|
|
39
|
+
platform: options.platform || undefined,
|
|
40
|
+
})
|
|
41
|
+
output({ ...report, action: 'validated', file: resolve(options.file) }, options.json)
|
|
42
|
+
if (!report.valid) process.exitCode = 2
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
output({ code: 'PACK_VALIDATE_FAILED', message: error instanceof Error ? error.message : String(error), valid: false }, true)
|
|
47
|
+
process.exitCode = 1
|
|
48
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
4
|
+
import { join, resolve } from 'node:path'
|
|
5
|
+
import { validateContentPack, validateRunLayout } from './contracts.mjs'
|
|
6
|
+
import { validateExportDirectory } from './export-contracts.mjs'
|
|
7
|
+
|
|
8
|
+
function parseArgs(argv) {
|
|
9
|
+
const options = { run: '', exportDir: '', requirePack: false, requireExport: false, requirePageTemplateId: false, json: false }
|
|
10
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
11
|
+
const arg = argv[i]
|
|
12
|
+
if (arg === '--run') options.run = argv[++i] ?? ''
|
|
13
|
+
else if (arg === '--export') options.exportDir = argv[++i] ?? ''
|
|
14
|
+
else if (arg === '--require-pack') options.requirePack = true
|
|
15
|
+
else if (arg === '--require-export') options.requireExport = true
|
|
16
|
+
else if (arg === '--require-page-template-id') options.requirePageTemplateId = true
|
|
17
|
+
else if (arg === '--json') options.json = true
|
|
18
|
+
else if (arg === '--help' || arg === '-h') options.help = true
|
|
19
|
+
else throw new Error(`Unknown argument: ${arg}`)
|
|
20
|
+
}
|
|
21
|
+
return options
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function output(value, json) {
|
|
25
|
+
process.stdout.write(json ? `${JSON.stringify(value, null, 2)}\n` : `${value.message ?? value}\n`)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
const options = parseArgs(process.argv.slice(2))
|
|
30
|
+
if (options.help) {
|
|
31
|
+
output({ usage: 'node validate-run.mjs --run <run-dir> [--require-pack] [--require-export] [--export <export-dir>] [--json]' }, false)
|
|
32
|
+
process.exit(0)
|
|
33
|
+
}
|
|
34
|
+
if (!options.run) {
|
|
35
|
+
output({ code: 'RUN_REQUIRED', message: 'Pass --run <run-dir>.', valid: false }, options.json)
|
|
36
|
+
process.exitCode = 2
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
const runDir = resolve(options.run)
|
|
40
|
+
const layout = validateRunLayout(runDir, { requirePack: options.requirePack })
|
|
41
|
+
const packs = []
|
|
42
|
+
for (const name of layout.files.packs) {
|
|
43
|
+
const platform = name.replace(/^pack-/, '').replace(/\.json$/i, '')
|
|
44
|
+
try {
|
|
45
|
+
const pack = JSON.parse(readFileSync(join(runDir, name), 'utf8'))
|
|
46
|
+
packs.push({ file: name, pages: Array.isArray(pack.pages) ? pack.pages.length : 0, ...validateContentPack(pack, {
|
|
47
|
+
requirePageTemplateId: options.requirePageTemplateId,
|
|
48
|
+
platform,
|
|
49
|
+
}) })
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
packs.push({ file: name, pages: 0, valid: false, errors: [`invalid JSON (${error instanceof Error ? error.message : String(error)})`], warnings: [] })
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const packErrors = packs.flatMap((item) => item.errors.map((e) => `${item.file}: ${e}`))
|
|
56
|
+
let exportReport
|
|
57
|
+
const needsExport = options.requireExport || layout.frontmatter.status === 'exported'
|
|
58
|
+
if (needsExport) {
|
|
59
|
+
let exportDir = options.exportDir
|
|
60
|
+
if (!exportDir) {
|
|
61
|
+
const engineRoot = resolve(runDir, '..', '..')
|
|
62
|
+
const engineConfig = join(engineRoot, 'engine.json')
|
|
63
|
+
if (existsSync(engineConfig)) {
|
|
64
|
+
try {
|
|
65
|
+
const engine = JSON.parse(readFileSync(engineConfig, 'utf8'))
|
|
66
|
+
const base = join(engineRoot, engine.paths.exports, layout.frontmatter.slug)
|
|
67
|
+
const nested = layout.frontmatter.platform ? join(base, layout.frontmatter.platform) : ''
|
|
68
|
+
exportDir = nested && existsSync(nested) ? nested : base
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
exportReport = { valid: false, errors: [`engine.json: invalid JSON (${error instanceof Error ? error.message : String(error)})`], warnings: [] }
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
else exportReport = { valid: false, errors: ['engine.json: required to derive export path'], warnings: [] }
|
|
75
|
+
}
|
|
76
|
+
if (!exportReport) {
|
|
77
|
+
const expectedPages = packs.find(item => item.pages > 0)?.pages ?? 1
|
|
78
|
+
exportReport = validateExportDirectory(resolve(exportDir), { expectedPages })
|
|
79
|
+
exportReport.export = resolve(exportDir)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const exportErrors = exportReport?.errors?.map(error => `export: ${error}`) ?? []
|
|
83
|
+
const valid = layout.valid && packErrors.length === 0 && exportErrors.length === 0
|
|
84
|
+
output({
|
|
85
|
+
valid,
|
|
86
|
+
action: 'validated',
|
|
87
|
+
run: runDir,
|
|
88
|
+
layout,
|
|
89
|
+
packs,
|
|
90
|
+
export: exportReport,
|
|
91
|
+
errors: [...layout.errors, ...packErrors, ...exportErrors],
|
|
92
|
+
warnings: [...layout.warnings, ...packs.flatMap((item) => item.warnings.map((w) => `${item.file}: ${w}`)), ...(exportReport?.warnings ?? [])],
|
|
93
|
+
}, options.json)
|
|
94
|
+
if (!valid) process.exitCode = 2
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
output({ code: 'RUN_VALIDATE_FAILED', message: error instanceof Error ? error.message : String(error), valid: false }, true)
|
|
99
|
+
process.exitCode = 1
|
|
100
|
+
}
|