@easysociable/cli 0.1.7 → 0.1.9
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 -9
- package/dist/easysociable.js +111 -100
- package/dist/easysociable.js.map +1 -1
- package/package.json +2 -2
- package/scripts/engine/migrate-split-vault.py +75 -0
- package/skills/ONBOARDING.md +19 -4
- package/skills/easysociable-content/SKILL.md +25 -9
- package/content-engine/scripts/fixture.test.mjs +0 -24
- package/content-engine/scripts/starter.mjs +0 -58
- package/content-engine/scripts/validate.test.mjs +0 -101
- package/content-engine/scripts/workbench.mjs +0 -140
- package/content-engine/starter/CLAUDE.md +0 -908
- package/content-engine/starter/accounts/README.md +0 -9
- package/content-engine/starter/accounts/_index.md +0 -31
- package/content-engine/starter/accounts/overrides/README.md +0 -3
- package/content-engine/starter/engine.json +0 -23
- package/content-engine/starter/exports/README.md +0 -28
- package/content-engine/starter/hits/README.md +0 -5
- package/content-engine/starter/hits/_index.md +0 -30
- package/content-engine/starter/hits/_template.md +0 -46
- package/content-engine/starter/hits/entries/README.md +0 -3
- package/content-engine/starter/inbox/README.md +0 -9
- package/content-engine/starter/inbox/_index.md +0 -24
- package/content-engine/starter/inbox/entries/README.md +0 -3
- package/content-engine/starter/needs/README.md +0 -38
- package/content-engine/starter/needs/_index.md +0 -31
- package/content-engine/starter/needs/_template.md +0 -24
- package/content-engine/starter/needs/entries/README.md +0 -5
- package/content-engine/starter/platforms/README.md +0 -19
- package/content-engine/starter/platforms/ig.md +0 -69
- package/content-engine/starter/platforms/instagram.md +0 -5
- package/content-engine/starter/platforms/linkedin.md +0 -72
- package/content-engine/starter/platforms/threads.md +0 -69
- package/content-engine/starter/platforms/tiktok.md +0 -71
- package/content-engine/starter/platforms/x.md +0 -78
- package/content-engine/starter/platforms/xiaohongshu.md +0 -19
- package/content-engine/starter/products/README.md +0 -38
- package/content-engine/starter/products/_index.md +0 -25
- package/content-engine/starter/products/_template.md +0 -56
- package/content-engine/starter/products/entries/P-001-easysociable.md +0 -150
- package/content-engine/starter/products/entries/P-002-content-engine.md +0 -136
- package/content-engine/starter/profiles/_index.md +0 -16
- package/content-engine/starter/profiles/default/README.md +0 -17
- package/content-engine/starter/profiles/default/audience.md +0 -38
- package/content-engine/starter/profiles/default/boundaries.md +0 -14
- package/content-engine/starter/profiles/default/expression.md +0 -18
- package/content-engine/starter/profiles/default/handles.md +0 -8
- package/content-engine/starter/profiles/default/keywords.md +0 -31
- package/content-engine/starter/profiles/default/lanes.md +0 -12
- package/content-engine/starter/profiles/default/pillars.md +0 -33
- package/content-engine/starter/profiles/default/stories.md +0 -15
- package/content-engine/starter/profiles/default/voice.md +0 -38
- package/content-engine/starter/published/_index.md +0 -20
- package/content-engine/starter/raw/README.md +0 -4
- package/content-engine/starter/raw/_index.md +0 -12
- package/content-engine/starter/recommendations/README.md +0 -21
- package/content-engine/starter/recommendations/_index.md +0 -21
- package/content-engine/starter/recommendations/_template.md +0 -44
- package/content-engine/starter/runs/README.md +0 -22
- package/content-engine/starter/runs/_index.md +0 -41
- package/content-engine/starter/runs/_template/brief.md +0 -76
- package/content-engine/starter/runs/_template/draft.md +0 -11
- package/content-engine/starter/runs/_template/editor.md +0 -18
- package/content-engine/starter/runs/_template/feedback.md +0 -15
- package/content-engine/starter/runs/_template/idea.md +0 -27
- package/content-engine/starter/runs/_template/pack.md +0 -55
- package/content-engine/starter/runs/_template/packet.md +0 -31
- package/content-engine/starter/runs/_template/rubric.md +0 -19
- package/content-engine/starter/swipe/_index.md +0 -27
- package/content-engine/starter/swipe/_template.md +0 -36
- package/content-engine/starter/topics/README.md +0 -21
- package/content-engine/starter/topics/_index.md +0 -51
- package/content-engine/starter/topics/_template-item.md +0 -106
- package/content-engine/starter/topics/items/README.md +0 -4
- package/content-engine/starter/topics/scoring.md +0 -255
- package/content-engine/starter/wiki/README.md +0 -13
- package/content-engine/starter/wiki/atoms/_index.md +0 -23
- package/content-engine/starter/wiki/atoms/_template.md +0 -31
- package/content-engine/starter/wiki/claims/_index.md +0 -26
- package/content-engine/starter/wiki/claims/_template.md +0 -38
- package/content-engine/starter/workbench/README.md +0 -17
- package/content-engine/workbench/__pycache__/build.cpython-310.pyc +0 -0
- package/content-engine/workbench/build.py +0 -1467
- package/content-engine/workbench/serve.py +0 -53
- package/content-engine/workbench/template.html +0 -2686
- /package/{content-engine/scripts → scripts/engine}/contracts.mjs +0 -0
- /package/{content-engine/scripts → scripts/engine}/export-contracts.mjs +0 -0
- /package/{content-engine → scripts/engine}/fixtures/golden-run-en/draft.md +0 -0
- /package/{content-engine → scripts/engine}/fixtures/golden-run-en/idea.md +0 -0
- /package/{content-engine → scripts/engine}/fixtures/golden-run-en/pack-tiktok.json +0 -0
- /package/{content-engine/scripts → scripts/engine}/validate-export.mjs +0 -0
- /package/{content-engine/scripts → scripts/engine}/validate-pack.mjs +0 -0
- /package/{content-engine/scripts → scripts/engine}/validate-run.mjs +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@easysociable/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.9",
|
|
4
4
|
"description": "Agent-first CLI for EasySociable: Content Engine, Template, Slideshow, and MCP",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"bin",
|
|
34
34
|
"dist",
|
|
35
35
|
"skills",
|
|
36
|
-
"
|
|
36
|
+
"scripts/engine",
|
|
37
37
|
"LICENSE",
|
|
38
38
|
"README.md"
|
|
39
39
|
],
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Split a mixed Content Engine folder into core + vault.
|
|
3
|
+
|
|
4
|
+
Default layout after migration (next to your current engine):
|
|
5
|
+
|
|
6
|
+
<parent>/
|
|
7
|
+
content-engine-core/ # fresh clone; engine.json vault_root → ../<old-engine-name>
|
|
8
|
+
<old-engine-name>/ # becomes the vault (your data stays here)
|
|
9
|
+
|
|
10
|
+
Usage:
|
|
11
|
+
python3 migrate-split-vault.py --engine "/path/to/Content Engine"
|
|
12
|
+
python3 migrate-split-vault.py --engine "/path/to/Content Engine" --core-name content-engine-core --yes
|
|
13
|
+
|
|
14
|
+
Requires git + network (clones https://github.com/murphywuwu/content-engine.git).
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import argparse
|
|
20
|
+
import json
|
|
21
|
+
import shutil
|
|
22
|
+
import subprocess
|
|
23
|
+
import sys
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
REPO = "https://github.com/murphywuwu/content-engine.git"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def main() -> int:
|
|
30
|
+
ap = argparse.ArgumentParser(description=__doc__)
|
|
31
|
+
ap.add_argument("--engine", required=True, help="Path to existing mixed Content Engine")
|
|
32
|
+
ap.add_argument("--core-name", default="content-engine-core", help="Sibling folder name for new core clone")
|
|
33
|
+
ap.add_argument("--repo", default=REPO)
|
|
34
|
+
ap.add_argument("--yes", action="store_true", help="Apply (otherwise dry-run)")
|
|
35
|
+
args = ap.parse_args()
|
|
36
|
+
|
|
37
|
+
engine = Path(args.engine).expanduser().resolve()
|
|
38
|
+
if not (engine / "CLAUDE.md").exists():
|
|
39
|
+
print(f"error: {engine} is not a Content Engine (missing CLAUDE.md)", file=sys.stderr)
|
|
40
|
+
return 2
|
|
41
|
+
|
|
42
|
+
parent = engine.parent
|
|
43
|
+
core = parent / args.core_name
|
|
44
|
+
rel_vault = Path("..") / engine.name
|
|
45
|
+
|
|
46
|
+
print(f"engine (vault): {engine}")
|
|
47
|
+
print(f"new core: {core}")
|
|
48
|
+
print(f"vault_root: {rel_vault.as_posix()}")
|
|
49
|
+
if core.exists():
|
|
50
|
+
print(f"error: {core} already exists", file=sys.stderr)
|
|
51
|
+
return 2
|
|
52
|
+
|
|
53
|
+
if not args.yes:
|
|
54
|
+
print("dry-run only. Re-run with --yes to clone core and write vault_root.")
|
|
55
|
+
return 0
|
|
56
|
+
|
|
57
|
+
subprocess.check_call(["git", "clone", "--depth", "1", args.repo, str(core)])
|
|
58
|
+
# Detach so users don't accidentally push private notes to the public remote.
|
|
59
|
+
shutil.rmtree(core / ".git", ignore_errors=True)
|
|
60
|
+
|
|
61
|
+
ej_path = core / "engine.json"
|
|
62
|
+
data = json.loads(ej_path.read_text(encoding="utf-8"))
|
|
63
|
+
data["vault_root"] = rel_vault.as_posix()
|
|
64
|
+
ej_path.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
|
|
65
|
+
|
|
66
|
+
print("done.")
|
|
67
|
+
print(f" cd {core}")
|
|
68
|
+
print(" python3 workbench/build.py")
|
|
69
|
+
print(" python3 workbench/serve.py")
|
|
70
|
+
print("Data stays in the old folder; core only holds CLAUDE.md + workbench + templates.")
|
|
71
|
+
return 0
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
if __name__ == "__main__":
|
|
75
|
+
raise SystemExit(main())
|
package/skills/ONBOARDING.md
CHANGED
|
@@ -22,16 +22,30 @@ V1 expects an Agent (Grok Bot, Codex, Claude Code, …) to drive setup. The webs
|
|
|
22
22
|
- `easysociable-slideshow` — turns a draft into slideshow pages you preview and download in the dashboard
|
|
23
23
|
- `easysociable-template` — creates page templates
|
|
24
24
|
|
|
25
|
-
Optional Content Engine skill (
|
|
25
|
+
Optional Content Engine skill (install via CLI only — not in the public
|
|
26
|
+
content-engine git repo; operations still follow that engine’s `CLAUDE.md`):
|
|
26
27
|
|
|
27
28
|
```bash
|
|
28
29
|
easysociable install --skills content,template,slideshow --yes
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Get a Content Engine core (preferred):
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
git clone https://github.com/murphywuwu/content-engine.git
|
|
36
|
+
cd content-engine
|
|
37
|
+
python3 workbench/build.py
|
|
38
|
+
python3 workbench/serve.py --no-open
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
See [LAYOUT.md](https://github.com/murphywuwu/content-engine/blob/main/LAYOUT.md).
|
|
42
|
+
Optional CLI clone into `Content Engine/`:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
29
45
|
easysociable engine init --root /workspace --yes
|
|
30
46
|
easysociable engine serve --engine /workspace/Content Engine --no-open
|
|
31
47
|
```
|
|
32
48
|
|
|
33
|
-
On a laptop, use any parent path instead of `/workspace` for `engine init` / `serve`.
|
|
34
|
-
|
|
35
49
|
3. **Ask the Agent to create graphics** — account creation is deferred until a hosted operation requires authentication. If you installed the content skill: interview and write the profile first, then capture audience quotes into `needs/` (at least 3, aim for 5); do not create a slideshow yet.
|
|
36
50
|
|
|
37
51
|
## Lazy auth (Agent script)
|
|
@@ -56,7 +70,8 @@ idempotency key when applicable.
|
|
|
56
70
|
## What works without auth
|
|
57
71
|
|
|
58
72
|
- `easysociable install` / `update`
|
|
59
|
-
- `easysociable engine init` / `serve` / `build` / `validate`
|
|
73
|
+
- `easysociable engine init` / `serve` / `build` / `validate` (optional; core may be copied from git)
|
|
74
|
+
- Local `python3 workbench/build.py` / `serve.py` without CLI
|
|
60
75
|
- Reading installed Skill instructions
|
|
61
76
|
- Public template catalog in the browser (`/templates`)
|
|
62
77
|
|
|
@@ -12,7 +12,8 @@ description: >
|
|
|
12
12
|
— edit `recommendations/`; do not treat third-party subjects as owned Products.
|
|
13
13
|
Triggers: 做成选题, 开一单, 出图
|
|
14
14
|
— follow engine CLAUDE.md (§ Topics / § Open a run / § PagePack).
|
|
15
|
-
If no engine exists,
|
|
15
|
+
If no engine exists, copy/clone the Content Engine core (see Create below;
|
|
16
|
+
`easysociable engine init` is optional offline scaffold). Then follow
|
|
16
17
|
Content Engine/CLAUDE.md. Render handoff: easysociable-template (1 page) or
|
|
17
18
|
easysociable-slideshow (2+). Never publish or post.
|
|
18
19
|
---
|
|
@@ -82,21 +83,36 @@ profile_thesis = Profile + Pillar
|
|
|
82
83
|
|
|
83
84
|
## Create / serve / build
|
|
84
85
|
|
|
86
|
+
**This skill** is installed only via EasySociable CLI (`easysociable install --skills content…`). It is **not** shipped inside the public Content Engine git repo.
|
|
87
|
+
|
|
88
|
+
**Primary (public core — preferred):** clone the Content Engine repo, then run
|
|
89
|
+
workbench from that folder. See `LAYOUT.md` (core vs vault; optional
|
|
90
|
+
`engine.json` `vault_root`).
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
git clone https://github.com/murphywuwu/content-engine.git
|
|
94
|
+
cd content-engine
|
|
95
|
+
python3 workbench/build.py
|
|
96
|
+
python3 workbench/serve.py --no-open
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
**Optional:** `easysociable engine init --root <parent> --yes` clones the same
|
|
100
|
+
repo into `<parent>/Content Engine` (never overwrites an existing engine).
|
|
101
|
+
|
|
85
102
|
```bash
|
|
86
|
-
easysociable engine init --root <parent-or-engine> --yes
|
|
87
103
|
easysociable engine serve --engine <parent>/Content Engine --no-open
|
|
88
104
|
easysociable engine build --engine <parent>/Content Engine
|
|
89
|
-
#
|
|
90
|
-
python3 workbench/build.py
|
|
105
|
+
# --sync-workbench refreshes workbench files from the public core checkout
|
|
91
106
|
```
|
|
92
107
|
|
|
93
|
-
On a dedicated agent computer (e.g. Grok Bot), prefer
|
|
108
|
+
On a dedicated agent computer (e.g. Grok Bot), prefer `/workspace` + clone or
|
|
109
|
+
`engine init`. Mixed→split vault: see `scripts/migrate-split-vault.py` in the
|
|
110
|
+
public repo.
|
|
94
111
|
|
|
95
|
-
|
|
112
|
+
`python3` is required for the workbench. Observatory: `http://127.0.0.1:8765/workbench/` — top tabs include **需求库 / Needs**. If `serve.py` is already up, do not start a second server; rebuild only.
|
|
96
113
|
|
|
97
|
-
`
|
|
98
|
-
|
|
99
|
-
entries or migrate existing product notes.
|
|
114
|
+
Engine methodology updates: `git pull` on the core (or `--sync-workbench`), not
|
|
115
|
+
CLI republishes.
|
|
100
116
|
|
|
101
117
|
## Operate
|
|
102
118
|
|
|
@@ -1,24 +0,0 @@
|
|
|
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.')
|
|
@@ -1,58 +0,0 @@
|
|
|
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
|
-
needs: 'needs',
|
|
15
|
-
hits: 'hits',
|
|
16
|
-
products: 'products',
|
|
17
|
-
recommendations: 'recommendations',
|
|
18
|
-
swipe: 'swipe',
|
|
19
|
-
profiles: 'profiles',
|
|
20
|
-
inbox: 'inbox',
|
|
21
|
-
accounts: 'accounts',
|
|
22
|
-
raw: 'raw',
|
|
23
|
-
published: 'published',
|
|
24
|
-
wiki: 'wiki',
|
|
25
|
-
platforms: 'platforms',
|
|
26
|
-
workbench: 'workbench',
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export function engineJson() {
|
|
30
|
-
return `${JSON.stringify({
|
|
31
|
-
schemaVersion: 'content-engine.v1',
|
|
32
|
-
locale: 'en',
|
|
33
|
-
root: ENGINE_ROOT,
|
|
34
|
-
paths: ENGINE_PATHS,
|
|
35
|
-
}, null, 2)}\n`
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function walkFiles(dir) {
|
|
39
|
-
const out = []
|
|
40
|
-
if (!existsSync(dir)) return out
|
|
41
|
-
for (const name of readdirSync(dir)) {
|
|
42
|
-
if (name === '.DS_Store') continue
|
|
43
|
-
const path = join(dir, name)
|
|
44
|
-
if (statSync(path).isDirectory()) out.push(...walkFiles(path))
|
|
45
|
-
else out.push(path)
|
|
46
|
-
}
|
|
47
|
-
return out
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export function filesFor() {
|
|
51
|
-
const files = {}
|
|
52
|
-
for (const path of walkFiles(STARTER_DIR)) {
|
|
53
|
-
const relativePath = relative(STARTER_DIR, path).split('\\').join('/')
|
|
54
|
-
files[relativePath] = readFileSync(path, 'utf8')
|
|
55
|
-
}
|
|
56
|
-
files['engine.json'] = engineJson()
|
|
57
|
-
return files
|
|
58
|
-
}
|
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
import assert from 'node:assert/strict'
|
|
2
|
-
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
|
3
|
-
import { tmpdir } from 'node:os'
|
|
4
|
-
import { join } from 'node:path'
|
|
5
|
-
import { spawnSync } from 'node:child_process'
|
|
6
|
-
import { validateContentPack } from './contracts.mjs'
|
|
7
|
-
import { validateExportDirectory } from './export-contracts.mjs'
|
|
8
|
-
|
|
9
|
-
const root = new URL('.', import.meta.url).pathname
|
|
10
|
-
const validatePack = join(root, 'validate-pack.mjs')
|
|
11
|
-
const validateRun = join(root, 'validate-run.mjs')
|
|
12
|
-
const fixture = mkdtempSync(join(tmpdir(), 'easysociable-validate-'))
|
|
13
|
-
|
|
14
|
-
function run(script, args) {
|
|
15
|
-
return spawnSync(process.execPath, [script, ...args, '--json'], { encoding: 'utf8' })
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function ideaMd({ platform = 'tiktok', status = 'pack-ready' } = {}) {
|
|
19
|
-
return `---
|
|
20
|
-
run_id: RUN-demo
|
|
21
|
-
status: ${status}
|
|
22
|
-
created: 2026-08-28
|
|
23
|
-
---
|
|
24
|
-
|
|
25
|
-
# Idea
|
|
26
|
-
|
|
27
|
-
- **one_liner:** Demo
|
|
28
|
-
- **primary_platform:** ${platform}
|
|
29
|
-
- **platforms:** ${platform}
|
|
30
|
-
- **swipe_id:** none
|
|
31
|
-
- **atoms:** none
|
|
32
|
-
- **claim_id:** none
|
|
33
|
-
`
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const tiktokPack = {
|
|
37
|
-
schemaVersion: 'content-pack.v1',
|
|
38
|
-
platform: 'tiktok',
|
|
39
|
-
title: 'Demo',
|
|
40
|
-
pages: [
|
|
41
|
-
{ role: 'hook', pageTemplateId: 'tpl_hook', slots: { text_0: 'a' } },
|
|
42
|
-
{ role: 'value', pageTemplateId: 'tpl_value', slots: { text_0: 'b' } },
|
|
43
|
-
{ role: 'value', pageTemplateId: 'tpl_value', slots: { text_0: 'c' } },
|
|
44
|
-
{ role: 'cta', pageTemplateId: 'tpl_cta', slots: { text_0: 'd' } },
|
|
45
|
-
],
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
try {
|
|
49
|
-
const ok = validateContentPack(tiktokPack)
|
|
50
|
-
assert.equal(ok.valid, true)
|
|
51
|
-
|
|
52
|
-
const short = validateContentPack({ ...tiktokPack, pages: tiktokPack.pages.slice(0, 3) })
|
|
53
|
-
assert.equal(short.valid, false)
|
|
54
|
-
|
|
55
|
-
const forbidden = validateContentPack({ ...tiktokPack, deck_template_id: 'x' })
|
|
56
|
-
assert.equal(forbidden.valid, false)
|
|
57
|
-
|
|
58
|
-
const badSlot = validateContentPack({ ...tiktokPack, pages: [{ ...tiktokPack.pages[0], slots: { text_0: [] } }, ...tiktokPack.pages.slice(1)] })
|
|
59
|
-
assert.equal(badSlot.valid, false)
|
|
60
|
-
|
|
61
|
-
const missingTemplate = validateContentPack({ ...tiktokPack, pages: [{ role: 'hook', slots: {} }, ...tiktokPack.pages.slice(1)] })
|
|
62
|
-
assert.equal(missingTemplate.valid, false)
|
|
63
|
-
|
|
64
|
-
const packFile = join(fixture, 'pack-tiktok.json')
|
|
65
|
-
writeFileSync(packFile, JSON.stringify(tiktokPack), 'utf8')
|
|
66
|
-
const packCli = run(validatePack, ['--file', packFile, '--platform', 'tiktok'])
|
|
67
|
-
assert.equal(packCli.status, 0)
|
|
68
|
-
|
|
69
|
-
const runDir = join(fixture, 'demo-run')
|
|
70
|
-
mkdirSync(runDir)
|
|
71
|
-
writeFileSync(join(runDir, 'idea.md'), ideaMd(), 'utf8')
|
|
72
|
-
writeFileSync(join(runDir, 'draft.md'), '# Draft\n', 'utf8')
|
|
73
|
-
writeFileSync(join(runDir, 'pack-tiktok.json'), JSON.stringify(tiktokPack), 'utf8')
|
|
74
|
-
|
|
75
|
-
const runCli = run(validateRun, ['--run', runDir, '--require-pack'])
|
|
76
|
-
assert.equal(runCli.status, 0)
|
|
77
|
-
const runReport = JSON.parse(runCli.stdout)
|
|
78
|
-
assert.equal(runReport.valid, true)
|
|
79
|
-
|
|
80
|
-
rmSync(join(runDir, 'pack-tiktok.json'))
|
|
81
|
-
const missingPack = run(validateRun, ['--run', runDir, '--require-pack'])
|
|
82
|
-
assert.equal(missingPack.status, 2)
|
|
83
|
-
|
|
84
|
-
const exportDir = join(fixture, 'exported')
|
|
85
|
-
mkdirSync(exportDir)
|
|
86
|
-
writeFileSync(join(exportDir, '01.png'), Buffer.from('89504e470d0a1a0a', 'hex'))
|
|
87
|
-
writeFileSync(join(exportDir, 'manifest.json'), JSON.stringify({ schema: 'template-export.v1', artifacts: [{ pageIndex: 0 }] }))
|
|
88
|
-
assert.equal(validateExportDirectory(exportDir, { expectedPages: 1 }).valid, true)
|
|
89
|
-
|
|
90
|
-
const exportedRun = join(fixture, 'exported-run')
|
|
91
|
-
mkdirSync(exportedRun)
|
|
92
|
-
writeFileSync(join(exportedRun, 'idea.md'), ideaMd({ platform: 'x', status: 'exported' }), 'utf8')
|
|
93
|
-
writeFileSync(join(exportedRun, 'draft.md'), '# Draft\n', 'utf8')
|
|
94
|
-
const exportedCli = run(validateRun, ['--run', exportedRun, '--export', exportDir, '--require-export'])
|
|
95
|
-
assert.equal(exportedCli.status, 0, exportedCli.stdout)
|
|
96
|
-
|
|
97
|
-
console.log('Content Skill validate smoke passed.')
|
|
98
|
-
}
|
|
99
|
-
finally {
|
|
100
|
-
rmSync(fixture, { recursive: true, force: true })
|
|
101
|
-
}
|
|
@@ -1,140 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import { spawn, spawnSync } from 'node:child_process'
|
|
4
|
-
import { existsSync, mkdirSync, copyFileSync } from 'node:fs'
|
|
5
|
-
import net from 'node:net'
|
|
6
|
-
import { pathToFileURL } from 'node:url'
|
|
7
|
-
import { join, resolve } from 'node:path'
|
|
8
|
-
import { WORKBENCH_FILES, WORKBENCH_SRC } from './starter.mjs'
|
|
9
|
-
|
|
10
|
-
export const WORKBENCH_PORT = 8765
|
|
11
|
-
export const WORKBENCH_URL = `http://127.0.0.1:${WORKBENCH_PORT}/workbench/`
|
|
12
|
-
|
|
13
|
-
function output(value, json) {
|
|
14
|
-
process.stdout.write(json ? `${JSON.stringify(value, null, 2)}\n` : `${value.message ?? value}\n`)
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export function python3Bin() {
|
|
18
|
-
const result = spawnSync('python3', ['--version'], { encoding: 'utf8' })
|
|
19
|
-
if (result.status === 0) return 'python3'
|
|
20
|
-
return ''
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export function portInUse(port = WORKBENCH_PORT) {
|
|
24
|
-
return new Promise((resolvePort) => {
|
|
25
|
-
const socket = net.connect({ port, host: '127.0.0.1' }, () => {
|
|
26
|
-
socket.end()
|
|
27
|
-
resolvePort(true)
|
|
28
|
-
})
|
|
29
|
-
socket.on('error', () => resolvePort(false))
|
|
30
|
-
})
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/** `update: false` = init (missing only). `update: true` = build/serve refresh from package. */
|
|
34
|
-
export function ensureWorkbench(engine, { update = false } = {}) {
|
|
35
|
-
const destDir = join(engine, 'workbench')
|
|
36
|
-
mkdirSync(destDir, { recursive: true })
|
|
37
|
-
const copied = []
|
|
38
|
-
const updated = []
|
|
39
|
-
const skipped = []
|
|
40
|
-
for (const name of WORKBENCH_FILES) {
|
|
41
|
-
const dest = join(destDir, name)
|
|
42
|
-
const from = join(WORKBENCH_SRC, name)
|
|
43
|
-
if (existsSync(dest) && !update) {
|
|
44
|
-
skipped.push(name)
|
|
45
|
-
continue
|
|
46
|
-
}
|
|
47
|
-
const existed = existsSync(dest)
|
|
48
|
-
copyFileSync(from, dest)
|
|
49
|
-
if (existed) updated.push(name)
|
|
50
|
-
else copied.push(name)
|
|
51
|
-
}
|
|
52
|
-
return { destDir, copied, updated, skipped }
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export function buildWorkbench(engine) {
|
|
56
|
-
const python = python3Bin()
|
|
57
|
-
if (!python) {
|
|
58
|
-
return { ok: false, code: 'PYTHON3_REQUIRED', message: 'python3 is required for the workbench. Install Python 3 and retry.' }
|
|
59
|
-
}
|
|
60
|
-
const script = join(engine, 'workbench', 'build.py')
|
|
61
|
-
if (!existsSync(script)) {
|
|
62
|
-
return { ok: false, code: 'WORKBENCH_MISSING', message: 'workbench/build.py is missing. Re-run setup --apply.' }
|
|
63
|
-
}
|
|
64
|
-
const result = spawnSync(python, [script], { encoding: 'utf8', cwd: engine })
|
|
65
|
-
if (result.status !== 0) {
|
|
66
|
-
return { ok: false, code: 'WORKBENCH_BUILD_FAILED', message: result.stderr.trim() || result.stdout.trim() || 'workbench build failed' }
|
|
67
|
-
}
|
|
68
|
-
return { ok: true, message: result.stdout.trim() }
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export async function startWorkbench(engine, { noOpen = false } = {}) {
|
|
72
|
-
const python = python3Bin()
|
|
73
|
-
if (!python) {
|
|
74
|
-
return { ok: false, code: 'PYTHON3_REQUIRED', message: 'python3 is required for the workbench. Install Python 3 and retry.' }
|
|
75
|
-
}
|
|
76
|
-
ensureWorkbench(engine, { update: true })
|
|
77
|
-
const built = buildWorkbench(engine)
|
|
78
|
-
if (!built.ok) return built
|
|
79
|
-
const url = WORKBENCH_URL
|
|
80
|
-
if (await portInUse()) {
|
|
81
|
-
if (!noOpen && process.env.EASYSOCIABLE_NO_OPEN !== 'true') openUrl(url)
|
|
82
|
-
return { ok: true, alreadyRunning: true, url, build: built.message }
|
|
83
|
-
}
|
|
84
|
-
const args = [join(engine, 'workbench', 'serve.py')]
|
|
85
|
-
if (noOpen) args.push('--no-open')
|
|
86
|
-
const child = spawn(python, args, { cwd: engine, detached: true, stdio: 'ignore' })
|
|
87
|
-
child.unref()
|
|
88
|
-
return { ok: true, alreadyRunning: false, url, pid: child.pid, build: built.message }
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function openUrl(url) {
|
|
92
|
-
const command = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'cmd' : 'xdg-open'
|
|
93
|
-
const args = process.platform === 'win32' ? ['/c', 'start', url] : [url]
|
|
94
|
-
spawn(command, args, { stdio: 'ignore', detached: true }).unref()
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function parseArgs(argv) {
|
|
98
|
-
const options = { engine: '', command: '', noOpen: false, json: false }
|
|
99
|
-
for (let i = 0; i < argv.length; i += 1) {
|
|
100
|
-
const arg = argv[i]
|
|
101
|
-
if (arg === '--engine') options.engine = argv[++i] ?? ''
|
|
102
|
-
else if (arg === '--no-open') options.noOpen = true
|
|
103
|
-
else if (arg === '--json') options.json = true
|
|
104
|
-
else if (arg === '--help' || arg === '-h') options.help = true
|
|
105
|
-
else if (!arg.startsWith('--') && !options.command) options.command = arg
|
|
106
|
-
else throw new Error(`Unknown argument: ${arg}`)
|
|
107
|
-
}
|
|
108
|
-
return options
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
async function main() {
|
|
112
|
-
const options = parseArgs(process.argv.slice(2))
|
|
113
|
-
if (options.help) {
|
|
114
|
-
output({ usage: 'node workbench.mjs build|start --engine <Content Engine> [--no-open] [--json]' }, false)
|
|
115
|
-
return
|
|
116
|
-
}
|
|
117
|
-
if (!options.engine) throw new Error('--engine is required')
|
|
118
|
-
const engine = resolve(options.engine)
|
|
119
|
-
if (options.command === 'build') {
|
|
120
|
-
const workbench = ensureWorkbench(engine, { update: true })
|
|
121
|
-
const built = buildWorkbench(engine)
|
|
122
|
-
output({ workbench, ...built }, options.json)
|
|
123
|
-
if (!built.ok) process.exitCode = 2
|
|
124
|
-
return
|
|
125
|
-
}
|
|
126
|
-
if (options.command === 'start' || options.command === '') {
|
|
127
|
-
const started = await startWorkbench(engine, { noOpen: options.noOpen })
|
|
128
|
-
output(started, options.json)
|
|
129
|
-
if (!started.ok) process.exitCode = 2
|
|
130
|
-
return
|
|
131
|
-
}
|
|
132
|
-
throw new Error('Command must be build or start')
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) {
|
|
136
|
-
main().catch((error) => {
|
|
137
|
-
output({ code: 'WORKBENCH_FAILED', message: error instanceof Error ? error.message : String(error) }, true)
|
|
138
|
-
process.exitCode = 1
|
|
139
|
-
})
|
|
140
|
-
}
|