@easysociable/cli 0.1.8 → 0.1.10

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.
Files changed (93) hide show
  1. package/README.md +30 -9
  2. package/dist/easysociable.js +234 -98
  3. package/dist/easysociable.js.map +1 -1
  4. package/package.json +2 -2
  5. package/scripts/engine/migrate-split-vault.py +75 -0
  6. package/skills/ONBOARDING.md +19 -4
  7. package/skills/easysociable-content/SKILL.md +25 -9
  8. package/content-engine/scripts/fixture.test.mjs +0 -24
  9. package/content-engine/scripts/starter.mjs +0 -58
  10. package/content-engine/scripts/validate.test.mjs +0 -101
  11. package/content-engine/scripts/workbench.mjs +0 -140
  12. package/content-engine/starter/CLAUDE.md +0 -908
  13. package/content-engine/starter/accounts/README.md +0 -9
  14. package/content-engine/starter/accounts/_index.md +0 -31
  15. package/content-engine/starter/accounts/overrides/README.md +0 -3
  16. package/content-engine/starter/engine.json +0 -23
  17. package/content-engine/starter/exports/README.md +0 -28
  18. package/content-engine/starter/hits/README.md +0 -5
  19. package/content-engine/starter/hits/_index.md +0 -30
  20. package/content-engine/starter/hits/_template.md +0 -46
  21. package/content-engine/starter/hits/entries/README.md +0 -3
  22. package/content-engine/starter/inbox/README.md +0 -9
  23. package/content-engine/starter/inbox/_index.md +0 -24
  24. package/content-engine/starter/inbox/entries/README.md +0 -3
  25. package/content-engine/starter/needs/README.md +0 -38
  26. package/content-engine/starter/needs/_index.md +0 -31
  27. package/content-engine/starter/needs/_template.md +0 -24
  28. package/content-engine/starter/needs/entries/README.md +0 -5
  29. package/content-engine/starter/platforms/README.md +0 -19
  30. package/content-engine/starter/platforms/ig.md +0 -69
  31. package/content-engine/starter/platforms/instagram.md +0 -5
  32. package/content-engine/starter/platforms/linkedin.md +0 -72
  33. package/content-engine/starter/platforms/threads.md +0 -69
  34. package/content-engine/starter/platforms/tiktok.md +0 -71
  35. package/content-engine/starter/platforms/x.md +0 -78
  36. package/content-engine/starter/platforms/xiaohongshu.md +0 -19
  37. package/content-engine/starter/products/README.md +0 -38
  38. package/content-engine/starter/products/_index.md +0 -25
  39. package/content-engine/starter/products/_template.md +0 -56
  40. package/content-engine/starter/products/entries/P-001-easysociable.md +0 -150
  41. package/content-engine/starter/products/entries/P-002-content-engine.md +0 -136
  42. package/content-engine/starter/profiles/_index.md +0 -16
  43. package/content-engine/starter/profiles/default/README.md +0 -17
  44. package/content-engine/starter/profiles/default/audience.md +0 -38
  45. package/content-engine/starter/profiles/default/boundaries.md +0 -14
  46. package/content-engine/starter/profiles/default/expression.md +0 -18
  47. package/content-engine/starter/profiles/default/handles.md +0 -8
  48. package/content-engine/starter/profiles/default/keywords.md +0 -31
  49. package/content-engine/starter/profiles/default/lanes.md +0 -12
  50. package/content-engine/starter/profiles/default/pillars.md +0 -33
  51. package/content-engine/starter/profiles/default/stories.md +0 -15
  52. package/content-engine/starter/profiles/default/voice.md +0 -38
  53. package/content-engine/starter/published/_index.md +0 -20
  54. package/content-engine/starter/raw/README.md +0 -4
  55. package/content-engine/starter/raw/_index.md +0 -12
  56. package/content-engine/starter/recommendations/README.md +0 -21
  57. package/content-engine/starter/recommendations/_index.md +0 -21
  58. package/content-engine/starter/recommendations/_template.md +0 -44
  59. package/content-engine/starter/runs/README.md +0 -22
  60. package/content-engine/starter/runs/_index.md +0 -41
  61. package/content-engine/starter/runs/_template/brief.md +0 -76
  62. package/content-engine/starter/runs/_template/draft.md +0 -11
  63. package/content-engine/starter/runs/_template/editor.md +0 -18
  64. package/content-engine/starter/runs/_template/feedback.md +0 -15
  65. package/content-engine/starter/runs/_template/idea.md +0 -27
  66. package/content-engine/starter/runs/_template/pack.md +0 -55
  67. package/content-engine/starter/runs/_template/packet.md +0 -31
  68. package/content-engine/starter/runs/_template/rubric.md +0 -19
  69. package/content-engine/starter/swipe/_index.md +0 -27
  70. package/content-engine/starter/swipe/_template.md +0 -36
  71. package/content-engine/starter/topics/README.md +0 -21
  72. package/content-engine/starter/topics/_index.md +0 -51
  73. package/content-engine/starter/topics/_template-item.md +0 -106
  74. package/content-engine/starter/topics/items/README.md +0 -4
  75. package/content-engine/starter/topics/scoring.md +0 -255
  76. package/content-engine/starter/wiki/README.md +0 -13
  77. package/content-engine/starter/wiki/atoms/_index.md +0 -23
  78. package/content-engine/starter/wiki/atoms/_template.md +0 -31
  79. package/content-engine/starter/wiki/claims/_index.md +0 -26
  80. package/content-engine/starter/wiki/claims/_template.md +0 -38
  81. package/content-engine/starter/workbench/README.md +0 -17
  82. package/content-engine/workbench/__pycache__/build.cpython-310.pyc +0 -0
  83. package/content-engine/workbench/build.py +0 -1467
  84. package/content-engine/workbench/serve.py +0 -53
  85. package/content-engine/workbench/template.html +0 -2683
  86. /package/{content-engine/scripts → scripts/engine}/contracts.mjs +0 -0
  87. /package/{content-engine/scripts → scripts/engine}/export-contracts.mjs +0 -0
  88. /package/{content-engine → scripts/engine}/fixtures/golden-run-en/draft.md +0 -0
  89. /package/{content-engine → scripts/engine}/fixtures/golden-run-en/idea.md +0 -0
  90. /package/{content-engine → scripts/engine}/fixtures/golden-run-en/pack-tiktok.json +0 -0
  91. /package/{content-engine/scripts → scripts/engine}/validate-export.mjs +0 -0
  92. /package/{content-engine/scripts → scripts/engine}/validate-pack.mjs +0 -0
  93. /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.8",
3
+ "version": "0.1.10",
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
- "content-engine",
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())
@@ -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 (pointer only — operations live in `Content Engine/CLAUDE.md`):
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, run `easysociable engine init`. Then follow
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
- # or, inside the vault:
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 `--root /workspace`.
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
- Existing files are never overwritten. `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.
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
- `engine init` adds missing `products/` and `recommendations/` starter files to
98
- older vaults without overwriting user files. It does not invent recommendation
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
- }