contentrain 0.3.1 → 0.3.3

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.
@@ -0,0 +1,58 @@
1
+ import { c as saveDefaults, o as loadCredentials } from "./client-D4xW8aLz.mjs";
2
+ import { confirm, isCancel, select } from "@clack/prompts";
3
+ //#region src/studio/resolve-context.ts
4
+ /**
5
+ * Resolve workspace + project IDs for a Studio command.
6
+ *
7
+ * Falls back to interactive prompts when IDs are not provided via args
8
+ * or saved defaults.
9
+ */
10
+ async function resolveStudioContext(client, args) {
11
+ const credentials = await loadCredentials();
12
+ let workspaceId = args.workspace ?? credentials?.defaultWorkspaceId;
13
+ if (!workspaceId) {
14
+ const workspaces = await client.listWorkspaces();
15
+ if (workspaces.length === 0) return null;
16
+ if (workspaces.length === 1) workspaceId = workspaces[0].id;
17
+ else {
18
+ const choice = await select({
19
+ message: "Select workspace",
20
+ options: workspaces.map((w) => ({
21
+ value: w.id,
22
+ label: w.name,
23
+ hint: w.plan
24
+ }))
25
+ });
26
+ if (isCancel(choice)) return null;
27
+ workspaceId = choice;
28
+ }
29
+ }
30
+ let projectId = args.project ?? credentials?.defaultProjectId;
31
+ if (!projectId) {
32
+ const projects = await client.listProjects(workspaceId);
33
+ if (projects.length === 0) return null;
34
+ if (projects.length === 1) projectId = projects[0].id;
35
+ else {
36
+ const choice = await select({
37
+ message: "Select project",
38
+ options: projects.map((p) => ({
39
+ value: p.id,
40
+ label: p.name,
41
+ hint: p.stack
42
+ }))
43
+ });
44
+ if (isCancel(choice)) return null;
45
+ projectId = choice;
46
+ }
47
+ }
48
+ if ((workspaceId !== credentials?.defaultWorkspaceId || projectId !== credentials?.defaultProjectId) && credentials) {
49
+ const shouldSave = await confirm({ message: "Save as default workspace/project?" });
50
+ if (!isCancel(shouldSave) && shouldSave) await saveDefaults(workspaceId, projectId);
51
+ }
52
+ return {
53
+ workspaceId,
54
+ projectId
55
+ };
56
+ }
57
+ //#endregion
58
+ export { resolveStudioContext as t };
@@ -66,7 +66,7 @@ var serve_default = defineCommand({
66
66
  consola.warn("Serve UI not found. Running in API-only mode.");
67
67
  consola.info("UI will be available after building: cd packages/cli/src/serve-ui && pnpm build");
68
68
  }
69
- const { createServeApp } = await import("./server-DW0KXB0T.mjs");
69
+ const { createServeApp } = await import("./server-WLDOAZa4.mjs");
70
70
  const { app, handleUpgrade } = await createServeApp({
71
71
  projectRoot,
72
72
  port,
@@ -1,4 +1,7 @@
1
1
  import { simpleGit } from "simple-git";
2
+ import { CONTENTRAIN_BRANCH } from "@contentrain/types";
3
+ import { tmpdir } from "node:os";
4
+ import { randomUUID } from "node:crypto";
2
5
  import { join } from "node:path";
3
6
  import { readFile, unlink, writeFile } from "node:fs/promises";
4
7
  import { existsSync, readFileSync } from "node:fs";
@@ -202,18 +205,19 @@ async function createServeApp(options) {
202
205
  }));
203
206
  router.add("/api/branches", defineEventHandler(async () => {
204
207
  const branches = await simpleGit(projectRoot).branchLocal();
205
- const crBranches = branches.all.filter((b) => b.startsWith("contentrain/")).map((b) => ({
208
+ const crBranches = branches.all.filter((b) => b.startsWith("cr/")).map((b) => ({
206
209
  name: b,
207
- current: b === branches.current
210
+ current: b === branches.current,
211
+ system: b === CONTENTRAIN_BRANCH
208
212
  }));
209
213
  return {
210
214
  branches: crBranches,
211
- total: crBranches.length
215
+ total: crBranches.filter((b) => !b.system).length
212
216
  };
213
217
  }));
214
218
  router.add("/api/branches/diff", defineEventHandler(async (event) => {
215
219
  const branchName = getQuery(event)["name"];
216
- if (!branchName?.startsWith("contentrain/")) throw createError({
220
+ if (!branchName?.startsWith("cr/")) throw createError({
217
221
  statusCode: 400,
218
222
  message: "Invalid branch name"
219
223
  });
@@ -232,29 +236,53 @@ async function createServeApp(options) {
232
236
  message: "Method not allowed"
233
237
  });
234
238
  const branchName = (await readBody(event)).branch;
235
- if (!branchName?.startsWith("contentrain/")) throw createError({
239
+ if (!branchName?.startsWith("cr/")) throw createError({
236
240
  statusCode: 400,
237
241
  message: "Invalid branch name"
238
242
  });
239
243
  const git = simpleGit(projectRoot);
240
244
  const baseBranch = getDefaultBranch();
241
- await git.checkout(baseBranch);
242
- await git.merge([
243
- branchName,
244
- "--no-edit",
245
- "-X",
246
- "theirs"
245
+ if (!(await git.branchLocal()).all.includes(CONTENTRAIN_BRANCH)) await git.branch([CONTENTRAIN_BRANCH, baseBranch]);
246
+ const mergePath = join(tmpdir(), `cr-merge-${randomUUID()}`);
247
+ await git.raw([
248
+ "worktree",
249
+ "add",
250
+ mergePath,
251
+ CONTENTRAIN_BRANCH
247
252
  ]);
248
- await git.deleteLocalBranch(branchName, true);
249
- broadcast({
250
- type: "branch:merged",
251
- branch: branchName
252
- });
253
- return {
254
- status: "merged",
255
- branch: branchName,
256
- into: baseBranch
257
- };
253
+ const mergeGit = simpleGit(mergePath);
254
+ try {
255
+ await mergeGit.merge([baseBranch, "--no-edit"]).catch(() => {});
256
+ await mergeGit.merge([branchName, "--no-edit"]);
257
+ const tip = (await mergeGit.raw(["rev-parse", "HEAD"])).trim();
258
+ await git.raw([
259
+ "update-ref",
260
+ `refs/heads/${baseBranch}`,
261
+ tip
262
+ ]);
263
+ if ((await git.raw(["branch", "--show-current"])).trim() === baseBranch) await git.checkout([
264
+ tip,
265
+ "--",
266
+ ".contentrain/"
267
+ ]);
268
+ await git.deleteLocalBranch(branchName, true);
269
+ broadcast({
270
+ type: "branch:merged",
271
+ branch: branchName
272
+ });
273
+ return {
274
+ status: "merged",
275
+ branch: branchName,
276
+ into: baseBranch
277
+ };
278
+ } finally {
279
+ await git.raw([
280
+ "worktree",
281
+ "remove",
282
+ mergePath,
283
+ "--force"
284
+ ]).catch(() => {});
285
+ }
258
286
  }));
259
287
  router.add("/api/branches/reject", defineEventHandler(async (event) => {
260
288
  if (event.method !== "POST") throw createError({
@@ -262,7 +290,7 @@ async function createServeApp(options) {
262
290
  message: "Method not allowed"
263
291
  });
264
292
  const branchName = (await readBody(event)).branch;
265
- if (!branchName?.startsWith("contentrain/")) throw createError({
293
+ if (!branchName?.startsWith("cr/")) throw createError({
266
294
  statusCode: 400,
267
295
  message: "Invalid branch name"
268
296
  });
@@ -417,7 +445,7 @@ async function createServeApp(options) {
417
445
  const op = JSON.parse(raw)?.lastOperation;
418
446
  if (op?.tool === "contentrain_scan" || op?.tool === "contentrain_apply") lastNormalize = op;
419
447
  } catch {}
420
- const branches = (await simpleGit(projectRoot).branchLocal()).all.filter((b) => b.startsWith("contentrain/normalize/")).map((b) => ({ name: b }));
448
+ const branches = (await simpleGit(projectRoot).branchLocal()).all.filter((b) => b.startsWith("cr/normalize/")).map((b) => ({ name: b }));
421
449
  return {
422
450
  lastOperation: lastNormalize,
423
451
  pendingBranches: branches
@@ -511,23 +539,53 @@ async function createServeApp(options) {
511
539
  message: "Method not allowed"
512
540
  });
513
541
  const branchName = (await readBody(event)).branch;
514
- if (!branchName?.startsWith("contentrain/")) throw createError({
542
+ if (!branchName?.startsWith("cr/")) throw createError({
515
543
  statusCode: 400,
516
544
  message: "Invalid branch name"
517
545
  });
518
546
  const git = simpleGit(projectRoot);
519
- const currentBranch = (await git.branchLocal()).current;
520
- await git.merge([branchName]);
521
- await git.deleteLocalBranch(branchName, true);
522
- broadcast({
523
- type: "branch:merged",
524
- branch: branchName
525
- });
526
- return {
527
- status: "merged",
528
- branch: branchName,
529
- into: currentBranch
530
- };
547
+ const baseBranch = getDefaultBranch();
548
+ if (!(await git.branchLocal()).all.includes(CONTENTRAIN_BRANCH)) await git.branch([CONTENTRAIN_BRANCH, baseBranch]);
549
+ const mergePath = join(tmpdir(), `cr-merge-${randomUUID()}`);
550
+ await git.raw([
551
+ "worktree",
552
+ "add",
553
+ mergePath,
554
+ CONTENTRAIN_BRANCH
555
+ ]);
556
+ const mergeGit = simpleGit(mergePath);
557
+ try {
558
+ await mergeGit.merge([baseBranch, "--no-edit"]).catch(() => {});
559
+ await mergeGit.merge([branchName, "--no-edit"]);
560
+ const tip = (await mergeGit.raw(["rev-parse", "HEAD"])).trim();
561
+ await git.raw([
562
+ "update-ref",
563
+ `refs/heads/${baseBranch}`,
564
+ tip
565
+ ]);
566
+ if ((await git.raw(["branch", "--show-current"])).trim() === baseBranch) await git.checkout([
567
+ tip,
568
+ "--",
569
+ ".contentrain/"
570
+ ]);
571
+ await git.deleteLocalBranch(branchName, true);
572
+ broadcast({
573
+ type: "branch:merged",
574
+ branch: branchName
575
+ });
576
+ return {
577
+ status: "merged",
578
+ branch: branchName,
579
+ into: baseBranch
580
+ };
581
+ } finally {
582
+ await git.raw([
583
+ "worktree",
584
+ "remove",
585
+ mergePath,
586
+ "--force"
587
+ ]).catch(() => {});
588
+ }
531
589
  }));
532
590
  router.add("/api/normalize/sources", defineEventHandler(async () => {
533
591
  const sourcesPath = join(crDir, "normalize-sources.json");
@@ -3,6 +3,7 @@ import { i as pc, n as formatPercent, r as formatTable, t as formatCount } from
3
3
  import { defineCommand } from "citty";
4
4
  import { intro, log, outro } from "@clack/prompts";
5
5
  import { simpleGit } from "simple-git";
6
+ import { CONTENTRAIN_BRANCH } from "@contentrain/types";
6
7
  import { countEntries, readModel } from "@contentrain/mcp/core/model-manager";
7
8
  import { validateProject } from "@contentrain/mcp/core/validator";
8
9
  //#region src/commands/status.ts
@@ -43,7 +44,22 @@ var status_default = defineCommand({
43
44
  };
44
45
  } catch {}
45
46
  try {
46
- jsonResult["pending_branches"] = (await simpleGit(projectRoot).branch(["--list", "contentrain/*"])).all;
47
+ const git = simpleGit(projectRoot);
48
+ const branches = await git.branch(["--list", "cr/*"]);
49
+ const allLocal = await git.branchLocal();
50
+ jsonResult["pending_branches"] = branches.all.filter((b) => b !== CONTENTRAIN_BRANCH);
51
+ const contentBranchExists = allLocal.all.includes(CONTENTRAIN_BRANCH);
52
+ const contentBranchInfo = { exists: contentBranchExists };
53
+ if (contentBranchExists) try {
54
+ const baseBranch = ctx.config?.repository?.default_branch ?? "main";
55
+ const aheadRaw = await git.raw([
56
+ "rev-list",
57
+ "--count",
58
+ `${baseBranch}..${CONTENTRAIN_BRANCH}`
59
+ ]);
60
+ contentBranchInfo["ahead"] = Number.parseInt(aheadRaw.trim(), 10);
61
+ } catch {}
62
+ jsonResult["content_branch"] = contentBranchInfo;
47
63
  } catch {}
48
64
  }
49
65
  process.stdout.write(JSON.stringify(jsonResult, null, 2));
@@ -91,9 +107,26 @@ var status_default = defineCommand({
91
107
  ], rows));
92
108
  } else log.message(" No models yet. Run `contentrain init` with a template or create models.");
93
109
  try {
94
- const branches = await simpleGit(projectRoot).branch(["--list", "contentrain/*"]);
95
- if (branches.all.length > 0) {
96
- const count = branches.all.length;
110
+ const git = simpleGit(projectRoot);
111
+ if ((await git.branchLocal()).all.includes(CONTENTRAIN_BRANCH)) try {
112
+ const baseBranch = ctx.config?.repository?.default_branch ?? "main";
113
+ const aheadRaw = await git.raw([
114
+ "rev-list",
115
+ "--count",
116
+ `${baseBranch}..${CONTENTRAIN_BRANCH}`
117
+ ]);
118
+ const ahead = Number.parseInt(aheadRaw.trim(), 10);
119
+ if (ahead > 0) {
120
+ log.info(pc.bold(`\nContent branch`));
121
+ log.message(` ${pc.cyan(CONTENTRAIN_BRANCH)} is ${pc.yellow(String(ahead))} commit(s) ahead of ${baseBranch}`);
122
+ } else {
123
+ log.info(pc.bold(`\nContent branch`));
124
+ log.message(` ${pc.cyan(CONTENTRAIN_BRANCH)} is in sync with ${baseBranch}`);
125
+ }
126
+ } catch {}
127
+ const featureBranches = (await git.branch(["--list", "cr/*"])).all.filter((b) => b !== CONTENTRAIN_BRANCH);
128
+ if (featureBranches.length > 0) {
129
+ const count = featureBranches.length;
97
130
  if (count >= 80) {
98
131
  log.error(pc.bold(`\nBLOCKED: ${count} active contentrain branches (limit: 80)`));
99
132
  log.message(` New writes are blocked. Merge or delete old branches with ${pc.cyan("contentrain diff")}.`);
@@ -101,7 +134,7 @@ var status_default = defineCommand({
101
134
  log.warning(pc.bold(`\nWARNING: ${count} active contentrain branches (limit: 50)`));
102
135
  log.message(` Consider merging or deleting old branches with ${pc.cyan("contentrain diff")}.`);
103
136
  } else log.info(pc.bold(`\nPending branches (${count})`));
104
- for (const branch of branches.all) log.message(` ${pc.yellow("●")} ${branch}`);
137
+ for (const branch of featureBranches) log.message(` ${pc.yellow("●")} ${branch}`);
105
138
  log.message(` Run ${pc.cyan("contentrain diff")} to review.`);
106
139
  }
107
140
  } catch {}
@@ -0,0 +1,128 @@
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 { intro, log, outro, spinner } from "@clack/prompts";
6
+ //#region src/studio/commands/status.ts
7
+ var status_default = defineCommand({
8
+ meta: {
9
+ name: "status",
10
+ description: "Show project overview 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
+ 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 status"));
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 project status...");
42
+ const [workspaces, projects, branches] = await Promise.all([
43
+ client.listWorkspaces(),
44
+ client.listProjects(workspaceId),
45
+ client.listBranches(workspaceId, projectId).catch(() => [])
46
+ ]);
47
+ const workspace = workspaces.find((w) => w.id === workspaceId);
48
+ const project = projects.find((p) => p.id === projectId);
49
+ let cdnBuilds;
50
+ try {
51
+ cdnBuilds = await client.listCdnBuilds(workspaceId, projectId, { limit: "1" });
52
+ } catch {}
53
+ s?.stop("Status loaded");
54
+ if (args.json) {
55
+ process.stdout.write(JSON.stringify({
56
+ workspace: workspace ? {
57
+ id: workspace.id,
58
+ name: workspace.name,
59
+ plan: workspace.plan
60
+ } : null,
61
+ project: project ? {
62
+ id: project.id,
63
+ name: project.name,
64
+ stack: project.stack
65
+ } : null,
66
+ branches: {
67
+ total: branches.length,
68
+ items: branches
69
+ },
70
+ cdn: cdnBuilds?.data[0] ? { lastBuild: {
71
+ status: cdnBuilds.data[0].status,
72
+ fileCount: cdnBuilds.data[0].fileCount,
73
+ createdAt: cdnBuilds.data[0].createdAt
74
+ } } : null
75
+ }, null, 2));
76
+ return;
77
+ }
78
+ log.info(pc.bold("Project"));
79
+ if (project) {
80
+ log.message(` Name: ${pc.cyan(project.name)}`);
81
+ log.message(` Stack: ${project.stack}`);
82
+ log.message(` Members: ${project.memberCount}`);
83
+ }
84
+ if (workspace) {
85
+ log.info(pc.bold("\nWorkspace"));
86
+ log.message(` Name: ${pc.cyan(workspace.name)}`);
87
+ log.message(` Plan: ${workspace.plan}`);
88
+ }
89
+ if (branches.length > 0) {
90
+ log.info(pc.bold(`\nPending branches (${branches.length})`));
91
+ const rows = branches.slice(0, 10).map((b) => [
92
+ b.name,
93
+ `+${b.ahead}`,
94
+ b.author ?? "—",
95
+ timeAgo(b.lastCommitDate)
96
+ ]);
97
+ log.message(formatTable([
98
+ "Branch",
99
+ "Ahead",
100
+ "Author",
101
+ "Updated"
102
+ ], rows));
103
+ if (branches.length > 10) log.message(pc.dim(` ... and ${formatCount(branches.length - 10, "more branch", "more branches")}`));
104
+ } else log.message("\nNo pending branches.");
105
+ const lastBuild = cdnBuilds?.data[0];
106
+ if (lastBuild) {
107
+ log.info(pc.bold("\nCDN"));
108
+ const statusColor = lastBuild.status === "success" ? pc.green : lastBuild.status === "failed" ? pc.red : pc.yellow;
109
+ log.message(` Last build: ${statusColor(lastBuild.status)} (${formatCount(lastBuild.fileCount, "file")}, ${timeAgo(lastBuild.createdAt)})`);
110
+ }
111
+ } catch (error) {
112
+ log.error(error instanceof Error ? error.message : String(error));
113
+ process.exitCode = 1;
114
+ }
115
+ if (!args.json) outro("");
116
+ }
117
+ });
118
+ function timeAgo(dateStr) {
119
+ const diff = Date.now() - new Date(dateStr).getTime();
120
+ const minutes = Math.floor(diff / 6e4);
121
+ if (minutes < 1) return "just now";
122
+ if (minutes < 60) return `${minutes}m ago`;
123
+ const hours = Math.floor(minutes / 60);
124
+ if (hours < 24) return `${hours}h ago`;
125
+ return `${Math.floor(hours / 24)}d ago`;
126
+ }
127
+ //#endregion
128
+ export { status_default as default };
@@ -0,0 +1,23 @@
1
+ import { defineCommand } from "citty";
2
+ //#region src/studio/index.ts
3
+ var studio_default = defineCommand({
4
+ meta: {
5
+ name: "studio",
6
+ description: "Interact with Contentrain Studio"
7
+ },
8
+ subCommands: {
9
+ login: () => import("./login-CO0hxICh.mjs").then((m) => m.default),
10
+ logout: () => import("./logout-CRM4fCSH.mjs").then((m) => m.default),
11
+ whoami: () => import("./whoami-D8NyFKs_.mjs").then((m) => m.default),
12
+ status: () => import("./status-ts5FvDEZ.mjs").then((m) => m.default),
13
+ activity: () => import("./activity-CtOcAA4X.mjs").then((m) => m.default),
14
+ usage: () => import("./usage-BAb4q7Pz.mjs").then((m) => m.default),
15
+ branches: () => import("./branches-C_kgqSv9.mjs").then((m) => m.default),
16
+ "cdn-init": () => import("./cdn-init-DXQ-3V_c.mjs").then((m) => m.default),
17
+ "cdn-build": () => import("./cdn-build-CDv_yM15.mjs").then((m) => m.default),
18
+ webhooks: () => import("./webhooks-C39IItU1.mjs").then((m) => m.default),
19
+ submissions: () => import("./submissions-6_OCrUNF.mjs").then((m) => m.default)
20
+ }
21
+ });
22
+ //#endregion
23
+ export { studio_default as default };
@@ -0,0 +1,169 @@
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/submissions.ts
7
+ var submissions_default = defineCommand({
8
+ meta: {
9
+ name: "submissions",
10
+ description: "Manage form submissions 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
+ form: {
24
+ type: "string",
25
+ description: "Form model ID",
26
+ required: false
27
+ },
28
+ status: {
29
+ type: "string",
30
+ description: "Filter by status (pending, approved, rejected)",
31
+ required: false
32
+ },
33
+ json: {
34
+ type: "boolean",
35
+ description: "JSON output",
36
+ required: false
37
+ }
38
+ },
39
+ async run({ args }) {
40
+ if (!args.json) intro(pc.bold("contentrain studio submissions"));
41
+ try {
42
+ const client = await resolveStudioClient();
43
+ const ctx = await resolveStudioContext(client, args);
44
+ if (!ctx) {
45
+ log.warning("No workspace or project found.");
46
+ outro("");
47
+ return;
48
+ }
49
+ const { workspaceId, projectId } = ctx;
50
+ const formId = args.form ?? await resolveFormId(args.json);
51
+ if (!formId) {
52
+ if (!args.json) outro("");
53
+ return;
54
+ }
55
+ const s = args.json ? null : spinner();
56
+ s?.start("Fetching submissions...");
57
+ const query = {};
58
+ if (args.status) query["status"] = args.status;
59
+ const result = await client.listSubmissions(workspaceId, projectId, formId, query);
60
+ s?.stop(`${formatCount(result.data.length, "submission")}`);
61
+ if (args.json) {
62
+ process.stdout.write(JSON.stringify(result, null, 2));
63
+ return;
64
+ }
65
+ if (result.data.length === 0) {
66
+ log.info("No submissions found.");
67
+ outro("");
68
+ return;
69
+ }
70
+ const rows = result.data.map((sub) => {
71
+ const preview = summarizeData(sub.data);
72
+ const statusColor = sub.status === "approved" ? pc.green : sub.status === "rejected" ? pc.red : sub.status === "spam" ? pc.dim : pc.yellow;
73
+ return [
74
+ sub.id.slice(0, 8),
75
+ preview,
76
+ statusColor(sub.status),
77
+ timeAgo(sub.submittedAt)
78
+ ];
79
+ });
80
+ log.message(formatTable([
81
+ "ID",
82
+ "Preview",
83
+ "Status",
84
+ "Submitted"
85
+ ], rows));
86
+ const pendingSubs = result.data.filter((sub) => sub.status === "pending");
87
+ if (pendingSubs.length === 0) {
88
+ outro("");
89
+ return;
90
+ }
91
+ const action = await select({
92
+ message: `${formatCount(pendingSubs.length, "pending submission")}. What would you like to do?`,
93
+ options: [
94
+ {
95
+ value: "approve",
96
+ label: "Approve a submission"
97
+ },
98
+ {
99
+ value: "reject",
100
+ label: "Reject a submission"
101
+ },
102
+ {
103
+ value: "cancel",
104
+ label: pc.dim("Done")
105
+ }
106
+ ]
107
+ });
108
+ if (isCancel(action) || action === "cancel") {
109
+ outro("");
110
+ return;
111
+ }
112
+ const subChoice = await select({
113
+ message: "Select submission",
114
+ options: pendingSubs.map((sub) => ({
115
+ value: sub.id,
116
+ label: `${sub.id.slice(0, 8)} — ${summarizeData(sub.data)}`
117
+ }))
118
+ });
119
+ if (isCancel(subChoice)) {
120
+ outro("");
121
+ return;
122
+ }
123
+ const targetStatus = action === "approve" ? "approved" : "rejected";
124
+ const confirmed = await confirm({ message: `${action === "approve" ? "Approve" : "Reject"} submission ${subChoice.slice(0, 8)}?` });
125
+ if (isCancel(confirmed) || !confirmed) {
126
+ outro(pc.dim("Cancelled"));
127
+ return;
128
+ }
129
+ const ms = spinner();
130
+ ms.start(`${action === "approve" ? "Approving" : "Rejecting"}...`);
131
+ await client.updateSubmissionStatus(workspaceId, projectId, formId, subChoice, targetStatus);
132
+ ms.stop(`Submission ${targetStatus}`);
133
+ log.success(`Submission ${pc.cyan(subChoice.slice(0, 8))} ${targetStatus}.`);
134
+ } catch (error) {
135
+ log.error(error instanceof Error ? error.message : String(error));
136
+ process.exitCode = 1;
137
+ }
138
+ if (!args.json) outro("");
139
+ }
140
+ });
141
+ async function resolveFormId(isJson) {
142
+ if (isJson) {
143
+ const { log: l } = await import("@clack/prompts");
144
+ l.error("--form is required for JSON output.");
145
+ process.exitCode = 1;
146
+ return null;
147
+ }
148
+ const { text: textPrompt, isCancel: isCancelled } = await import("@clack/prompts");
149
+ const input = await textPrompt({ message: "Form model ID" });
150
+ if (isCancelled(input)) return null;
151
+ return input;
152
+ }
153
+ function summarizeData(data) {
154
+ return Object.entries(data).slice(0, 2).map(([k, v]) => {
155
+ const val = typeof v === "string" ? v : JSON.stringify(v);
156
+ return `${k}: ${val.length > 20 ? val.slice(0, 17) + "..." : val}`;
157
+ }).join(", ") || "(empty)";
158
+ }
159
+ function timeAgo(dateStr) {
160
+ const diff = Date.now() - new Date(dateStr).getTime();
161
+ const minutes = Math.floor(diff / 6e4);
162
+ if (minutes < 1) return "just now";
163
+ if (minutes < 60) return `${minutes}m ago`;
164
+ const hours = Math.floor(minutes / 60);
165
+ if (hours < 24) return `${hours}h ago`;
166
+ return `${Math.floor(hours / 24)}d ago`;
167
+ }
168
+ //#endregion
169
+ export { submissions_default as default };