@easysociable/cli 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -1
- package/dist/easysociable.js +301 -2829
- package/dist/easysociable.js.map +1 -1
- package/package.json +17 -12
- package/skills/ONBOARDING.md +3 -1
- package/skills/easysociable-content/INSTALL.md +60 -0
- package/skills/easysociable-content/SKILL.md +107 -96
- package/skills/easysociable-content/fixtures/golden-run-en/draft.md +13 -0
- package/skills/easysociable-content/fixtures/golden-run-en/idea.md +26 -0
- package/skills/easysociable-content/fixtures/golden-run-en/pack-tiktok.json +11 -0
- package/skills/easysociable-content/platforms/claude.md +1 -2
- package/skills/easysociable-content/platforms/codex.md +1 -2
- package/skills/easysociable-content/platforms/hermes.md +1 -2
- package/skills/easysociable-content/platforms/openclaw.md +1 -2
- package/skills/easysociable-content/references/CLI.md +25 -12
- package/skills/easysociable-content/references/CONTRACT_SYNC.md +42 -0
- package/skills/easysociable-content/references/MCP.md +7 -12
- package/skills/easysociable-content/references/PAGE_PACK.md +128 -5
- package/skills/easysociable-content/references/PLATFORM_GUIDE.md +36 -0
- package/skills/easysociable-content/references/RUN_LAYOUT.md +73 -0
- package/skills/easysociable-content/references/TROUBLESHOOTING.md +11 -9
- package/skills/easysociable-content/references/VAULT.md +32 -12
- package/skills/easysociable-content/references/VOICE.md +16 -0
- package/skills/easysociable-content/scripts/contracts.mjs +183 -0
- package/skills/easysociable-content/scripts/export-contracts.mjs +76 -0
- package/skills/easysociable-content/scripts/fixture.test.mjs +24 -0
- package/skills/easysociable-content/scripts/package-skill.mjs +49 -0
- package/skills/easysociable-content/scripts/setup.mjs +127 -83
- package/skills/easysociable-content/scripts/setup.test.mjs +49 -38
- package/skills/easysociable-content/scripts/starter.mjs +50 -48
- package/skills/easysociable-content/scripts/validate-export.mjs +42 -0
- package/skills/easysociable-content/scripts/validate-pack.mjs +48 -0
- package/skills/easysociable-content/scripts/validate-run.mjs +100 -0
- package/skills/easysociable-content/scripts/validate.test.mjs +101 -0
- package/skills/easysociable-content/scripts/workbench.mjs +135 -0
- package/skills/easysociable-content/starter/CLAUDE.md +756 -0
- package/skills/easysociable-content/starter/accounts/README.md +9 -0
- package/skills/easysociable-content/starter/accounts/_index.md +31 -0
- package/skills/easysociable-content/starter/accounts/overrides/README.md +3 -0
- package/skills/easysociable-content/starter/discovery/README.md +18 -0
- package/skills/easysociable-content/starter/discovery/_index.md +27 -0
- package/skills/easysociable-content/starter/discovery/_template.md +41 -0
- package/skills/easysociable-content/starter/engine.json +19 -0
- package/skills/easysociable-content/starter/exports/README.md +28 -0
- package/skills/easysociable-content/starter/inbox/README.md +9 -0
- package/skills/easysociable-content/starter/inbox/_index.md +24 -0
- package/skills/easysociable-content/starter/inbox/entries/README.md +3 -0
- package/skills/easysociable-content/starter/platforms/README.md +19 -0
- package/skills/easysociable-content/starter/platforms/ig.md +69 -0
- package/skills/easysociable-content/starter/platforms/instagram.md +5 -0
- package/skills/easysociable-content/starter/platforms/linkedin.md +72 -0
- package/skills/easysociable-content/starter/platforms/threads.md +69 -0
- package/skills/easysociable-content/starter/platforms/tiktok.md +71 -0
- package/skills/easysociable-content/starter/platforms/x.md +78 -0
- package/skills/easysociable-content/starter/platforms/xiaohongshu.md +19 -0
- package/skills/easysociable-content/starter/profiles/_index.md +13 -0
- package/skills/easysociable-content/starter/profiles/default/README.md +11 -0
- package/skills/easysociable-content/starter/profiles/default/audience.md +38 -0
- package/skills/easysociable-content/starter/profiles/default/pillars.md +33 -0
- package/skills/easysociable-content/starter/profiles/default/voice.md +24 -0
- package/skills/easysociable-content/starter/published/_index.md +20 -0
- package/skills/easysociable-content/starter/raw/README.md +4 -0
- package/skills/easysociable-content/starter/raw/_index.md +12 -0
- package/skills/easysociable-content/starter/runs/README.md +22 -0
- package/skills/easysociable-content/starter/runs/_index.md +41 -0
- package/skills/easysociable-content/starter/runs/_template/brief.md +66 -0
- package/skills/easysociable-content/starter/runs/_template/draft.md +11 -0
- package/skills/easysociable-content/starter/runs/_template/editor.md +18 -0
- package/skills/easysociable-content/starter/runs/_template/feedback.md +15 -0
- package/skills/easysociable-content/starter/runs/_template/idea.md +27 -0
- package/skills/easysociable-content/starter/runs/_template/pack.md +47 -0
- package/skills/easysociable-content/starter/runs/_template/packet.md +25 -0
- package/skills/easysociable-content/starter/runs/_template/rubric.md +19 -0
- package/skills/easysociable-content/starter/swipe/_index.md +27 -0
- package/skills/easysociable-content/starter/swipe/_template.md +34 -0
- package/skills/easysociable-content/starter/topics/README.md +25 -0
- package/skills/easysociable-content/starter/topics/WATCHLIST.md +65 -0
- package/skills/easysociable-content/starter/topics/_index.md +44 -0
- package/skills/easysociable-content/starter/topics/_template-item.md +90 -0
- package/skills/easysociable-content/starter/topics/items/README.md +4 -0
- package/skills/easysociable-content/starter/topics/scoring.md +201 -0
- package/skills/easysociable-content/starter/topics/watchlists/README.md +10 -0
- package/skills/easysociable-content/starter/topics/watchlists/_shared.md +14 -0
- package/skills/easysociable-content/starter/topics/watchlists/default.md +6 -0
- package/skills/easysociable-content/starter/wiki/README.md +13 -0
- package/skills/easysociable-content/starter/wiki/atoms/_index.md +23 -0
- package/skills/easysociable-content/starter/wiki/atoms/_template.md +31 -0
- package/skills/easysociable-content/starter/wiki/claims/_index.md +26 -0
- package/skills/easysociable-content/starter/wiki/claims/_template.md +38 -0
- package/skills/easysociable-content/starter/workbench/README.md +17 -0
- package/skills/easysociable-content/workbench/build.py +1074 -0
- package/skills/easysociable-content/workbench/serve.py +53 -0
- package/skills/easysociable-content/workbench/template.html +2119 -0
- package/skills/easysociable-content/workflows/ANALYSIS_AND_COVERAGE.md +14 -5
- package/skills/easysociable-content/workflows/CONTENT_EXPORT.md +16 -11
- package/skills/easysociable-content/workflows/GETTING_STARTED.md +40 -0
- package/skills/easysociable-content/workflows/MCP_FIRST.md +3 -8
- package/skills/easysociable-content/workflows/OUT_OF_SCOPE.md +18 -0
- package/skills/easysociable-content/workflows/PACK_AUTHORING.md +42 -0
- package/skills/easysociable-content/workflows/SWIPE_CAPTURE.md +18 -0
- package/skills/easysociable-content/workflows/TOPIC_DRAFT.md +28 -4
- package/skills/easysociable-content/workflows/WORKING_DRAFT.md +17 -8
- package/skills/easysociable-slideshow/SKILL.md +20 -13
- package/skills/easysociable-slideshow/references/VAULT.md +13 -11
- package/skills/easysociable-template/scripts/generate-theme-from-hex.mjs +0 -0
- package/skills/easysociable-content/workflows/DEVICE_TASKS.md +0 -5
- package/skills/easysociable-content/workflows/EXPERIMENTS.md +0 -6
- package/skills/easysociable-content/workflows/PUBLICATION.md +0 -5
|
@@ -1,6 +1,129 @@
|
|
|
1
|
-
# PagePack handoff
|
|
1
|
+
# PagePack handoff (`content-pack.v1`)
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
composition
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
The Pack is the boundary between semantic content (Content Skill) and visual
|
|
4
|
+
composition (Template / Slideshow Skills). Content writes slots and roles;
|
|
5
|
+
downstream skills choose Page Templates and render.
|
|
6
|
+
|
|
7
|
+
**Source of truth for caps and roles:** `packages/template-public-contracts`
|
|
8
|
+
(`contentPackSchema`). This file is the Agent-facing summary.
|
|
9
|
+
|
|
10
|
+
## Schema
|
|
11
|
+
|
|
12
|
+
```json
|
|
13
|
+
{
|
|
14
|
+
"schemaVersion": "content-pack.v1",
|
|
15
|
+
"platform": "tiktok",
|
|
16
|
+
"title": "Optional title",
|
|
17
|
+
"pages": [
|
|
18
|
+
{
|
|
19
|
+
"role": "hook",
|
|
20
|
+
"slots": { "text_0": "Hook line", "text_1": "Support" },
|
|
21
|
+
"pageTemplateId": "tpl_selected_at_handoff"
|
|
22
|
+
}
|
|
23
|
+
]
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
| Field | Required | Notes |
|
|
28
|
+
|---|---|---|
|
|
29
|
+
| `schemaVersion` | yes | Must be `content-pack.v1` |
|
|
30
|
+
| `platform` | yes | See platform table below |
|
|
31
|
+
| `title` | no | Display title for Studio |
|
|
32
|
+
| `pages` | yes | Ordered array, length within platform caps |
|
|
33
|
+
| `pages[].role` | yes | Closed `pageRoles` enum |
|
|
34
|
+
| `pages[].slots` | yes | Public slot key → string/number/boolean/null |
|
|
35
|
+
| `pages[].pageTemplateId` | yes | Published Page Template selected by Slideshow before `create` |
|
|
36
|
+
|
|
37
|
+
## Forbidden in Pack
|
|
38
|
+
|
|
39
|
+
Never include:
|
|
40
|
+
|
|
41
|
+
- `deck_template_id` / Deck references
|
|
42
|
+
- Template AST, Binding Maps, RenderPlans
|
|
43
|
+
- `ContentDocument` / `contentDocumentHash`
|
|
44
|
+
- Private gateway credentials or image URLs as slot values (use `imageId` after import)
|
|
45
|
+
|
|
46
|
+
## Platform page caps
|
|
47
|
+
|
|
48
|
+
| Platform | Min pages | Max pages | V1 Slideshow |
|
|
49
|
+
|---|---:|---:|---|
|
|
50
|
+
| `tiktok` | 4 | 35 | yes |
|
|
51
|
+
| `instagram` | 2 | 20 | yes |
|
|
52
|
+
| `linkedin` | 2 | 300 | yes |
|
|
53
|
+
| `threads` | 2 | 20 | yes |
|
|
54
|
+
| `xiaohongshu` | — | — | not a Content Pack route in V1; use Template single-page |
|
|
55
|
+
|
|
56
|
+
`x` is not a Pack platform. Use Template fill-slots for single-page X graphics.
|
|
57
|
+
|
|
58
|
+
## Page roles (closed set)
|
|
59
|
+
|
|
60
|
+
`cover`, `hook`, `setup`, `context`, `value`, `point`, `checklist`, `proof`,
|
|
61
|
+
`evidence`, `compare`, `screenshot`, `metric`, `quote`, `steps`, `cta`, `follow`, `recap`
|
|
62
|
+
|
|
63
|
+
Pick roles that match the narrative arc. Slideshow maps each page to a published
|
|
64
|
+
Page Template with compatible `supportedRoles`.
|
|
65
|
+
|
|
66
|
+
## File naming
|
|
67
|
+
|
|
68
|
+
During the run, write vault-native pack markdown:
|
|
69
|
+
|
|
70
|
+
```text
|
|
71
|
+
pack.md
|
|
72
|
+
pack-<platform>.md
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
`content-pack.v1` JSON is created **at Slideshow/Template handoff**, not during draft:
|
|
76
|
+
|
|
77
|
+
```text
|
|
78
|
+
pack-<platform>.json
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Example: `pack-tiktok.json`. Pure X text has no pack.
|
|
82
|
+
|
|
83
|
+
## Validation
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
node scripts/validate-pack.mjs --file ./pack-tiktok.json --platform tiktok --json
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Before Slideshow `create`, also run:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
node scripts/validate-pack.mjs --file ./pack-tiktok.json --require-page-template-id --json
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Examples
|
|
96
|
+
|
|
97
|
+
### English — TikTok (4 pages, content stage)
|
|
98
|
+
|
|
99
|
+
```json
|
|
100
|
+
{
|
|
101
|
+
"schemaVersion": "content-pack.v1",
|
|
102
|
+
"platform": "tiktok",
|
|
103
|
+
"title": "Morning routine tips",
|
|
104
|
+
"pages": [
|
|
105
|
+
{ "role": "hook", "pageTemplateId": "tpl_hook_001", "slots": { "text_0": "Stop scrolling — this changed my mornings" } },
|
|
106
|
+
{ "role": "value", "pageTemplateId": "tpl_value_001", "slots": { "text_0": "Wake at the same time", "text_1": "No phone for 20 minutes" } },
|
|
107
|
+
{ "role": "value", "pageTemplateId": "tpl_value_001", "slots": { "text_0": "Move before caffeine", "text_1": "Even 5 minutes counts" } },
|
|
108
|
+
{ "role": "cta", "pageTemplateId": "tpl_cta_001", "slots": { "text_0": "Save this for tomorrow" } }
|
|
109
|
+
]
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### 中文 — TikTok(4 页,内容阶段)
|
|
114
|
+
|
|
115
|
+
```json
|
|
116
|
+
{
|
|
117
|
+
"schemaVersion": "content-pack.v1",
|
|
118
|
+
"platform": "tiktok",
|
|
119
|
+
"title": "晨间习惯三条",
|
|
120
|
+
"pages": [
|
|
121
|
+
{ "role": "hook", "pageTemplateId": "tpl_hook_001", "slots": { "text_0": "别再瞎忙了——我靠这三条稳住早晨" } },
|
|
122
|
+
{ "role": "value", "pageTemplateId": "tpl_value_001", "slots": { "text_0": "固定起床时间", "text_1": "醒来先别碰手机" } },
|
|
123
|
+
{ "role": "value", "pageTemplateId": "tpl_value_001", "slots": { "text_0": "喝咖啡前先动一动", "text_1": "五分钟也算数" } },
|
|
124
|
+
{ "role": "cta", "pageTemplateId": "tpl_cta_001", "slots": { "text_0": "先收藏,明早试" } }
|
|
125
|
+
]
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
JSON field names and `role` values stay English even when slot copy is 中文.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Platform guide
|
|
2
|
+
|
|
3
|
+
Use this matrix before opening a run. **Ask** if the user did not name a platform. Multi-select → **N runs**.
|
|
4
|
+
|
|
5
|
+
| Platform | Slideshow (V1) | Pack platform key | Page cap (slideshow) | Default path |
|
|
6
|
+
|---|---|---|---:|---|
|
|
7
|
+
| TikTok | yes | `tiktok` | 4–35 | Slideshow |
|
|
8
|
+
| Instagram | yes | `instagram` (`ig`) | 2–20 | Slideshow |
|
|
9
|
+
| LinkedIn | yes | `linkedin` | 2–300 | Slideshow |
|
|
10
|
+
| Threads | yes | `threads` | 2–20 | Slideshow |
|
|
11
|
+
| X | **no** | — (no Pack) | — | Template single-page or text-only |
|
|
12
|
+
| Xiaohongshu | **no** (V1) | — | — | Template single-page |
|
|
13
|
+
|
|
14
|
+
Craft notes live under `Content Engine/platforms/`. Map: `instagram` → `ig.md`. Read the file before brief/draft/editor.
|
|
15
|
+
|
|
16
|
+
## Routing
|
|
17
|
+
|
|
18
|
+
```text
|
|
19
|
+
1 page → easysociable-template
|
|
20
|
+
2+ pages + slideshow platform → easysociable-slideshow
|
|
21
|
+
x → easysociable-template only (if images)
|
|
22
|
+
xiaohongshu → easysociable-template until slideshow ships
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Canvas hints (for downstream skills)
|
|
26
|
+
|
|
27
|
+
| Platform | Typical canvas | Notes |
|
|
28
|
+
|---|---|---|
|
|
29
|
+
| tiktok | 9:16 @ 1080×1920 | Photo Mode carousel |
|
|
30
|
+
| instagram / ig | 4:5 or 1:1 @ 1080 | Feed carousel |
|
|
31
|
+
| linkedin | 4:5 or 1:1 | Document-style decks |
|
|
32
|
+
| threads | 4:5 or 1:1 | Conversational copy |
|
|
33
|
+
| x | 16:9 or 1:1 | Single image post |
|
|
34
|
+
| xiaohongshu | 3:4 @ 1080×1440 | See Template `PLATFORM_SPECS` |
|
|
35
|
+
|
|
36
|
+
Content Skill does not pick Page Templates — only platform, roles and slot copy. Pack JSON is written at Slideshow/Template handoff, not during draft.
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# Run layout
|
|
2
|
+
|
|
3
|
+
Copy `runs/_template/` to `runs/<YYYY-MM-DD-short-slug[-platform]>/`.
|
|
4
|
+
|
|
5
|
+
**One folder = one platform.** Same Topic on another network = another folder sharing `topic_id`.
|
|
6
|
+
|
|
7
|
+
```text
|
|
8
|
+
Content Engine/runs/<slug>/
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Core files
|
|
12
|
+
|
|
13
|
+
| File | Purpose |
|
|
14
|
+
|---|---|
|
|
15
|
+
| `idea.md` | Run id, status, **this** platform, selection (`swipe_id` / `atoms` / `claim_id`) |
|
|
16
|
+
| `brief.md` | Angle, constraints, selection why |
|
|
17
|
+
| `packet.md` | Writer context (pillar + voice + audience + craft notes) |
|
|
18
|
+
| `draft.md` | Copy for **this platform only** |
|
|
19
|
+
| `editor.md` | REVISE / APPROVE |
|
|
20
|
+
| `rubric.md` | 6 × 0–2; SHIP ≥ 8 |
|
|
21
|
+
| `pack.md` | Image deck slots after SHIP (slideshow platforms) |
|
|
22
|
+
| `feedback.md` | URL + `win`/`flat`/`loss` after publish |
|
|
23
|
+
| `pack-<platform>.json` | Complete `content-pack.v1` — **only at render handoff** |
|
|
24
|
+
|
|
25
|
+
## `idea.md` frontmatter
|
|
26
|
+
|
|
27
|
+
```yaml
|
|
28
|
+
---
|
|
29
|
+
run_id: RUN-YYYYMMDD-slug
|
|
30
|
+
status: RUN_OPEN
|
|
31
|
+
created: YYYY-MM-DD
|
|
32
|
+
---
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Body must include `primary_platform` equal to `platforms` (single value) and the three selection fields. `none` is allowed with why.
|
|
36
|
+
|
|
37
|
+
### File status vs index status
|
|
38
|
+
|
|
39
|
+
| `idea.md` | `runs/_index.md` |
|
|
40
|
+
|---|---|
|
|
41
|
+
| `RUN_OPEN` | `open` |
|
|
42
|
+
| `BRIEF_WAIT` | `brief` |
|
|
43
|
+
| `DRAFTED` / `EDIT_WAIT` | `draft` |
|
|
44
|
+
| `SCORED` | `scored` |
|
|
45
|
+
| `READY_TO_POST` | `ready` |
|
|
46
|
+
| `PUBLISHED` | `published` |
|
|
47
|
+
| `HOLD` / `KILLED` / `SUPERSEDED` | `hold` / `killed` / `superseded` |
|
|
48
|
+
|
|
49
|
+
Do not use index status `partial` for new runs.
|
|
50
|
+
|
|
51
|
+
## Validation
|
|
52
|
+
|
|
53
|
+
Semantic run (no Pack JSON yet):
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
node scripts/validate-run.mjs --run <run-dir> --json
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Slideshow handoff (Pack JSON required):
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
node scripts/validate-run.mjs --run <run-dir> --require-pack --json
|
|
63
|
+
node scripts/validate-pack.mjs --file <run-dir>/pack-<platform>.json --platform <platform> --json
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Export path
|
|
67
|
+
|
|
68
|
+
Final PNGs at the **engine root**, not inside the run folder:
|
|
69
|
+
|
|
70
|
+
```text
|
|
71
|
+
Content Engine/exports/<run-slug>/01.png
|
|
72
|
+
Content Engine/exports/<run-slug>/<platform>/01.png # optional subfolder
|
|
73
|
+
```
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
# Troubleshooting
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
3
|
+
No Content CLI, Local API, SQLite, or Obsidian requirement.
|
|
4
|
+
|
|
5
|
+
If setup fails, stop before overwriting files. `python3` missing → install Python 3; starter files may already be written.
|
|
6
|
+
|
|
7
|
+
If port 8765 is in use, reuse `http://127.0.0.1:8765/workbench/` — do not start a second server. Rebuild with `python3 workbench/build.py`.
|
|
8
|
+
|
|
9
|
+
Never overwrite an existing `workbench/template.html`. `--apply` only adds missing files.
|
|
10
|
+
|
|
11
|
+
If a run is incomplete, keep existing markdown and report the missing file. `validate-run.mjs` / `validate-pack.mjs` list concrete gaps. Do not auto-migrate an existing engine.
|
|
12
|
+
|
|
13
|
+
Deferred features: `workflows/OUT_OF_SCOPE.md`.
|
|
@@ -1,20 +1,40 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Engine layout
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The skill creates one English root:
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
| `zh` | `内容引擎/` | `成稿/<slug>/` | `导出/<slug>/<platform>/` |
|
|
5
|
+
```text
|
|
6
|
+
<parent>/Content Engine/
|
|
7
|
+
```
|
|
9
8
|
|
|
10
|
-
|
|
9
|
+
`--root` is the **parent** directory, or the `Content Engine/` path itself.
|
|
11
10
|
|
|
12
11
|
```json
|
|
13
|
-
{
|
|
12
|
+
{
|
|
13
|
+
"schemaVersion": "content-engine.v1",
|
|
14
|
+
"locale": "en",
|
|
15
|
+
"root": "Content Engine",
|
|
16
|
+
"paths": { "runs": "runs", "exports": "exports", "workbench": "workbench" }
|
|
17
|
+
}
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Folders: `accounts`, `inbox`, `swipe`, `topics`, `discovery`, `raw`, `profiles`, `published`, `wiki`, `platforms`, `runs`, `exports`, `workbench`.
|
|
21
|
+
|
|
22
|
+
Platform craft files: `x.md`, `ig.md` (`instagram` → `ig.md`), `tiktok.md`, `linkedin.md`, `threads.md`, `xiaohongshu.md`.
|
|
23
|
+
|
|
24
|
+
## Workbench
|
|
25
|
+
|
|
26
|
+
Copied from the skill package **only when missing**. Never overwrite an existing `workbench/template.html`.
|
|
27
|
+
|
|
28
|
+
```text
|
|
29
|
+
http://127.0.0.1:8765/workbench/
|
|
14
30
|
```
|
|
15
31
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
32
|
+
Rebuild with `python3 workbench/build.py` after markdown writes.
|
|
33
|
+
|
|
34
|
+
## Export path
|
|
35
|
+
|
|
36
|
+
```text
|
|
37
|
+
Content Engine/exports/<run-slug>/01.png
|
|
38
|
+
```
|
|
19
39
|
|
|
20
|
-
|
|
40
|
+
Exports are **not** nested under `runs/<slug>/`. See `references/RUN_LAYOUT.md`.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Local voice vs hosted Profile
|
|
2
|
+
|
|
3
|
+
| Concept | Where | When | Purpose |
|
|
4
|
+
|---|---|---|---|
|
|
5
|
+
| **Local voice** | `profiles/<id>/voice.md` | First draft (optional) | Identity + tone for the Agent |
|
|
6
|
+
| **Local profile** | `profiles/<id>/` | Multi-brand | Pillars + audience + voice |
|
|
7
|
+
| **Hosted Brand Profile** | Gateway (`easysociable profiles …`) | First render | Visual identity: Theme, logo, quotas |
|
|
8
|
+
|
|
9
|
+
## Rules
|
|
10
|
+
|
|
11
|
+
1. **Setup does not create a hosted Profile.**
|
|
12
|
+
2. **Local voice is optional.** Ask once; user may skip.
|
|
13
|
+
3. **Hosted Profile is required for render**, owned by Template/Slideshow skills.
|
|
14
|
+
4. Never store API keys, passwords or Stripe tokens in `profiles/` or `accounts/`.
|
|
15
|
+
|
|
16
|
+
When writing `draft.md` or pack slots, read the resolved voice file and audience **core + this platform**.
|
|
@@ -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,49 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { cpSync, existsSync, mkdirSync, rmSync } from 'node:fs'
|
|
4
|
+
import { join, relative, resolve, sep } from 'node:path'
|
|
5
|
+
|
|
6
|
+
const skillRoot = resolve(new URL('..', import.meta.url).pathname)
|
|
7
|
+
const include = ['SKILL.md', 'INSTALL.md', 'scripts', 'references', 'workflows', 'platforms', 'fixtures', 'starter', 'workbench']
|
|
8
|
+
|
|
9
|
+
function parseArgs(argv) {
|
|
10
|
+
let output = ''
|
|
11
|
+
let force = false
|
|
12
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
13
|
+
if (argv[i] === '--output') output = argv[++i] ?? ''
|
|
14
|
+
else if (argv[i] === '--force') force = true
|
|
15
|
+
else if (argv[i] === '--help' || argv[i] === '-h') return { help: true }
|
|
16
|
+
else throw new Error(`Unknown argument: ${argv[i]}`)
|
|
17
|
+
}
|
|
18
|
+
return { output, force }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
const options = parseArgs(process.argv.slice(2))
|
|
23
|
+
if (options.help) {
|
|
24
|
+
process.stdout.write('Usage: node package-skill.mjs --output <destination-dir> [--force]\n')
|
|
25
|
+
process.exit(0)
|
|
26
|
+
}
|
|
27
|
+
if (!options.output) throw new Error('--output is required')
|
|
28
|
+
|
|
29
|
+
const dest = resolve(options.output)
|
|
30
|
+
const sourceToDest = relative(skillRoot, dest)
|
|
31
|
+
const destToSource = relative(dest, skillRoot)
|
|
32
|
+
const touchesSource = dest === skillRoot
|
|
33
|
+
|| (!sourceToDest.startsWith(`..${sep}`) && sourceToDest !== '..')
|
|
34
|
+
|| (!destToSource.startsWith(`..${sep}`) && destToSource !== '..')
|
|
35
|
+
if (touchesSource) throw new Error('--output must be outside the skill source directory')
|
|
36
|
+
if (existsSync(dest) && !options.force) throw new Error('Output already exists; pass --force to replace it')
|
|
37
|
+
if (existsSync(dest)) rmSync(dest, { recursive: true, force: true })
|
|
38
|
+
mkdirSync(dest, { recursive: true })
|
|
39
|
+
|
|
40
|
+
for (const name of include) {
|
|
41
|
+
cpSync(join(skillRoot, name), join(dest, name), { recursive: true })
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
process.stdout.write(`Packaged skill → ${dest}\n`)
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
console.error(error instanceof Error ? error.message : String(error))
|
|
48
|
+
process.exitCode = 1
|
|
49
|
+
}
|