contentrain 0.3.2 → 0.3.4

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 CHANGED
@@ -2,9 +2,16 @@
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/contentrain?label=contentrain)](https://www.npmjs.com/package/contentrain)
4
4
  [![GitHub source](https://img.shields.io/badge/source-Contentrain%2Fai-181717?logo=github)](https://github.com/Contentrain/ai/tree/main/packages/cli)
5
+ [![Docs](https://img.shields.io/badge/docs-ai.contentrain.io-0f172a)](https://ai.contentrain.io/packages/cli)
5
6
 
6
7
  CLI for Contentrain.
7
8
 
9
+ Start here:
10
+
11
+ - [2-minute product demo](https://ai.contentrain.io/demo)
12
+ - [CLI docs](https://ai.contentrain.io/packages/cli)
13
+ - [Getting started](https://ai.contentrain.io/getting-started)
14
+
8
15
  `contentrain` is the local operations surface for a Contentrain project:
9
16
 
10
17
  - initialize `.contentrain/` in an existing repo
@@ -51,6 +58,17 @@ Requirements:
51
58
  | `contentrain generate` | Generate `.contentrain/client/` and `#contentrain` package imports |
52
59
  | `contentrain diff` | Review and merge or reject pending `contentrain/*` branches |
53
60
  | `contentrain serve` | Start the local review UI or the MCP stdio server |
61
+ | `contentrain studio login` | Authenticate with Contentrain Studio |
62
+ | `contentrain studio logout` | Log out from Studio |
63
+ | `contentrain studio whoami` | Show current authentication status |
64
+ | `contentrain studio status` | Show project overview from Studio |
65
+ | `contentrain studio activity` | Show recent activity feed |
66
+ | `contentrain studio usage` | Show workspace usage metrics |
67
+ | `contentrain studio branches` | Manage remote content branches |
68
+ | `contentrain studio cdn-init` | Set up CDN for content delivery |
69
+ | `contentrain studio cdn-build` | Trigger a CDN rebuild |
70
+ | `contentrain studio webhooks` | Manage webhooks |
71
+ | `contentrain studio submissions` | Manage form submissions |
54
72
 
55
73
  ## 🔄 Typical Flow
56
74
 
@@ -150,6 +168,42 @@ to understand:
150
168
  - whether branch health is blocking new writes
151
169
  - what changed before merging or deleting a branch
152
170
 
171
+ ## 🔗 Studio Integration
172
+
173
+ The `studio` command group connects the CLI to [Contentrain Studio](https://studio.contentrain.io) for enterprise workflows.
174
+
175
+ Authenticate:
176
+
177
+ ```bash
178
+ contentrain studio login
179
+ contentrain studio whoami
180
+ ```
181
+
182
+ Set up CDN for content delivery:
183
+
184
+ ```bash
185
+ contentrain studio cdn-init
186
+ contentrain studio cdn-build --wait
187
+ ```
188
+
189
+ Monitor project activity and usage:
190
+
191
+ ```bash
192
+ contentrain studio status
193
+ contentrain studio activity --limit 10
194
+ contentrain studio usage
195
+ ```
196
+
197
+ Manage branches, webhooks, and form submissions:
198
+
199
+ ```bash
200
+ contentrain studio branches
201
+ contentrain studio webhooks
202
+ contentrain studio submissions --form contact-form
203
+ ```
204
+
205
+ Credentials are stored securely in `~/.contentrain/credentials.json` with `0o600` permissions. Use `CONTENTRAIN_STUDIO_TOKEN` environment variable for CI/CD.
206
+
153
207
  ## 🤖 IDE Rules
154
208
 
155
209
  `contentrain init` installs project-level AI rules automatically:
@@ -0,0 +1,88 @@
1
+ import { i as pc } from "./ui-B5mXontH.mjs";
2
+ import { n as resolveStudioClient } from "./client-D4xW8aLz.mjs";
3
+ import { t as resolveStudioContext } from "./resolve-context-BMV7JwCw.mjs";
4
+ import { defineCommand } from "citty";
5
+ import { intro, log, outro, spinner } from "@clack/prompts";
6
+ //#region src/studio/commands/activity.ts
7
+ var activity_default = defineCommand({
8
+ meta: {
9
+ name: "activity",
10
+ description: "Show recent activity from Studio"
11
+ },
12
+ args: {
13
+ workspace: {
14
+ type: "string",
15
+ description: "Workspace ID",
16
+ required: false
17
+ },
18
+ project: {
19
+ type: "string",
20
+ description: "Project ID",
21
+ required: false
22
+ },
23
+ limit: {
24
+ type: "string",
25
+ description: "Number of entries (default 20)",
26
+ required: false
27
+ },
28
+ json: {
29
+ type: "boolean",
30
+ description: "JSON output",
31
+ required: false
32
+ }
33
+ },
34
+ async run({ args }) {
35
+ if (!args.json) intro(pc.bold("contentrain studio activity"));
36
+ try {
37
+ const client = await resolveStudioClient();
38
+ const ctx = await resolveStudioContext(client, args);
39
+ if (!ctx) {
40
+ log.warning("No workspace or project found.");
41
+ outro("");
42
+ return;
43
+ }
44
+ const s = args.json ? null : spinner();
45
+ s?.start("Fetching activity...");
46
+ const result = await client.getActivity(ctx.workspaceId, ctx.projectId, { limit: args.limit ?? "20" });
47
+ s?.stop(`${result.data.length} entries`);
48
+ if (args.json) {
49
+ process.stdout.write(JSON.stringify(result, null, 2));
50
+ return;
51
+ }
52
+ if (result.data.length === 0) {
53
+ log.info("No recent activity.");
54
+ outro("");
55
+ return;
56
+ }
57
+ for (const entry of result.data) {
58
+ const time = pc.dim(timeAgo(entry.createdAt).padEnd(10));
59
+ const actor = pc.cyan(entry.actor.padEnd(24));
60
+ const action = actionColor(entry.action);
61
+ const details = entry.details ? pc.dim(` ${entry.details}`) : "";
62
+ log.message(` ${time} ${actor} ${action}${details}`);
63
+ }
64
+ if (result.total > result.data.length) log.message(pc.dim(`\n Showing ${result.data.length} of ${result.total} entries. Use --limit to see more.`));
65
+ } catch (error) {
66
+ log.error(error instanceof Error ? error.message : String(error));
67
+ process.exitCode = 1;
68
+ }
69
+ if (!args.json) outro("");
70
+ }
71
+ });
72
+ function timeAgo(dateStr) {
73
+ const diff = Date.now() - new Date(dateStr).getTime();
74
+ const minutes = Math.floor(diff / 6e4);
75
+ if (minutes < 1) return "just now";
76
+ if (minutes < 60) return `${minutes}m ago`;
77
+ const hours = Math.floor(minutes / 60);
78
+ if (hours < 24) return `${hours}h ago`;
79
+ return `${Math.floor(hours / 24)}d ago`;
80
+ }
81
+ function actionColor(action) {
82
+ if (action.includes("saved") || action.includes("merged")) return pc.green(action);
83
+ if (action.includes("deleted") || action.includes("rejected")) return pc.red(action);
84
+ if (action.includes("build")) return pc.yellow(action);
85
+ return action;
86
+ }
87
+ //#endregion
88
+ export { activity_default as default };
@@ -0,0 +1,142 @@
1
+ import { i as pc, r as formatTable, t as formatCount } from "./ui-B5mXontH.mjs";
2
+ import { n as resolveStudioClient } from "./client-D4xW8aLz.mjs";
3
+ import { t as resolveStudioContext } from "./resolve-context-BMV7JwCw.mjs";
4
+ import { defineCommand } from "citty";
5
+ import { confirm, intro, isCancel, log, outro, select, spinner } from "@clack/prompts";
6
+ //#region src/studio/commands/branches.ts
7
+ var branches_default = defineCommand({
8
+ meta: {
9
+ name: "branches",
10
+ description: "Manage content branches on Studio"
11
+ },
12
+ args: {
13
+ workspace: {
14
+ type: "string",
15
+ description: "Workspace ID",
16
+ required: false
17
+ },
18
+ project: {
19
+ type: "string",
20
+ description: "Project ID",
21
+ required: false
22
+ },
23
+ json: {
24
+ type: "boolean",
25
+ description: "JSON output",
26
+ required: false
27
+ }
28
+ },
29
+ async run({ args }) {
30
+ if (!args.json) intro(pc.bold("contentrain studio branches"));
31
+ try {
32
+ const client = await resolveStudioClient();
33
+ const ctx = await resolveStudioContext(client, args);
34
+ if (!ctx) {
35
+ log.warning("No workspace or project found.");
36
+ outro("");
37
+ return;
38
+ }
39
+ const { workspaceId, projectId } = ctx;
40
+ const s = args.json ? null : spinner();
41
+ s?.start("Fetching branches...");
42
+ const branches = await client.listBranches(workspaceId, projectId);
43
+ s?.stop(`${formatCount(branches.length, "branch", "branches")}`);
44
+ if (args.json) {
45
+ process.stdout.write(JSON.stringify(branches, null, 2));
46
+ return;
47
+ }
48
+ if (branches.length === 0) {
49
+ log.info("No pending content branches.");
50
+ outro("");
51
+ return;
52
+ }
53
+ const rows = branches.map((b) => [
54
+ b.name,
55
+ `+${b.ahead}`,
56
+ b.author ?? "—",
57
+ timeAgo(b.lastCommitDate)
58
+ ]);
59
+ log.message(formatTable([
60
+ "Branch",
61
+ "Ahead",
62
+ "Author",
63
+ "Updated"
64
+ ], rows));
65
+ const branchChoice = await select({
66
+ message: "Select a branch to manage",
67
+ options: [...branches.map((b) => ({
68
+ value: b.name,
69
+ label: `${b.name} (+${b.ahead})`
70
+ })), {
71
+ value: "__cancel__",
72
+ label: pc.dim("Cancel")
73
+ }]
74
+ });
75
+ if (isCancel(branchChoice) || branchChoice === "__cancel__") {
76
+ outro("");
77
+ return;
78
+ }
79
+ const selectedBranch = branchChoice;
80
+ const action = await select({
81
+ message: `Action for ${pc.cyan(selectedBranch)}`,
82
+ options: [
83
+ {
84
+ value: "merge",
85
+ label: "Merge into main branch"
86
+ },
87
+ {
88
+ value: "reject",
89
+ label: "Reject and delete"
90
+ },
91
+ {
92
+ value: "cancel",
93
+ label: pc.dim("Cancel")
94
+ }
95
+ ]
96
+ });
97
+ if (isCancel(action) || action === "cancel") {
98
+ outro("");
99
+ return;
100
+ }
101
+ if (action === "merge") {
102
+ const confirmed = await confirm({ message: `Merge ${pc.cyan(selectedBranch)} into the main branch?` });
103
+ if (isCancel(confirmed) || !confirmed) {
104
+ outro(pc.dim("Cancelled"));
105
+ return;
106
+ }
107
+ const ms = spinner();
108
+ ms.start("Merging...");
109
+ await client.mergeBranch(workspaceId, projectId, selectedBranch);
110
+ ms.stop("Merged");
111
+ log.success(`Branch ${pc.cyan(selectedBranch)} merged successfully.`);
112
+ }
113
+ if (action === "reject") {
114
+ const confirmed = await confirm({ message: `Reject and delete ${pc.cyan(selectedBranch)}? This cannot be undone.` });
115
+ if (isCancel(confirmed) || !confirmed) {
116
+ outro(pc.dim("Cancelled"));
117
+ return;
118
+ }
119
+ const rs = spinner();
120
+ rs.start("Rejecting...");
121
+ await client.rejectBranch(workspaceId, projectId, selectedBranch);
122
+ rs.stop("Rejected");
123
+ log.success(`Branch ${pc.cyan(selectedBranch)} rejected and deleted.`);
124
+ }
125
+ } catch (error) {
126
+ log.error(error instanceof Error ? error.message : String(error));
127
+ process.exitCode = 1;
128
+ }
129
+ if (!args.json) outro("");
130
+ }
131
+ });
132
+ function timeAgo(dateStr) {
133
+ const diff = Date.now() - new Date(dateStr).getTime();
134
+ const minutes = Math.floor(diff / 6e4);
135
+ if (minutes < 1) return "just now";
136
+ if (minutes < 60) return `${minutes}m ago`;
137
+ const hours = Math.floor(minutes / 60);
138
+ if (hours < 24) return `${hours}h ago`;
139
+ return `${Math.floor(hours / 24)}d ago`;
140
+ }
141
+ //#endregion
142
+ export { branches_default as default };
@@ -0,0 +1,106 @@
1
+ import { i as pc, t as formatCount } from "./ui-B5mXontH.mjs";
2
+ import { n as resolveStudioClient } from "./client-D4xW8aLz.mjs";
3
+ import { t as resolveStudioContext } from "./resolve-context-BMV7JwCw.mjs";
4
+ import { defineCommand } from "citty";
5
+ import { intro, log, outro, spinner } from "@clack/prompts";
6
+ //#region src/studio/commands/cdn-build.ts
7
+ const POLL_INTERVAL_MS = 3e3;
8
+ var cdn_build_default = defineCommand({
9
+ meta: {
10
+ name: "cdn-build",
11
+ description: "Trigger a CDN rebuild"
12
+ },
13
+ args: {
14
+ workspace: {
15
+ type: "string",
16
+ description: "Workspace ID",
17
+ required: false
18
+ },
19
+ project: {
20
+ type: "string",
21
+ description: "Project ID",
22
+ required: false
23
+ },
24
+ wait: {
25
+ type: "boolean",
26
+ description: "Wait for build to complete",
27
+ required: false
28
+ },
29
+ json: {
30
+ type: "boolean",
31
+ description: "JSON output",
32
+ required: false
33
+ }
34
+ },
35
+ async run({ args }) {
36
+ if (!args.json) intro(pc.bold("contentrain studio cdn-build"));
37
+ try {
38
+ const client = await resolveStudioClient();
39
+ const ctx = await resolveStudioContext(client, args);
40
+ if (!ctx) {
41
+ log.warning("No workspace or project found.");
42
+ outro("");
43
+ return;
44
+ }
45
+ const { workspaceId, projectId } = ctx;
46
+ const s = args.json ? null : spinner();
47
+ s?.start("Triggering CDN build...");
48
+ const build = await client.triggerCdnBuild(workspaceId, projectId);
49
+ if (build.status === "success") {
50
+ s?.stop("Build complete");
51
+ outputResult(build, args.json);
52
+ if (!args.json) outro("");
53
+ return;
54
+ }
55
+ if (!args.wait || build.status === "failed") {
56
+ s?.stop(`Build ${build.status}`);
57
+ outputResult(build, args.json);
58
+ if (build.status === "failed" && build.errorMessage && !args.json) {
59
+ log.error(build.errorMessage);
60
+ process.exitCode = 1;
61
+ }
62
+ if (!args.json) outro("");
63
+ return;
64
+ }
65
+ s?.stop("Build started, waiting...");
66
+ const ps = args.json ? null : spinner();
67
+ ps?.start("Building...");
68
+ let current = build;
69
+ while (current.status === "building") {
70
+ await sleep(POLL_INTERVAL_MS);
71
+ const builds = await client.listCdnBuilds(workspaceId, projectId, { limit: "1" });
72
+ if (builds.data[0]) current = builds.data[0];
73
+ }
74
+ if (current.status === "success") {
75
+ ps?.stop("Build complete");
76
+ outputResult(current, args.json);
77
+ } else {
78
+ ps?.stop("Build failed");
79
+ outputResult(current, args.json);
80
+ if (current.errorMessage && !args.json) log.error(current.errorMessage);
81
+ process.exitCode = 1;
82
+ }
83
+ } catch (error) {
84
+ log.error(error instanceof Error ? error.message : String(error));
85
+ process.exitCode = 1;
86
+ }
87
+ if (!args.json) outro("");
88
+ }
89
+ });
90
+ function outputResult(build, json) {
91
+ if (json) {
92
+ process.stdout.write(JSON.stringify(build, null, 2));
93
+ return;
94
+ }
95
+ const sizeKb = (build.totalSizeBytes / 1024).toFixed(1);
96
+ const duration = build.buildDurationMs ? `${(build.buildDurationMs / 1e3).toFixed(1)}s` : "—";
97
+ log.message(` Files: ${formatCount(build.fileCount, "file")}`);
98
+ log.message(` Size: ${sizeKb} KB`);
99
+ log.message(` Duration: ${duration}`);
100
+ if (build.status === "success") log.success("CDN is up to date.");
101
+ }
102
+ function sleep(ms) {
103
+ return new Promise((resolve) => setTimeout(resolve, ms));
104
+ }
105
+ //#endregion
106
+ export { cdn_build_default as default };
@@ -0,0 +1,90 @@
1
+ import { i as pc } from "./ui-B5mXontH.mjs";
2
+ import { n as resolveStudioClient } from "./client-D4xW8aLz.mjs";
3
+ import { t as resolveStudioContext } from "./resolve-context-BMV7JwCw.mjs";
4
+ import { defineCommand } from "citty";
5
+ import { confirm, intro, isCancel, log, outro, spinner } from "@clack/prompts";
6
+ //#region src/studio/commands/cdn-init.ts
7
+ var cdn_init_default = defineCommand({
8
+ meta: {
9
+ name: "cdn-init",
10
+ description: "Set up CDN for content delivery"
11
+ },
12
+ args: {
13
+ workspace: {
14
+ type: "string",
15
+ description: "Workspace ID",
16
+ required: false
17
+ },
18
+ project: {
19
+ type: "string",
20
+ description: "Project ID",
21
+ required: false
22
+ }
23
+ },
24
+ async run({ args }) {
25
+ intro(pc.bold("contentrain studio cdn-init"));
26
+ try {
27
+ const client = await resolveStudioClient();
28
+ const ctx = await resolveStudioContext(client, args);
29
+ if (!ctx) {
30
+ log.warning("No workspace or project found.");
31
+ outro("");
32
+ return;
33
+ }
34
+ const { workspaceId, projectId } = ctx;
35
+ const s = spinner();
36
+ s.start("Checking CDN status...");
37
+ const existingKeys = await client.listCdnKeys(workspaceId, projectId);
38
+ if (existingKeys.length > 0) {
39
+ s.stop("CDN already configured");
40
+ log.info(`Found ${existingKeys.length} existing CDN key(s):`);
41
+ for (const key of existingKeys) log.message(` ${pc.cyan(key.name)} — ${pc.dim(key.prefix + "...")} (${key.createdAt})`);
42
+ const createAnother = await confirm({ message: "Create an additional CDN key?" });
43
+ if (isCancel(createAnother) || !createAnother) {
44
+ outro("");
45
+ return;
46
+ }
47
+ } else s.stop("No CDN keys found");
48
+ const s2 = spinner();
49
+ s2.start("Creating CDN API key...");
50
+ const newKey = await client.createCdnKey(workspaceId, projectId, "cli-generated");
51
+ s2.stop("CDN key created");
52
+ log.warning("Save this API key — it will not be shown again:");
53
+ log.message(`\n ${pc.bold(pc.green(newKey.key ?? newKey.prefix + "..."))}`);
54
+ log.message("");
55
+ const shouldBuild = await confirm({ message: "Trigger initial CDN build now?" });
56
+ if (!isCancel(shouldBuild) && shouldBuild) {
57
+ const s3 = spinner();
58
+ s3.start("Building CDN...");
59
+ const build = await client.triggerCdnBuild(workspaceId, projectId);
60
+ if (build.status === "success") s3.stop(`Build complete (${build.fileCount} files)`);
61
+ else if (build.status === "building") s3.stop("Build started (check status with `contentrain studio cdn-build --wait`)");
62
+ else {
63
+ s3.stop(`Build ${build.status}`);
64
+ if (build.errorMessage) log.warning(build.errorMessage);
65
+ }
66
+ }
67
+ log.info(pc.bold("\nSDK Integration"));
68
+ log.message(pc.dim(" Install the SDK:"));
69
+ log.message(` ${pc.cyan("npm install @contentrain/query")}`);
70
+ log.message("");
71
+ log.message(pc.dim(" Configure the CDN client:"));
72
+ log.message(`
73
+ ${pc.dim("import { createContentrainCDN } from '@contentrain/query/cdn'")}
74
+
75
+ ${pc.dim("const client = createContentrainCDN({")}
76
+ ${pc.dim(` projectId: '${projectId}',`)}
77
+ ${pc.dim(" apiKey: process.env.CONTENTRAIN_CDN_KEY,")}
78
+ ${pc.dim("})")}
79
+ `);
80
+ log.message(pc.dim(" Add to your .env:"));
81
+ log.message(` ${pc.yellow(`CONTENTRAIN_CDN_KEY=${newKey.key ?? "<your-key>"}`)}`);
82
+ } catch (error) {
83
+ log.error(error instanceof Error ? error.message : String(error));
84
+ process.exitCode = 1;
85
+ }
86
+ outro("");
87
+ }
88
+ });
89
+ //#endregion
90
+ export { cdn_init_default as default };