@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,38 +1,26 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import { existsSync, mkdirSync, statSync, writeFileSync } from 'node:fs'
|
|
3
|
+
import { existsSync, mkdirSync, readdirSync, statSync, writeFileSync } from 'node:fs'
|
|
4
4
|
import { homedir } from 'node:os'
|
|
5
|
-
import { join, resolve } from 'node:path'
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
const LOCALES = new Set(['en', 'zh'])
|
|
10
|
-
|
|
11
|
-
function obsidianCandidates() {
|
|
12
|
-
if (process.env.EASYSOCIABLE_OBSIDIAN_APP)
|
|
13
|
-
return [process.env.EASYSOCIABLE_OBSIDIAN_APP]
|
|
14
|
-
return [
|
|
15
|
-
'/Applications/Obsidian.app',
|
|
16
|
-
join(homedir(), 'Applications/Obsidian.app'),
|
|
17
|
-
'/System/Applications/Obsidian.app',
|
|
18
|
-
].filter(Boolean)
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function hasObsidian() {
|
|
22
|
-
if (process.env.EASYSOCIABLE_SKIP_OBSIDIAN_CHECK === 'true') return true
|
|
23
|
-
return obsidianCandidates().some((path) => existsSync(path))
|
|
24
|
-
}
|
|
5
|
+
import { basename, join, resolve } from 'node:path'
|
|
6
|
+
import { ENGINE_ROOT, filesFor } from './starter.mjs'
|
|
7
|
+
import { buildWorkbench, ensureWorkbench, python3Bin, startWorkbench } from './workbench.mjs'
|
|
25
8
|
|
|
26
9
|
function parseArgs(argv) {
|
|
27
|
-
const options = {
|
|
10
|
+
const options = {
|
|
11
|
+
root: '', engine: '', apply: false, validate: false, preview: false,
|
|
12
|
+
detect: false, noOpen: false, json: false, workbench: false,
|
|
13
|
+
}
|
|
28
14
|
for (let i = 0; i < argv.length; i += 1) {
|
|
29
15
|
const arg = argv[i]
|
|
30
|
-
if (arg === '--
|
|
31
|
-
else if (arg === '--
|
|
16
|
+
if (arg === '--root') options.root = argv[++i] ?? ''
|
|
17
|
+
else if (arg === '--engine') options.engine = argv[++i] ?? ''
|
|
32
18
|
else if (arg === '--apply') options.apply = true
|
|
33
19
|
else if (arg === '--validate') options.validate = true
|
|
34
20
|
else if (arg === '--preview') options.preview = true
|
|
21
|
+
else if (arg === '--detect') options.detect = true
|
|
35
22
|
else if (arg === '--no-open') options.noOpen = true
|
|
23
|
+
else if (arg === '--workbench') options.workbench = true
|
|
36
24
|
else if (arg === '--json') options.json = true
|
|
37
25
|
else if (arg === '--help' || arg === '-h') options.help = true
|
|
38
26
|
else throw new Error(`Unknown argument: ${arg}`)
|
|
@@ -48,76 +36,123 @@ function isDir(path) {
|
|
|
48
36
|
return existsSync(path) && statSync(path).isDirectory()
|
|
49
37
|
}
|
|
50
38
|
|
|
51
|
-
function
|
|
52
|
-
return
|
|
39
|
+
function isEngine(path) {
|
|
40
|
+
return isDir(path) && existsSync(join(path, 'CLAUDE.md')) && isDir(join(path, 'runs'))
|
|
53
41
|
}
|
|
54
42
|
|
|
55
|
-
function
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
if (en && zh) return { code: 'BOTH_ENGINES', message: 'Vault has both Content Engine/ and 内容引擎/. Choose one folder before setup.' }
|
|
60
|
-
if (zh) return { locale: 'zh' }
|
|
61
|
-
if (en) return { locale: 'en' }
|
|
62
|
-
return { locale: '' }
|
|
43
|
+
function resolveEngine(rootOrEngine) {
|
|
44
|
+
const absolute = resolve(rootOrEngine)
|
|
45
|
+
if (isEngine(absolute) || basename(absolute) === ENGINE_ROOT) return absolute
|
|
46
|
+
return join(absolute, ENGINE_ROOT)
|
|
63
47
|
}
|
|
64
48
|
|
|
65
|
-
function
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
return { code: 'LOCALE_MISMATCH', message: `Vault already uses locale ${found.locale}. Do not create the other language tree.` }
|
|
73
|
-
}
|
|
74
|
-
const locale = requested || found.locale
|
|
75
|
-
if (!locale) return { code: 'LOCALE_REQUIRED', message: 'Ask the user to choose 中文 (zh) or English (en), then pass --locale.' }
|
|
76
|
-
return { locale }
|
|
49
|
+
function searchRoots() {
|
|
50
|
+
const extra = (process.env.EASYSOCIABLE_ENGINE_SEARCH_ROOTS || '')
|
|
51
|
+
.split(process.platform === 'win32' ? ';' : ':')
|
|
52
|
+
.map((item) => item.trim())
|
|
53
|
+
.filter(Boolean)
|
|
54
|
+
const home = homedir()
|
|
55
|
+
return [...extra, join(home, 'Documents'), home]
|
|
77
56
|
}
|
|
78
57
|
|
|
79
|
-
function
|
|
80
|
-
const
|
|
81
|
-
const
|
|
58
|
+
function detectEngines() {
|
|
59
|
+
const found = []
|
|
60
|
+
for (const root of searchRoots()) {
|
|
61
|
+
if (!isDir(root)) continue
|
|
62
|
+
const direct = join(root, ENGINE_ROOT)
|
|
63
|
+
if (isEngine(direct) || isDir(direct)) found.push(direct)
|
|
64
|
+
try {
|
|
65
|
+
for (const name of readdirSync(root)) {
|
|
66
|
+
const child = join(root, name, ENGINE_ROOT)
|
|
67
|
+
if (isDir(child)) found.push(child)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
continue
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return [...new Set(found)]
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function inventory(engine) {
|
|
78
|
+
const files = filesFor()
|
|
82
79
|
const missing = []
|
|
83
80
|
const existing = []
|
|
84
81
|
for (const relative of Object.keys(files)) {
|
|
85
82
|
const path = join(engine, relative)
|
|
86
83
|
;(existsSync(path) ? existing : missing).push(relative)
|
|
87
84
|
}
|
|
88
|
-
return {
|
|
85
|
+
return {
|
|
86
|
+
engine,
|
|
87
|
+
engineExists: isDir(engine),
|
|
88
|
+
rootName: ENGINE_ROOT,
|
|
89
|
+
missing,
|
|
90
|
+
existing,
|
|
91
|
+
starterComplete: missing.length === 0,
|
|
92
|
+
}
|
|
89
93
|
}
|
|
90
94
|
|
|
91
|
-
function
|
|
92
|
-
const
|
|
95
|
+
function validateRoot(rootOrEngine, { strict = false } = {}) {
|
|
96
|
+
const engine = resolveEngine(rootOrEngine)
|
|
97
|
+
const parent = resolve(engine, '..')
|
|
93
98
|
const errors = []
|
|
94
|
-
if (!existsSync(
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
99
|
+
if (!existsSync(parent) || !statSync(parent).isDirectory()) {
|
|
100
|
+
errors.push('Parent directory does not exist')
|
|
101
|
+
return { engine, valid: false, errors, created: [] }
|
|
102
|
+
}
|
|
103
|
+
const result = inventory(engine)
|
|
104
|
+
if (existsSync(engine) && !isDir(engine)) errors.push('Content Engine exists but is not a directory')
|
|
105
|
+
if (strict) {
|
|
106
|
+
if (!result.engineExists) errors.push('Content Engine/ folder does not exist')
|
|
107
|
+
else if (result.missing.length > 0) {
|
|
108
|
+
errors.push(`Starter incomplete: ${result.missing.length} file(s) missing (use --apply to add only missing files)`)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return { ...result, valid: errors.length === 0, errors, created: [] }
|
|
100
112
|
}
|
|
101
113
|
|
|
102
|
-
function applyStarter(
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
mkdirSync(
|
|
114
|
+
async function applyStarter(rootOrEngine, noOpen) {
|
|
115
|
+
const engine = resolveEngine(rootOrEngine)
|
|
116
|
+
const checked = validateRoot(engine)
|
|
117
|
+
if (checked.code) return { ...checked, created: [] }
|
|
118
|
+
mkdirSync(engine, { recursive: true })
|
|
119
|
+
const report = inventory(engine)
|
|
120
|
+
const created = []
|
|
121
|
+
const files = filesFor()
|
|
107
122
|
for (const relative of report.missing) {
|
|
108
|
-
const path = join(
|
|
123
|
+
const path = join(engine, relative)
|
|
109
124
|
mkdirSync(join(path, '..'), { recursive: true })
|
|
110
125
|
writeFileSync(path, files[relative], { encoding: 'utf8', flag: 'wx' })
|
|
126
|
+
created.push(relative)
|
|
111
127
|
}
|
|
128
|
+
const workbench = ensureWorkbench(engine)
|
|
129
|
+
const python = python3Bin()
|
|
130
|
+
if (!python) {
|
|
131
|
+
return {
|
|
132
|
+
...validateRoot(engine),
|
|
133
|
+
created,
|
|
134
|
+
workbench,
|
|
135
|
+
action: 'applied',
|
|
136
|
+
valid: false,
|
|
137
|
+
code: 'PYTHON3_REQUIRED',
|
|
138
|
+
errors: ['python3 is required for the workbench. Install Python 3 and retry. Starter files were written; workbench was not started.'],
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
const built = buildWorkbench(engine)
|
|
142
|
+
let started = { skipped: true, build: built }
|
|
112
143
|
if (!noOpen && process.env.EASYSOCIABLE_NO_OPEN !== 'true') {
|
|
113
|
-
|
|
114
|
-
spawnSync('open', [`obsidian://open?vault=${encodeURIComponent(vaultName)}`], { stdio: 'ignore' })
|
|
144
|
+
started = await startWorkbench(engine, { noOpen: false })
|
|
115
145
|
}
|
|
116
|
-
return { ...
|
|
146
|
+
return { ...validateRoot(engine), created, workbench, workbenchBuild: built, workbenchStart: started, action: 'applied' }
|
|
117
147
|
}
|
|
118
148
|
|
|
119
149
|
function printHelp() {
|
|
120
|
-
process.stdout.write(`Usage:
|
|
150
|
+
process.stdout.write(`Usage:
|
|
151
|
+
node setup.mjs --detect [--json]
|
|
152
|
+
node setup.mjs --root <parent-or-engine> [--preview|--apply|--validate] [--workbench] [--no-open] [--json]
|
|
153
|
+
|
|
154
|
+
English Content Engine/ only. Existing files are never overwritten.
|
|
155
|
+
`)
|
|
121
156
|
}
|
|
122
157
|
|
|
123
158
|
try {
|
|
@@ -126,29 +161,38 @@ try {
|
|
|
126
161
|
printHelp()
|
|
127
162
|
process.exit(0)
|
|
128
163
|
}
|
|
129
|
-
if (
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
process.exitCode = 2
|
|
164
|
+
if (options.detect) {
|
|
165
|
+
output({ action: 'detect', engines: detectEngines() }, options.json)
|
|
166
|
+
process.exit(0)
|
|
133
167
|
}
|
|
134
|
-
|
|
135
|
-
|
|
168
|
+
const target = options.engine || options.root
|
|
169
|
+
if (!target) {
|
|
170
|
+
output({ code: 'ROOT_REQUIRED', message: 'Choose a parent directory or Content Engine path with --root.', created: false }, options.json)
|
|
136
171
|
process.exitCode = 2
|
|
137
172
|
}
|
|
138
|
-
else {
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
173
|
+
else if (options.apply) {
|
|
174
|
+
const applied = await applyStarter(target, options.noOpen)
|
|
175
|
+
output(applied, options.json)
|
|
176
|
+
if (applied.code) process.exitCode = 2
|
|
177
|
+
}
|
|
178
|
+
else if (options.workbench) {
|
|
179
|
+
const engine = resolveEngine(target)
|
|
180
|
+
if (!isEngine(engine) && !isDir(engine)) {
|
|
181
|
+
output({ code: 'ENGINE_MISSING', message: 'Content Engine/ not found. Run --apply first.', created: false }, options.json)
|
|
142
182
|
process.exitCode = 2
|
|
143
183
|
}
|
|
144
|
-
else if (options.apply) {
|
|
145
|
-
const applied = applyStarter(options.vault, report.locale, options.noOpen)
|
|
146
|
-
output({ ...applied, action: 'applied' }, options.json)
|
|
147
|
-
}
|
|
148
184
|
else {
|
|
149
|
-
|
|
185
|
+
const started = await startWorkbench(engine, { noOpen: options.noOpen })
|
|
186
|
+
output({ action: 'workbench', ...started, engine }, options.json)
|
|
187
|
+
if (!started.ok) process.exitCode = 2
|
|
150
188
|
}
|
|
151
189
|
}
|
|
190
|
+
else {
|
|
191
|
+
const report = validateRoot(target, { strict: options.validate })
|
|
192
|
+
const action = options.validate ? 'validated' : 'preview'
|
|
193
|
+
output({ ...report, action, created: [] }, options.json)
|
|
194
|
+
if (report.code || (options.validate && !report.valid)) process.exitCode = 2
|
|
195
|
+
}
|
|
152
196
|
}
|
|
153
197
|
catch (error) {
|
|
154
198
|
output({ code: 'CONTENT_SETUP_FAILED', message: error instanceof Error ? error.message : String(error), created: false }, true)
|
|
@@ -6,63 +6,74 @@ import { spawnSync } from 'node:child_process'
|
|
|
6
6
|
|
|
7
7
|
const script = new URL('./setup.mjs', import.meta.url).pathname
|
|
8
8
|
const fixture = mkdtempSync(join(tmpdir(), 'easysociable-content-'))
|
|
9
|
-
const
|
|
10
|
-
mkdirSync(
|
|
11
|
-
const env = { ...process.env,
|
|
9
|
+
const root = join(fixture, 'Workspace')
|
|
10
|
+
mkdirSync(root)
|
|
11
|
+
const env = { ...process.env, EASYSOCIABLE_NO_OPEN: 'true' }
|
|
12
12
|
|
|
13
13
|
function run(args) {
|
|
14
14
|
return spawnSync(process.execPath, [script, ...args, '--json'], { encoding: 'utf8', env })
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
try {
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
})
|
|
22
|
-
assert.equal(missingObsidian.status, 2)
|
|
23
|
-
assert.equal(existsSync(join(vault, 'Content Engine')), false)
|
|
24
|
-
|
|
25
|
-
const missingLocale = run(['--vault', vault, '--preview'])
|
|
26
|
-
assert.equal(missingLocale.status, 2)
|
|
27
|
-
assert.equal(JSON.parse(missingLocale.stdout).code, 'LOCALE_REQUIRED')
|
|
18
|
+
const missingRoot = run(['--preview'])
|
|
19
|
+
assert.equal(missingRoot.status, 2)
|
|
20
|
+
assert.equal(JSON.parse(missingRoot.stdout).code, 'ROOT_REQUIRED')
|
|
28
21
|
|
|
29
|
-
const preview = run(['--
|
|
22
|
+
const preview = run(['--root', root, '--preview'])
|
|
30
23
|
assert.equal(preview.status, 0)
|
|
31
24
|
const previewReport = JSON.parse(preview.stdout)
|
|
32
25
|
assert.equal(previewReport.created.length, 0)
|
|
33
26
|
assert.equal(previewReport.valid, true)
|
|
34
|
-
assert.equal(previewReport.
|
|
27
|
+
assert.equal(previewReport.rootName, 'Content Engine')
|
|
35
28
|
|
|
36
|
-
const apply = run(['--
|
|
37
|
-
assert.equal(apply.status, 0)
|
|
29
|
+
const apply = run(['--root', root, '--apply', '--no-open'])
|
|
30
|
+
assert.equal(apply.status, 0, apply.stdout + apply.stderr)
|
|
38
31
|
const applied = JSON.parse(apply.stdout)
|
|
39
32
|
assert.equal(applied.action, 'applied')
|
|
40
|
-
assert.equal(applied.created.length >
|
|
41
|
-
|
|
42
|
-
assert.equal(
|
|
43
|
-
|
|
44
|
-
|
|
33
|
+
assert.equal(applied.created.length > 10, true)
|
|
34
|
+
const engine = join(root, 'Content Engine')
|
|
35
|
+
assert.equal(readFileSync(join(engine, 'CLAUDE.md'), 'utf8').includes('one platform = one Run') || readFileSync(join(engine, 'CLAUDE.md'), 'utf8').includes('One folder = one platform'), true)
|
|
36
|
+
const engineJson = JSON.parse(readFileSync(join(engine, 'engine.json'), 'utf8'))
|
|
37
|
+
assert.equal(engineJson.schemaVersion, 'content-engine.v1')
|
|
38
|
+
assert.equal(engineJson.root, 'Content Engine')
|
|
39
|
+
assert.equal(engineJson.paths.runs, 'runs')
|
|
40
|
+
assert.equal(existsSync(join(engine, 'runs', '_template', 'idea.md')), true)
|
|
41
|
+
assert.equal(existsSync(join(engine, 'runs', '_template', 'run.md')), false)
|
|
42
|
+
assert.equal(existsSync(join(engine, 'platforms', 'ig.md')), true)
|
|
43
|
+
assert.equal(existsSync(join(engine, 'discovery', '_template.md')), true)
|
|
44
|
+
assert.equal(existsSync(join(engine, 'discovery', '_index.md')), true)
|
|
45
|
+
assert.equal(readFileSync(join(engine, 'CLAUDE.md'), 'utf8').includes('§ Discovery'), true)
|
|
46
|
+
assert.equal(existsSync(join(engine, 'workbench', 'build.py')), true)
|
|
47
|
+
assert.equal(existsSync(join(engine, 'workbench', 'template.html')), true)
|
|
48
|
+
assert.equal(existsSync(join(engine, 'workbench', 'index.html')), true)
|
|
49
|
+
const sentinel = join(engine, 'runs', 'user-note.md')
|
|
45
50
|
writeFileSync(sentinel, 'keep me', 'utf8')
|
|
46
|
-
const
|
|
51
|
+
const customTemplate = join(engine, 'workbench', 'template.html')
|
|
52
|
+
const before = readFileSync(customTemplate, 'utf8')
|
|
53
|
+
writeFileSync(customTemplate, `${before}\n<!-- user-custom -->\n`, 'utf8')
|
|
54
|
+
const second = run(['--root', root, '--apply', '--no-open'])
|
|
47
55
|
assert.equal(second.status, 0)
|
|
48
56
|
assert.equal(readFileSync(sentinel, 'utf8'), 'keep me')
|
|
57
|
+
assert.equal(readFileSync(customTemplate, 'utf8').includes('user-custom'), true)
|
|
58
|
+
assert.equal(JSON.parse(second.stdout).created.includes('CLAUDE.md'), false)
|
|
49
59
|
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
60
|
+
const detectRoot = join(fixture, 'DetectRoot')
|
|
61
|
+
const detectEngine = join(detectRoot, 'MyFolder', 'Content Engine')
|
|
62
|
+
mkdirSync(detectEngine, { recursive: true })
|
|
63
|
+
const detect = spawnSync(process.execPath, [script, '--detect', '--json'], {
|
|
64
|
+
encoding: 'utf8',
|
|
65
|
+
env: { ...env, EASYSOCIABLE_ENGINE_SEARCH_ROOTS: detectRoot },
|
|
66
|
+
})
|
|
67
|
+
assert.equal(detect.status, 0, detect.stdout + detect.stderr)
|
|
68
|
+
const detectReport = JSON.parse(detect.stdout)
|
|
69
|
+
assert.equal(detectReport.action, 'detect')
|
|
70
|
+
assert.equal(detectReport.engines.includes(detectEngine), true)
|
|
54
71
|
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
assert.equal(existsSync(join(vault, 'Content Engine')), true)
|
|
61
|
-
assert.equal(readFileSync(join(zhEngine, 'CLAUDE.md'), 'utf8').includes('内容引擎'), true)
|
|
62
|
-
assert.equal(existsSync(join(zhEngine, '成稿', '_索引.md')), true)
|
|
63
|
-
assert.equal(existsSync(join(zhEngine, '选题', '观察清单.md')), true)
|
|
64
|
-
assert.equal(JSON.parse(readFileSync(join(zhEngine, 'locale.json'), 'utf8')).locale, 'zh')
|
|
65
|
-
assert.equal(existsSync(join(zhVault, 'Content Engine')), false)
|
|
72
|
+
const validateOk = run(['--root', root, '--validate'])
|
|
73
|
+
assert.equal(validateOk.status, 0)
|
|
74
|
+
rmSync(join(engine, 'engine.json'))
|
|
75
|
+
const validateMissing = run(['--root', root, '--validate'])
|
|
76
|
+
assert.equal(validateMissing.status, 2)
|
|
66
77
|
|
|
67
78
|
console.log('Content Skill setup smoke passed.')
|
|
68
79
|
}
|
|
@@ -1,53 +1,55 @@
|
|
|
1
|
-
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs'
|
|
2
|
+
import { join, relative } from 'node:path'
|
|
3
|
+
import { fileURLToPath } from 'node:url'
|
|
2
4
|
|
|
3
|
-
export const
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
'平台/threads.md': '# Threads\n\n在此保存 Threads 平台的文案和约束。\n',
|
|
44
|
-
'平台/tiktok.md': '# TikTok\n\n在此保存 TikTok 平台的文案和约束。\n',
|
|
45
|
-
},
|
|
5
|
+
export const ENGINE_ROOT = 'Content Engine'
|
|
6
|
+
export const STARTER_DIR = fileURLToPath(new URL('../starter/', import.meta.url))
|
|
7
|
+
export const WORKBENCH_SRC = fileURLToPath(new URL('../workbench/', import.meta.url))
|
|
8
|
+
export const WORKBENCH_FILES = ['template.html', 'build.py', 'serve.py']
|
|
9
|
+
|
|
10
|
+
export const ENGINE_PATHS = {
|
|
11
|
+
runs: 'runs',
|
|
12
|
+
exports: 'exports',
|
|
13
|
+
topics: 'topics',
|
|
14
|
+
discovery: 'discovery',
|
|
15
|
+
swipe: 'swipe',
|
|
16
|
+
profiles: 'profiles',
|
|
17
|
+
inbox: 'inbox',
|
|
18
|
+
accounts: 'accounts',
|
|
19
|
+
raw: 'raw',
|
|
20
|
+
published: 'published',
|
|
21
|
+
wiki: 'wiki',
|
|
22
|
+
platforms: 'platforms',
|
|
23
|
+
workbench: 'workbench',
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function engineJson() {
|
|
27
|
+
return `${JSON.stringify({
|
|
28
|
+
schemaVersion: 'content-engine.v1',
|
|
29
|
+
locale: 'en',
|
|
30
|
+
root: ENGINE_ROOT,
|
|
31
|
+
paths: ENGINE_PATHS,
|
|
32
|
+
}, null, 2)}\n`
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function walkFiles(dir) {
|
|
36
|
+
const out = []
|
|
37
|
+
if (!existsSync(dir)) return out
|
|
38
|
+
for (const name of readdirSync(dir)) {
|
|
39
|
+
if (name === '.DS_Store') continue
|
|
40
|
+
const path = join(dir, name)
|
|
41
|
+
if (statSync(path).isDirectory()) out.push(...walkFiles(path))
|
|
42
|
+
else out.push(path)
|
|
43
|
+
}
|
|
44
|
+
return out
|
|
46
45
|
}
|
|
47
46
|
|
|
48
|
-
export function filesFor(
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
47
|
+
export function filesFor() {
|
|
48
|
+
const files = {}
|
|
49
|
+
for (const path of walkFiles(STARTER_DIR)) {
|
|
50
|
+
const relativePath = relative(STARTER_DIR, path).split('\\').join('/')
|
|
51
|
+
files[relativePath] = readFileSync(path, 'utf8')
|
|
52
52
|
}
|
|
53
|
+
files['engine.json'] = engineJson()
|
|
54
|
+
return files
|
|
53
55
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { resolve } from 'node:path'
|
|
4
|
+
import { validateExportDirectory } from './export-contracts.mjs'
|
|
5
|
+
|
|
6
|
+
function parseArgs(argv) {
|
|
7
|
+
const options = { exportDir: '', expectedPages: undefined, json: false }
|
|
8
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
9
|
+
const arg = argv[i]
|
|
10
|
+
if (arg === '--export') options.exportDir = argv[++i] ?? ''
|
|
11
|
+
else if (arg === '--expected-pages') options.expectedPages = Number(argv[++i])
|
|
12
|
+
else if (arg === '--json') options.json = true
|
|
13
|
+
else if (arg === '--help' || arg === '-h') options.help = true
|
|
14
|
+
else throw new Error(`Unknown argument: ${arg}`)
|
|
15
|
+
}
|
|
16
|
+
return options
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function output(value, json) {
|
|
20
|
+
process.stdout.write(json ? `${JSON.stringify(value, null, 2)}\n` : `${value.message ?? value}\n`)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
const options = parseArgs(process.argv.slice(2))
|
|
25
|
+
if (options.help) {
|
|
26
|
+
output({ usage: 'node validate-export.mjs --export <export-dir> [--expected-pages N] [--json]' }, false)
|
|
27
|
+
process.exit(0)
|
|
28
|
+
}
|
|
29
|
+
if (!options.exportDir) {
|
|
30
|
+
output({ code: 'EXPORT_REQUIRED', message: 'Pass --export <export-dir>.', valid: false }, options.json)
|
|
31
|
+
process.exitCode = 2
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
const report = validateExportDirectory(resolve(options.exportDir), { expectedPages: options.expectedPages })
|
|
35
|
+
output({ ...report, action: 'validated', export: resolve(options.exportDir) }, options.json)
|
|
36
|
+
if (!report.valid) process.exitCode = 2
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
output({ code: 'EXPORT_VALIDATE_FAILED', message: error instanceof Error ? error.message : String(error), valid: false }, true)
|
|
41
|
+
process.exitCode = 1
|
|
42
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { readFileSync } from 'node:fs'
|
|
4
|
+
import { resolve } from 'node:path'
|
|
5
|
+
import { validateContentPack } from './contracts.mjs'
|
|
6
|
+
|
|
7
|
+
function parseArgs(argv) {
|
|
8
|
+
const options = { file: '', platform: '', requirePageTemplateId: false, json: false }
|
|
9
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
10
|
+
const arg = argv[i]
|
|
11
|
+
if (arg === '--file') options.file = argv[++i] ?? ''
|
|
12
|
+
else if (arg === '--platform') options.platform = argv[++i] ?? ''
|
|
13
|
+
else if (arg === '--require-page-template-id') options.requirePageTemplateId = true
|
|
14
|
+
else if (arg === '--json') options.json = true
|
|
15
|
+
else if (arg === '--help' || arg === '-h') options.help = true
|
|
16
|
+
else throw new Error(`Unknown argument: ${arg}`)
|
|
17
|
+
}
|
|
18
|
+
return options
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function output(value, json) {
|
|
22
|
+
process.stdout.write(json ? `${JSON.stringify(value, null, 2)}\n` : `${value.message ?? value}\n`)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
const options = parseArgs(process.argv.slice(2))
|
|
27
|
+
if (options.help) {
|
|
28
|
+
output({ usage: 'node validate-pack.mjs --file <pack.json> [--platform tiktok] [--require-page-template-id] [--json]' }, false)
|
|
29
|
+
process.exit(0)
|
|
30
|
+
}
|
|
31
|
+
if (!options.file) {
|
|
32
|
+
output({ code: 'FILE_REQUIRED', message: 'Pass --file <pack.json>.', valid: false }, options.json)
|
|
33
|
+
process.exitCode = 2
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
const pack = JSON.parse(readFileSync(resolve(options.file), 'utf8'))
|
|
37
|
+
const report = validateContentPack(pack, {
|
|
38
|
+
requirePageTemplateId: options.requirePageTemplateId,
|
|
39
|
+
platform: options.platform || undefined,
|
|
40
|
+
})
|
|
41
|
+
output({ ...report, action: 'validated', file: resolve(options.file) }, options.json)
|
|
42
|
+
if (!report.valid) process.exitCode = 2
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
output({ code: 'PACK_VALIDATE_FAILED', message: error instanceof Error ? error.message : String(error), valid: false }, true)
|
|
47
|
+
process.exitCode = 1
|
|
48
|
+
}
|