contentrain 0.16.1 → 0.17.1
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 +8 -1
- package/dist/{ide-D1J4iAhK.mjs → ide-zPN-87R5.mjs} +23 -20
- package/dist/{import-D9TtmPgM.mjs → import-DXA71z9x.mjs} +88 -9
- package/dist/index.mjs +5 -5
- package/dist/{init-Bp5aMELm.mjs → init-CqQgYoJq.mjs} +1 -1
- package/dist/{setup-BRVyTOxY.mjs → setup-aaeHJ90i.mjs} +1 -1
- package/dist/{skills-DZT0pbmO.mjs → skills-ix03X2Rj.mjs} +8 -2
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -332,11 +332,18 @@ Import a WordPress site into a `.contentrain` content store:
|
|
|
332
332
|
```bash
|
|
333
333
|
contentrain import export.xml # WXR export file (highest offline fidelity)
|
|
334
334
|
contentrain import https://site.example # public REST
|
|
335
|
-
contentrain import https://site.example --auth
|
|
335
|
+
contentrain import https://site.example --auth editor # authenticated REST: prompts for the Application Password
|
|
336
|
+
CONTENTRAIN_WP_APP_PASSWORD=… contentrain import https://site.example --auth editor
|
|
337
|
+
CONTENTRAIN_WP_AUTH=editor:… contentrain import https://site.example
|
|
338
|
+
contentrain import export.xml --include-emails # also write author/commenter e-mails
|
|
336
339
|
```
|
|
337
340
|
|
|
338
341
|
Writes the canonical store plus `import-report.json`, `entry-source-map.json` (WP id → entry address), and — when the source has comments — `comments-export.json` (`contentrain-comments@1`, ready for a comments-service intake). An existing `.contentrain` is only overwritten with `--force`. Powered by `@contentrain/wp-import`.
|
|
339
342
|
|
|
343
|
+
**Personal data.** The store holds author names and logins, and commenters' names, website URLs and comment text. A WXR export also carries author and commenter e-mail addresses; they are left out of the store and of `comments-export.json` unless you pass `--include-emails`. Commenter IPs, user agents and avatar URLs (including Akismet's comment meta) are never written. The e-mails stay out by default because the store usually ends up in git (sometimes a public repo). The REST path never sees them.
|
|
344
|
+
|
|
345
|
+
**Credentials.** The Application Password is read from `CONTENTRAIN_WP_APP_PASSWORD` (with `--auth <user>`), from `CONTENTRAIN_WP_AUTH=user:password`, or from a hidden prompt. `--auth user:password` still works but is deprecated and warns: argv lands in shell history and process listings.
|
|
346
|
+
|
|
340
347
|
### `contentrain delta`
|
|
341
348
|
|
|
342
349
|
Plan what a later WordPress change means for a store that was already delivered:
|
|
@@ -1,25 +1,24 @@
|
|
|
1
1
|
import { appendFile, readFile, writeFile } from "node:fs/promises";
|
|
2
|
-
import { join } from "node:path";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
3
|
import { ensureDir, pathExists } from "@contentrain/mcp/util/fs";
|
|
4
4
|
import { createGit } from "@contentrain/mcp/git/identity";
|
|
5
5
|
//#region src/utils/ide.ts
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
"
|
|
17
|
-
"contentrain
|
|
18
|
-
|
|
19
|
-
"
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
];
|
|
6
|
+
/**
|
|
7
|
+
* Every skill the installed `@contentrain/skills` ships: the directories under
|
|
8
|
+
* its `skills/` that hold a `SKILL.md`, sorted. Read from disk rather than kept
|
|
9
|
+
* as a second hand-written list, which is how `contentrain skills` came to
|
|
10
|
+
* install 15 of 16 (`contentrain-migrate-wordpress` was never added).
|
|
11
|
+
*
|
|
12
|
+
* The package exports `./skills/*` only, so the directory is found through one
|
|
13
|
+
* known file: the core `contentrain` skill.
|
|
14
|
+
*/
|
|
15
|
+
async function listAgentSkillNames(resolveSkillFile) {
|
|
16
|
+
const { readdir } = await import("node:fs/promises");
|
|
17
|
+
const skillsRoot = dirname(dirname(resolveSkillFile("skills/contentrain/SKILL.md")));
|
|
18
|
+
const dirs = (await readdir(skillsRoot, { withFileTypes: true })).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
19
|
+
const hasSkill = await Promise.all(dirs.map((d) => pathExists(join(skillsRoot, d, "SKILL.md"))));
|
|
20
|
+
return dirs.filter((_, i) => hasSkill[i]).toSorted();
|
|
21
|
+
}
|
|
23
22
|
const OLD_RULE_FILES = [
|
|
24
23
|
"contentrain-content-quality.md",
|
|
25
24
|
"contentrain-seo-rules.md",
|
|
@@ -223,7 +222,11 @@ async function installIdeRulesAndSkills(projectRoot, ide, resolveRuleFile, resol
|
|
|
223
222
|
if (resolveSkillFile) {
|
|
224
223
|
const skillsDir = join(projectRoot, ide.skillsDir);
|
|
225
224
|
await ensureDir(skillsDir);
|
|
226
|
-
|
|
225
|
+
let skillNames = [];
|
|
226
|
+
try {
|
|
227
|
+
skillNames = await listAgentSkillNames(resolveSkillFile);
|
|
228
|
+
} catch {}
|
|
229
|
+
for (const skillName of skillNames) {
|
|
227
230
|
const skillDir = join(skillsDir, skillName);
|
|
228
231
|
const skillMd = join(skillDir, "SKILL.md");
|
|
229
232
|
try {
|
|
@@ -294,4 +297,4 @@ async function addClaudeMdReference(projectRoot) {
|
|
|
294
297
|
}
|
|
295
298
|
}
|
|
296
299
|
//#endregion
|
|
297
|
-
export {
|
|
300
|
+
export { detectIdes as a, writeMcpConfig as c, createPackageResolver as i, MCP_CONFIGS as n, installIdeRulesAndSkills as o, addClaudeMdReference as r, listAgentSkillNames as s, IDE_CONFIGS as t };
|
|
@@ -1,10 +1,64 @@
|
|
|
1
1
|
import { i as pc } from "./ui-B8l_LC36.mjs";
|
|
2
2
|
import { defineCommand } from "citty";
|
|
3
|
-
import { intro, log, outro, spinner } from "@clack/prompts";
|
|
3
|
+
import { intro, isCancel, log, outro, password, spinner } from "@clack/prompts";
|
|
4
4
|
import { createReadStream, existsSync } from "node:fs";
|
|
5
5
|
import { access, mkdir, writeFile } from "node:fs/promises";
|
|
6
6
|
import { dirname, join, resolve } from "node:path";
|
|
7
7
|
//#region src/commands/import.ts
|
|
8
|
+
/** `user:password` for REST, read from the environment so it stays out of argv and shell history. */
|
|
9
|
+
const WP_AUTH_ENV = "CONTENTRAIN_WP_AUTH";
|
|
10
|
+
/** The Application Password alone, for `--auth <user>`. */
|
|
11
|
+
const WP_APP_PASSWORD_ENV = "CONTENTRAIN_WP_APP_PASSWORD";
|
|
12
|
+
/** `user:password` → parts; the password may itself contain colons. */
|
|
13
|
+
const splitUserPassword = (value) => {
|
|
14
|
+
const i = value.indexOf(":");
|
|
15
|
+
if (i <= 0 || i === value.length - 1) return void 0;
|
|
16
|
+
return {
|
|
17
|
+
user: value.slice(0, i),
|
|
18
|
+
appPassword: value.slice(i + 1)
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Where the REST credential comes from, in order:
|
|
23
|
+
* 1. `--auth user:password` — still works, but argv lands in shell history and
|
|
24
|
+
* process listings, so the caller prints a deprecation warning.
|
|
25
|
+
* 2. `--auth user` — the password from `CONTENTRAIN_WP_APP_PASSWORD`, else an
|
|
26
|
+
* interactive hidden prompt (only when `interactive`).
|
|
27
|
+
* 3. `CONTENTRAIN_WP_AUTH=user:password` in the environment.
|
|
28
|
+
* Nothing given → anonymous REST.
|
|
29
|
+
*/
|
|
30
|
+
async function resolveRestAuth(input) {
|
|
31
|
+
const flag = input.auth?.trim();
|
|
32
|
+
if (flag) {
|
|
33
|
+
if (flag.includes(":")) {
|
|
34
|
+
const auth = splitUserPassword(flag);
|
|
35
|
+
return auth ? {
|
|
36
|
+
auth,
|
|
37
|
+
deprecatedArgv: true
|
|
38
|
+
} : { error: "--auth expects a user name (the password comes from the environment or a prompt)" };
|
|
39
|
+
}
|
|
40
|
+
const fromEnv = input.env[WP_APP_PASSWORD_ENV];
|
|
41
|
+
if (fromEnv) return { auth: {
|
|
42
|
+
user: flag,
|
|
43
|
+
appPassword: fromEnv
|
|
44
|
+
} };
|
|
45
|
+
if (input.interactive && input.prompt) {
|
|
46
|
+
const pw = await input.prompt(flag);
|
|
47
|
+
if (pw) return { auth: {
|
|
48
|
+
user: flag,
|
|
49
|
+
appPassword: pw
|
|
50
|
+
} };
|
|
51
|
+
return { error: "No Application Password entered" };
|
|
52
|
+
}
|
|
53
|
+
return { error: `--auth ${flag} needs the Application Password in ${WP_APP_PASSWORD_ENV} (or run in a terminal to be prompted)` };
|
|
54
|
+
}
|
|
55
|
+
const envAuth = input.env[WP_AUTH_ENV];
|
|
56
|
+
if (envAuth) {
|
|
57
|
+
const auth = splitUserPassword(envAuth);
|
|
58
|
+
return auth ? { auth } : { error: `${WP_AUTH_ENV} must be user:password` };
|
|
59
|
+
}
|
|
60
|
+
return {};
|
|
61
|
+
}
|
|
8
62
|
var import_default = defineCommand({
|
|
9
63
|
meta: {
|
|
10
64
|
name: "import",
|
|
@@ -23,7 +77,12 @@ var import_default = defineCommand({
|
|
|
23
77
|
},
|
|
24
78
|
auth: {
|
|
25
79
|
type: "string",
|
|
26
|
-
description:
|
|
80
|
+
description: `REST user for an Application Password (lifts access to rest_auth). The password comes from ${WP_APP_PASSWORD_ENV} or a prompt; ${WP_AUTH_ENV}=user:password also works. user:password here is deprecated`,
|
|
81
|
+
required: false
|
|
82
|
+
},
|
|
83
|
+
"include-emails": {
|
|
84
|
+
type: "boolean",
|
|
85
|
+
description: "Write author and commenter e-mail addresses from a WXR export into the store (default: left out — they are personal data and the store usually lands in git)",
|
|
27
86
|
required: false
|
|
28
87
|
},
|
|
29
88
|
force: {
|
|
@@ -58,11 +117,29 @@ var import_default = defineCommand({
|
|
|
58
117
|
let raw;
|
|
59
118
|
const warnings = [];
|
|
60
119
|
if (isUrl) {
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
120
|
+
const resolved = await resolveRestAuth({
|
|
121
|
+
auth: args.auth,
|
|
122
|
+
env: process.env,
|
|
123
|
+
interactive: !args.json && Boolean(process.stdin.isTTY),
|
|
124
|
+
prompt: async (user) => {
|
|
125
|
+
s?.stop("Credential needed");
|
|
126
|
+
const value = await password({ message: `Application Password for ${user}` });
|
|
127
|
+
s?.start(`Fetching ${args.source} over REST`);
|
|
128
|
+
return isCancel(value) ? void 0 : value;
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
if (resolved.error) {
|
|
132
|
+
s?.stop("Credential missing");
|
|
133
|
+
log.error(resolved.error);
|
|
134
|
+
process.exitCode = 1;
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
if (resolved.deprecatedArgv) {
|
|
138
|
+
const msg = `--auth user:password is deprecated: the password lands in shell history and process listings. Use --auth <user> with ${WP_APP_PASSWORD_ENV} (or the prompt), or ${WP_AUTH_ENV}=user:password`;
|
|
139
|
+
if (args.json) process.stderr.write(`warning: ${msg}\n`);
|
|
140
|
+
else log.warning(msg);
|
|
141
|
+
}
|
|
142
|
+
const auth = resolved.auth;
|
|
66
143
|
const result = await fetchRestRawIR({
|
|
67
144
|
origin: args.source,
|
|
68
145
|
auth,
|
|
@@ -73,9 +150,10 @@ var import_default = defineCommand({
|
|
|
73
150
|
} else raw = (await parseWxr(createReadStream(args.source), { tool: "contentrain-cli" })).raw;
|
|
74
151
|
s?.stop(`Source read: ${raw.posts.length} posts, ${raw.attachments.length} media, ${raw.comments?.length ?? 0} comments (${raw.provenance.kind})`);
|
|
75
152
|
s?.start("Converting to .contentrain");
|
|
76
|
-
const
|
|
153
|
+
const includeEmails = Boolean(args["include-emails"]);
|
|
154
|
+
const { files, entry_source_map, report } = rawToContentrain(raw, { includeEmails });
|
|
77
155
|
if (raw.comments?.length) {
|
|
78
|
-
const exp = buildCommentsExport(raw, entry_source_map);
|
|
156
|
+
const exp = buildCommentsExport(raw, entry_source_map, { includeEmails });
|
|
79
157
|
files["comments-export.json"] = `${JSON.stringify(exp, null, 2)}\n`;
|
|
80
158
|
const summary = summarizeComments(exp);
|
|
81
159
|
if (summary.unresolved?.length) warnings.push(`${summary.unresolved.length} comments reference posts outside the import`);
|
|
@@ -99,6 +177,7 @@ var import_default = defineCommand({
|
|
|
99
177
|
if (report.password_protected_drafts) log.warning(`${report.password_protected_drafts} password-protected posts brought in as drafts — their body is the protected text; publish one only after deciding what it should show`);
|
|
100
178
|
if (report.dropped_relations) log.warning(`${report.dropped_relations} relations pointed outside the import and were dropped (details in import-report.json)`);
|
|
101
179
|
for (const w of warnings) log.warning(w);
|
|
180
|
+
if (includeEmails && raw.provenance.kind === "wxr") log.warning("Author and commenter e-mail addresses were written into the store (--include-emails). They are personal data: keep this repository private");
|
|
102
181
|
if (raw.comments?.length) log.info(`comments-export.json written (contentrain-comments@1) — ready for a comments-service intake`);
|
|
103
182
|
outro(`Done. Next: ${pc.bold("contentrain validate")} to check the imported store.`);
|
|
104
183
|
}
|
package/dist/index.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { i as pc } from "./ui-B8l_LC36.mjs";
|
|
3
3
|
import { defineCommand, runMain } from "citty";
|
|
4
4
|
//#region package.json
|
|
5
|
-
var version = "0.
|
|
5
|
+
var version = "0.17.1";
|
|
6
6
|
//#endregion
|
|
7
7
|
//#region src/utils/debug.ts
|
|
8
8
|
/**
|
|
@@ -43,8 +43,8 @@ runMain(defineCommand({
|
|
|
43
43
|
description: "Contentrain CLI — AI content governance infrastructure"
|
|
44
44
|
},
|
|
45
45
|
subCommands: {
|
|
46
|
-
init: () => import("./init-
|
|
47
|
-
import: () => import("./import-
|
|
46
|
+
init: () => import("./init-CqQgYoJq.mjs").then((m) => m.default),
|
|
47
|
+
import: () => import("./import-DXA71z9x.mjs").then((m) => m.default),
|
|
48
48
|
delta: () => import("./delta-D6Cp9BdP.mjs").then((m) => m.default),
|
|
49
49
|
status: () => import("./status-DSzoldCi.mjs").then((m) => m.default),
|
|
50
50
|
doctor: () => import("./doctor-B9MJgRO5.mjs").then((m) => m.default),
|
|
@@ -58,8 +58,8 @@ runMain(defineCommand({
|
|
|
58
58
|
describe: () => import("./describe-C6-AWlKx.mjs").then((m) => m.default),
|
|
59
59
|
"describe-format": () => import("./describe-format-CR0-NMR8.mjs").then((m) => m.default),
|
|
60
60
|
scaffold: () => import("./scaffold-DX6eJ0n1.mjs").then((m) => m.default),
|
|
61
|
-
setup: () => import("./setup-
|
|
62
|
-
skills: () => import("./skills-
|
|
61
|
+
setup: () => import("./setup-aaeHJ90i.mjs").then((m) => m.default),
|
|
62
|
+
skills: () => import("./skills-ix03X2Rj.mjs").then((m) => m.default),
|
|
63
63
|
studio: () => import("./studio-CU0fTK5f.mjs").then((m) => m.default)
|
|
64
64
|
}
|
|
65
65
|
}));
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { i as pc } from "./ui-B8l_LC36.mjs";
|
|
2
2
|
import { r as resolveProjectRoot, t as loadProjectContext } from "./context-SIGQmozp.mjs";
|
|
3
|
-
import { a as
|
|
3
|
+
import { a as detectIdes, c as writeMcpConfig, i as createPackageResolver, o as installIdeRulesAndSkills, r as addClaudeMdReference, t as IDE_CONFIGS } from "./ide-zPN-87R5.mjs";
|
|
4
4
|
import { defineCommand } from "citty";
|
|
5
5
|
import { confirm, intro, isCancel, log, multiselect, outro, select, spinner } from "@clack/prompts";
|
|
6
6
|
import { appendFile, readFile, writeFile } from "node:fs/promises";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { i as pc } from "./ui-B8l_LC36.mjs";
|
|
2
2
|
import { r as resolveProjectRoot, t as loadProjectContext } from "./context-SIGQmozp.mjs";
|
|
3
|
-
import { a as
|
|
3
|
+
import { a as detectIdes, c as writeMcpConfig, i as createPackageResolver, n as MCP_CONFIGS, o as installIdeRulesAndSkills, t as IDE_CONFIGS } from "./ide-zPN-87R5.mjs";
|
|
4
4
|
import { defineCommand } from "citty";
|
|
5
5
|
import { intro, log, outro, spinner } from "@clack/prompts";
|
|
6
6
|
//#region src/commands/setup.ts
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { i as pc } from "./ui-B8l_LC36.mjs";
|
|
2
2
|
import { r as resolveProjectRoot } from "./context-SIGQmozp.mjs";
|
|
3
|
-
import { a as
|
|
3
|
+
import { a as detectIdes, i as createPackageResolver, o as installIdeRulesAndSkills, s as listAgentSkillNames, t as IDE_CONFIGS } from "./ide-zPN-87R5.mjs";
|
|
4
4
|
import { defineCommand } from "citty";
|
|
5
5
|
import { intro, log, outro, spinner } from "@clack/prompts";
|
|
6
6
|
import { join } from "node:path";
|
|
@@ -72,12 +72,18 @@ var skills_default = defineCommand({
|
|
|
72
72
|
});
|
|
73
73
|
async function listInstalledSkills(projectRoot) {
|
|
74
74
|
let found = false;
|
|
75
|
+
const resolveSkillFile = await createPackageResolver("@contentrain/skills", projectRoot);
|
|
76
|
+
let skillNames = [];
|
|
77
|
+
if (resolveSkillFile) try {
|
|
78
|
+
skillNames = await listAgentSkillNames(resolveSkillFile);
|
|
79
|
+
} catch {}
|
|
80
|
+
if (skillNames.length === 0) log.warning("@contentrain/skills package not found — cannot tell which skills are missing");
|
|
75
81
|
for (const [, ide] of Object.entries(IDE_CONFIGS)) {
|
|
76
82
|
const skillsDir = join(projectRoot, ide.skillsDir);
|
|
77
83
|
if (!await pathExists(skillsDir)) continue;
|
|
78
84
|
found = true;
|
|
79
85
|
log.info(`${pc.bold(ide.name)} (${ide.skillsDir}/):`);
|
|
80
|
-
for (const skillName of
|
|
86
|
+
for (const skillName of skillNames) if (await pathExists(join(skillsDir, skillName, "SKILL.md"))) log.message(` ${pc.green("✓")} ${skillName}`);
|
|
81
87
|
else log.message(` ${pc.red("✗")} ${skillName} ${pc.dim("(missing)")}`);
|
|
82
88
|
if (await pathExists(join(projectRoot, ide.rulesDir, ide.guardrailsFileName))) log.message(` ${pc.green("✓")} essential rules`);
|
|
83
89
|
else log.message(` ${pc.red("✗")} essential rules ${pc.dim("(missing)")}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "contentrain",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.1",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "CLI for Contentrain — AI content governance infrastructure",
|
|
6
6
|
"type": "module",
|
|
@@ -50,9 +50,9 @@
|
|
|
50
50
|
"@contentrain/mcp": "3.10.1",
|
|
51
51
|
"@contentrain/query": "7.6.42",
|
|
52
52
|
"@contentrain/rules": "0.7.3",
|
|
53
|
-
"@contentrain/skills": "0.9.
|
|
53
|
+
"@contentrain/skills": "0.9.6",
|
|
54
54
|
"@contentrain/types": "1.62.0",
|
|
55
|
-
"@contentrain/wp-import": "0.
|
|
55
|
+
"@contentrain/wp-import": "0.13.1"
|
|
56
56
|
},
|
|
57
57
|
"devDependencies": {
|
|
58
58
|
"@types/ws": "^8.5.0",
|