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 +54 -0
- package/dist/activity-CtOcAA4X.mjs +88 -0
- package/dist/branches-C_kgqSv9.mjs +142 -0
- package/dist/cdn-build-CDv_yM15.mjs +106 -0
- package/dist/cdn-init-DXQ-3V_c.mjs +90 -0
- package/dist/client-D4xW8aLz.mjs +275 -0
- package/dist/index.mjs +3 -2
- package/dist/login-CO0hxICh.mjs +243 -0
- package/dist/logout-CRM4fCSH.mjs +29 -0
- package/dist/resolve-context-BMV7JwCw.mjs +58 -0
- package/dist/status-ts5FvDEZ.mjs +128 -0
- package/dist/studio-BmXS_bCq.mjs +23 -0
- package/dist/submissions-6_OCrUNF.mjs +169 -0
- package/dist/usage-BAb4q7Pz.mjs +81 -0
- package/dist/webhooks-C39IItU1.mjs +235 -0
- package/dist/whoami-D8NyFKs_.mjs +65 -0
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -2,9 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/contentrain)
|
|
4
4
|
[](https://github.com/Contentrain/ai/tree/main/packages/cli)
|
|
5
|
+
[](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 };
|