runline 0.8.1 → 0.10.0
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/dist/plugin/loader.js +41 -25
- package/dist/plugins/linear/src/attachments.js +29 -7
- package/dist/plugins/linear/src/comments.js +20 -6
- package/dist/plugins/linear/src/cycles.js +3 -1
- package/dist/plugins/linear/src/index.js +4 -0
- package/dist/plugins/linear/src/initiatives.js +9 -4
- package/dist/plugins/linear/src/issues.js +29 -7
- package/dist/plugins/linear/src/labels.js +6 -1
- package/dist/plugins/linear/src/organization.js +2 -1
- package/dist/plugins/linear/src/projects.js +12 -1
- package/dist/plugins/linear/src/shared.js +103 -0
- package/dist/plugins/linear/src/states.js +3 -1
- package/dist/plugins/linear/src/teams.js +4 -1
- package/dist/plugins/linear/src/users.js +2 -1
- package/dist/plugins/linear/src/views.js +24 -15
- package/dist/plugins/linear/src/webhooks.js +5 -1
- package/dist/plugins/steel/src/browser.js +175 -0
- package/dist/plugins/steel/src/captchas.js +19 -0
- package/dist/plugins/steel/src/credentials.js +38 -0
- package/dist/plugins/steel/src/extensions.js +46 -0
- package/dist/plugins/steel/src/files.js +96 -0
- package/dist/plugins/steel/src/index.js +21 -374
- package/dist/plugins/steel/src/profiles.js +55 -0
- package/dist/plugins/steel/src/sessions.js +119 -0
- package/dist/plugins/steel/src/shared.js +72 -0
- package/dist/plugins/vercel/src/account.js +11 -0
- package/dist/plugins/vercel/src/deployments.js +79 -0
- package/dist/plugins/vercel/src/env.js +101 -0
- package/dist/plugins/vercel/src/index.js +27 -0
- package/dist/plugins/vercel/src/projects.js +29 -0
- package/dist/plugins/vercel/src/shared.js +73 -0
- package/package.json +9 -1
package/dist/plugin/loader.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
|
-
import { dirname, join, resolve } from "node:path";
|
|
3
|
+
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { createJiti } from "jiti";
|
|
6
6
|
import { findConfigDir } from "../config/loader.js";
|
|
@@ -81,6 +81,19 @@ async function loadFromDirectory(dir) {
|
|
|
81
81
|
join(fullPath, "src", "index.ts"),
|
|
82
82
|
join(fullPath, "src", "index.js"),
|
|
83
83
|
];
|
|
84
|
+
const pkgJson = join(fullPath, "package.json");
|
|
85
|
+
let packagePluginPaths = [];
|
|
86
|
+
if (existsSync(pkgJson)) {
|
|
87
|
+
try {
|
|
88
|
+
const pkg = JSON.parse(readFileSync(pkgJson, "utf-8"));
|
|
89
|
+
if (pkg.main)
|
|
90
|
+
candidates.unshift(join(fullPath, pkg.main));
|
|
91
|
+
packagePluginPaths = pkg.runline?.plugins ?? [];
|
|
92
|
+
}
|
|
93
|
+
catch (err) {
|
|
94
|
+
console.error(`[runline] Failed to parse ${pkgJson}:`, err.message);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
84
97
|
const found = candidates.find((c) => existsSync(c));
|
|
85
98
|
if (found) {
|
|
86
99
|
try {
|
|
@@ -90,22 +103,15 @@ async function loadFromDirectory(dir) {
|
|
|
90
103
|
console.error(`[runline] Failed to load plugin from ${found}:`, err.message);
|
|
91
104
|
}
|
|
92
105
|
}
|
|
93
|
-
|
|
94
|
-
|
|
106
|
+
else if (packagePluginPaths.length === 0) {
|
|
107
|
+
console.error(`[runline] Failed to load plugin from ${fullPath}: No entry point found`);
|
|
108
|
+
}
|
|
109
|
+
for (const p of packagePluginPaths) {
|
|
95
110
|
try {
|
|
96
|
-
|
|
97
|
-
const pluginPaths = pkg.runline?.plugins ?? [];
|
|
98
|
-
for (const p of pluginPaths) {
|
|
99
|
-
try {
|
|
100
|
-
plugins.push(await loadPluginFromPath(join(fullPath, p)));
|
|
101
|
-
}
|
|
102
|
-
catch (err) {
|
|
103
|
-
console.error(`[runline] Failed to load plugin from ${join(fullPath, p)}:`, err.message);
|
|
104
|
-
}
|
|
105
|
-
}
|
|
111
|
+
plugins.push(await loadPluginFromPath(join(fullPath, p)));
|
|
106
112
|
}
|
|
107
113
|
catch (err) {
|
|
108
|
-
console.error(`[runline] Failed to
|
|
114
|
+
console.error(`[runline] Failed to load plugin from ${join(fullPath, p)}:`, err.message);
|
|
109
115
|
}
|
|
110
116
|
}
|
|
111
117
|
}
|
|
@@ -120,13 +126,21 @@ export async function loadPluginsFromConfig(configDir) {
|
|
|
120
126
|
try {
|
|
121
127
|
const data = JSON.parse(readFileSync(pluginsFile, "utf-8"));
|
|
122
128
|
const entries = data.plugins ?? data;
|
|
129
|
+
if (!Array.isArray(entries)) {
|
|
130
|
+
throw new Error("Expected an array or { plugins: [...] }");
|
|
131
|
+
}
|
|
123
132
|
for (const entry of entries) {
|
|
124
|
-
const p = typeof entry === "string" ? entry : entry
|
|
133
|
+
const p = typeof entry === "string" ? entry : entry?.path;
|
|
134
|
+
if (typeof p !== "string" || p.length === 0) {
|
|
135
|
+
console.error(`[runline] Invalid plugin entry in ${pluginsFile}: expected string path or { path }`);
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
const pluginPath = isAbsolute(p) ? p : join(configDir, p);
|
|
125
139
|
try {
|
|
126
|
-
plugins.push(await loadPluginFromPath(
|
|
140
|
+
plugins.push(await loadPluginFromPath(pluginPath));
|
|
127
141
|
}
|
|
128
142
|
catch (err) {
|
|
129
|
-
console.error(`[runline] Failed to load plugin from ${
|
|
143
|
+
console.error(`[runline] Failed to load plugin from ${pluginPath}:`, err.message);
|
|
130
144
|
}
|
|
131
145
|
}
|
|
132
146
|
}
|
|
@@ -146,32 +160,34 @@ export function defaultBuiltinDir() {
|
|
|
146
160
|
export async function discoverPlugins(configDir, options = {}) {
|
|
147
161
|
const loaded = new Set();
|
|
148
162
|
const result = [];
|
|
149
|
-
function addIfNew(plugin) {
|
|
150
|
-
if (
|
|
151
|
-
|
|
152
|
-
|
|
163
|
+
function addIfNew(plugin, source) {
|
|
164
|
+
if (loaded.has(plugin.name)) {
|
|
165
|
+
console.error(`[runline] Skipping duplicate plugin "${plugin.name}" from ${source}: a plugin with that name is already loaded`);
|
|
166
|
+
return;
|
|
153
167
|
}
|
|
168
|
+
result.push(plugin);
|
|
169
|
+
loaded.add(plugin.name);
|
|
154
170
|
}
|
|
155
171
|
if (configDir) {
|
|
156
172
|
const projectPluginsDir = join(configDir, "plugins");
|
|
157
173
|
const projectPlugins = await loadFromDirectory(projectPluginsDir);
|
|
158
174
|
for (const p of projectPlugins)
|
|
159
|
-
addIfNew(p);
|
|
175
|
+
addIfNew(p, projectPluginsDir);
|
|
160
176
|
const configPlugins = await loadPluginsFromConfig(configDir);
|
|
161
177
|
for (const p of configPlugins)
|
|
162
|
-
addIfNew(p);
|
|
178
|
+
addIfNew(p, join(configDir, "plugins.json"));
|
|
163
179
|
}
|
|
164
180
|
const globalDir = join(homedir(), ".runline", "plugins");
|
|
165
181
|
const globalPlugins = await loadFromDirectory(globalDir);
|
|
166
182
|
for (const p of globalPlugins)
|
|
167
|
-
addIfNew(p);
|
|
183
|
+
addIfNew(p, globalDir);
|
|
168
184
|
const builtinDir = options.builtinDir ?? defaultBuiltinDir();
|
|
169
185
|
const builtinPlugins = await loadFromDirectory(builtinDir);
|
|
170
186
|
for (const p of builtinPlugins) {
|
|
171
187
|
if (options.builtinAllowlist && !options.builtinAllowlist.has(p.name)) {
|
|
172
188
|
continue;
|
|
173
189
|
}
|
|
174
|
-
addIfNew(p);
|
|
190
|
+
addIfNew(p, builtinDir);
|
|
175
191
|
}
|
|
176
192
|
return result;
|
|
177
193
|
}
|
|
@@ -1,10 +1,26 @@
|
|
|
1
1
|
import * as t from "typebox";
|
|
2
|
-
import { ATTACHMENT_FIELDS,
|
|
2
|
+
import { ATTACHMENT_FIELDS, assertAttachmentInScope, assertIssueInScope, gql, key, requireUnscoped } from "./shared.js";
|
|
3
3
|
export function registerAttachmentActions(rl) {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
rl.registerAction("attachment.list", {
|
|
5
|
+
description: "List issue attachments. Disabled for scoped Linear connections.",
|
|
6
|
+
inputSchema: t.Object({ limit: t.Optional(t.Number()) }),
|
|
7
|
+
async execute(input, ctx) {
|
|
8
|
+
requireUnscoped(ctx, "attachment.list");
|
|
9
|
+
const limit = input?.limit ?? 50;
|
|
10
|
+
const data = await gql(key(ctx), `query($first: Int) { attachments(first: $first) { nodes { ${ATTACHMENT_FIELDS} } pageInfo { hasNextPage endCursor } } }`, { first: limit });
|
|
11
|
+
return data.attachments;
|
|
12
|
+
},
|
|
13
|
+
});
|
|
14
|
+
rl.registerAction("attachment.get", {
|
|
15
|
+
description: "Get an attachment by ID.",
|
|
16
|
+
inputSchema: t.Object({ id: t.String() }),
|
|
17
|
+
async execute(input, ctx) {
|
|
18
|
+
const id = input.id;
|
|
19
|
+
await assertAttachmentInScope(ctx, id);
|
|
20
|
+
const data = await gql(key(ctx), `query($id: String!) { attachment(id: $id) { ${ATTACHMENT_FIELDS} } }`, { id });
|
|
21
|
+
return data.attachment;
|
|
22
|
+
},
|
|
23
|
+
});
|
|
8
24
|
rl.registerAction("attachment.create", {
|
|
9
25
|
description: "Create an attachment on an issue.",
|
|
10
26
|
inputSchema: t.Object({
|
|
@@ -19,7 +35,9 @@ export function registerAttachmentActions(rl) {
|
|
|
19
35
|
id: t.Optional(t.String({ description: "The identifier in UUID v4 format. If none is provided, the backend will generate one" })),
|
|
20
36
|
}),
|
|
21
37
|
async execute(input, ctx) {
|
|
22
|
-
const
|
|
38
|
+
const fields = input;
|
|
39
|
+
await assertIssueInScope(ctx, String(fields.issueId));
|
|
40
|
+
const data = await gql(key(ctx), `mutation($input: AttachmentCreateInput!) { attachmentCreate(input: $input) { success attachment { ${ATTACHMENT_FIELDS} } } }`, { input: fields });
|
|
23
41
|
return data.attachmentCreate?.attachment;
|
|
24
42
|
},
|
|
25
43
|
});
|
|
@@ -34,6 +52,7 @@ export function registerAttachmentActions(rl) {
|
|
|
34
52
|
}),
|
|
35
53
|
async execute(input, ctx) {
|
|
36
54
|
const { id, ...fields } = input;
|
|
55
|
+
await assertAttachmentInScope(ctx, String(id));
|
|
37
56
|
const data = await gql(key(ctx), `mutation($id: String!, $input: AttachmentUpdateInput!) { attachmentUpdate(id: $id, input: $input) { success attachment { ${ATTACHMENT_FIELDS} } } }`, { id, input: fields });
|
|
38
57
|
return data.attachmentUpdate?.attachment;
|
|
39
58
|
},
|
|
@@ -48,6 +67,7 @@ export function registerAttachmentActions(rl) {
|
|
|
48
67
|
}),
|
|
49
68
|
async execute(input, ctx) {
|
|
50
69
|
const { issueId, url, title, id } = input;
|
|
70
|
+
await assertIssueInScope(ctx, String(issueId));
|
|
51
71
|
const data = await gql(key(ctx), `mutation($issueId: String!, $url: String!, $title: String, $id: String) {
|
|
52
72
|
attachmentLinkURL(issueId: $issueId, url: $url, title: $title, id: $id) { success attachment { ${ATTACHMENT_FIELDS} } }
|
|
53
73
|
}`, { issueId, url, title: title ?? null, id: id ?? null });
|
|
@@ -58,7 +78,9 @@ export function registerAttachmentActions(rl) {
|
|
|
58
78
|
description: "Delete an attachment.",
|
|
59
79
|
inputSchema: t.Object({ id: t.String({ description: "The identifier of the attachment to delete" }) }),
|
|
60
80
|
async execute(input, ctx) {
|
|
61
|
-
const
|
|
81
|
+
const id = input.id;
|
|
82
|
+
await assertAttachmentInScope(ctx, id);
|
|
83
|
+
const data = await gql(key(ctx), `mutation($id: String!) { attachmentDelete(id: $id) { success } }`, { id });
|
|
62
84
|
return data.attachmentDelete;
|
|
63
85
|
},
|
|
64
86
|
});
|
|
@@ -1,28 +1,39 @@
|
|
|
1
1
|
import * as t from "typebox";
|
|
2
|
-
import { COMMENT_FIELDS,
|
|
2
|
+
import { COMMENT_FIELDS, assertCommentInScope, assertIssueInScope, gql, key, requireUnscoped } from "./shared.js";
|
|
3
3
|
export function registerCommentActions(rl) {
|
|
4
|
-
const listAction = bindListAction(rl);
|
|
5
4
|
rl.registerAction("issue.addComment", {
|
|
6
5
|
description: "Add a comment to an issue. Pass parentId to nest as a reply.",
|
|
7
6
|
inputSchema: t.Object({
|
|
8
7
|
issueId: t.String({ description: "The issue to associate the comment with. UUID or issue identifier (e.g., 'LIN-123')" }),
|
|
9
8
|
body: t.String({ description: "The comment content in markdown format" }),
|
|
10
|
-
parentId: t.Optional(t.String({ description: "The parent comment under which to nest
|
|
9
|
+
parentId: t.Optional(t.String({ description: "The parent comment under which to nest as a reply" })),
|
|
11
10
|
doNotSubscribeToIssue: t.Optional(t.Boolean({ description: "Prevent auto-subscription to the issue the comment is created on" })),
|
|
12
11
|
quotedText: t.Optional(t.String({ description: "The text that this comment references (inline comments)" })),
|
|
13
12
|
}),
|
|
14
13
|
async execute(input, ctx) {
|
|
15
14
|
const fields = input;
|
|
15
|
+
await assertIssueInScope(ctx, String(fields.issueId));
|
|
16
16
|
const data = await gql(key(ctx), `mutation($input: CommentCreateInput!) { commentCreate(input: $input) { success comment { ${COMMENT_FIELDS} } } }`, { input: fields });
|
|
17
17
|
return data.commentCreate?.comment;
|
|
18
18
|
},
|
|
19
19
|
});
|
|
20
|
-
|
|
20
|
+
rl.registerAction("comment.list", {
|
|
21
|
+
description: "List comments across the workspace. Disabled for scoped Linear connections; use issue.listComments instead.",
|
|
22
|
+
inputSchema: t.Object({ limit: t.Optional(t.Number()) }),
|
|
23
|
+
async execute(input, ctx) {
|
|
24
|
+
requireUnscoped(ctx, "comment.list");
|
|
25
|
+
const limit = input?.limit ?? 50;
|
|
26
|
+
const data = await gql(key(ctx), `query($first: Int) { comments(first: $first) { nodes { ${COMMENT_FIELDS} } pageInfo { hasNextPage endCursor } } }`, { first: limit });
|
|
27
|
+
return data.comments;
|
|
28
|
+
},
|
|
29
|
+
});
|
|
21
30
|
rl.registerAction("comment.get", {
|
|
22
31
|
description: "Get a comment by ID.",
|
|
23
32
|
inputSchema: t.Object({ id: t.String() }),
|
|
24
33
|
async execute(input, ctx) {
|
|
25
|
-
const
|
|
34
|
+
const id = input.id;
|
|
35
|
+
await assertCommentInScope(ctx, id);
|
|
36
|
+
const data = await gql(key(ctx), `query($id: String!) { comment(id: $id) { ${COMMENT_FIELDS} } }`, { id });
|
|
26
37
|
return data.comment;
|
|
27
38
|
},
|
|
28
39
|
});
|
|
@@ -35,6 +46,7 @@ export function registerCommentActions(rl) {
|
|
|
35
46
|
}),
|
|
36
47
|
async execute(input, ctx) {
|
|
37
48
|
const { id, ...fields } = input;
|
|
49
|
+
await assertCommentInScope(ctx, String(id));
|
|
38
50
|
const data = await gql(key(ctx), `mutation($id: String!, $input: CommentUpdateInput!) { commentUpdate(id: $id, input: $input) { success comment { ${COMMENT_FIELDS} } } }`, { id, input: fields });
|
|
39
51
|
return data.commentUpdate?.comment;
|
|
40
52
|
},
|
|
@@ -43,7 +55,9 @@ export function registerCommentActions(rl) {
|
|
|
43
55
|
description: "Delete a comment.",
|
|
44
56
|
inputSchema: t.Object({ id: t.String() }),
|
|
45
57
|
async execute(input, ctx) {
|
|
46
|
-
const
|
|
58
|
+
const id = input.id;
|
|
59
|
+
await assertCommentInScope(ctx, id);
|
|
60
|
+
const data = await gql(key(ctx), `mutation($id: String!) { commentDelete(id: $id) { success } }`, { id });
|
|
47
61
|
return data.commentDelete;
|
|
48
62
|
},
|
|
49
63
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as t from "typebox";
|
|
2
|
-
import { CYCLE_FIELDS, bindGetAction, bindListAction, gql, key } from "./shared.js";
|
|
2
|
+
import { CYCLE_FIELDS, bindGetAction, bindListAction, gql, key, requireUnscoped } from "./shared.js";
|
|
3
3
|
export function registerCycleActions(rl) {
|
|
4
4
|
const listAction = bindListAction(rl);
|
|
5
5
|
const getAction = bindGetAction(rl);
|
|
@@ -17,6 +17,7 @@ export function registerCycleActions(rl) {
|
|
|
17
17
|
id: t.Optional(t.String({ description: "The identifier in UUID v4 format. If none is provided, the backend will generate one" })),
|
|
18
18
|
}),
|
|
19
19
|
async execute(input, ctx) {
|
|
20
|
+
requireUnscoped(ctx, "cycles.*");
|
|
20
21
|
const data = await gql(key(ctx), `mutation($input: CycleCreateInput!) { cycleCreate(input: $input) { success cycle { ${CYCLE_FIELDS} } } }`, { input: input });
|
|
21
22
|
return data.cycleCreate?.cycle;
|
|
22
23
|
},
|
|
@@ -32,6 +33,7 @@ export function registerCycleActions(rl) {
|
|
|
32
33
|
completedAt: t.Optional(t.String({ description: "The completion time of the cycle (DateTime). If null, the cycle hasn't been completed" })),
|
|
33
34
|
}),
|
|
34
35
|
async execute(input, ctx) {
|
|
36
|
+
requireUnscoped(ctx, "cycles.*");
|
|
35
37
|
const { id, ...fields } = input;
|
|
36
38
|
const data = await gql(key(ctx), `mutation($id: String!, $input: CycleUpdateInput!) { cycleUpdate(id: $id, input: $input) { success cycle { ${CYCLE_FIELDS} } } }`, { id, input: fields });
|
|
37
39
|
return data.cycleUpdate?.cycle;
|
|
@@ -20,6 +20,10 @@ export default function linear(rl) {
|
|
|
20
20
|
description: "Linear API key (https://linear.app/settings/account/security)",
|
|
21
21
|
env: "LINEAR_API_KEY",
|
|
22
22
|
}),
|
|
23
|
+
scopeLabelIds: t.Optional(t.String({
|
|
24
|
+
description: "Comma-separated Linear issue label IDs. When set, issue/comment/attachment access is restricted to issues with one of these labels.",
|
|
25
|
+
env: "LINEAR_SCOPE_LABEL_IDS",
|
|
26
|
+
})),
|
|
23
27
|
}));
|
|
24
28
|
registerIssueActions(rl);
|
|
25
29
|
registerCommentActions(rl);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as t from "typebox";
|
|
2
|
-
import { INITIATIVE_FIELDS, bindGetAction, bindListAction, gql, key } from "./shared.js";
|
|
2
|
+
import { INITIATIVE_FIELDS, bindGetAction, bindListAction, gql, key, requireUnscoped } from "./shared.js";
|
|
3
3
|
export function registerInitiativeActions(rl) {
|
|
4
4
|
const listAction = bindListAction(rl);
|
|
5
5
|
const getAction = bindGetAction(rl);
|
|
@@ -21,6 +21,7 @@ export function registerInitiativeActions(rl) {
|
|
|
21
21
|
id: t.Optional(t.String({ description: "The identifier in UUID v4 format. If none is provided, the backend will generate one" })),
|
|
22
22
|
}),
|
|
23
23
|
async execute(input, ctx) {
|
|
24
|
+
requireUnscoped(ctx, "initiatives.*");
|
|
24
25
|
const data = await gql(key(ctx), `mutation($input: InitiativeCreateInput!) { initiativeCreate(input: $input) { success initiative { ${INITIATIVE_FIELDS} } } }`, { input: input });
|
|
25
26
|
return data.initiativeCreate?.initiative;
|
|
26
27
|
},
|
|
@@ -42,6 +43,7 @@ export function registerInitiativeActions(rl) {
|
|
|
42
43
|
trashed: t.Optional(t.Boolean({ description: "Whether the initiative has been trashed. Set to true to trash, or null to restore" })),
|
|
43
44
|
}),
|
|
44
45
|
async execute(input, ctx) {
|
|
46
|
+
requireUnscoped(ctx, "initiatives.*");
|
|
45
47
|
const { id, ...fields } = input;
|
|
46
48
|
const data = await gql(key(ctx), `mutation($id: String!, $input: InitiativeUpdateInput!) { initiativeUpdate(id: $id, input: $input) { success initiative { ${INITIATIVE_FIELDS} } } }`, { id, input: fields });
|
|
47
49
|
return data.initiativeUpdate?.initiative;
|
|
@@ -51,12 +53,13 @@ export function registerInitiativeActions(rl) {
|
|
|
51
53
|
description: "Trash an initiative.",
|
|
52
54
|
inputSchema: t.Object({ id: t.String({ description: "The identifier of the initiative to delete" }) }),
|
|
53
55
|
async execute(input, ctx) {
|
|
56
|
+
requireUnscoped(ctx, "initiatives.*");
|
|
54
57
|
const data = await gql(key(ctx), `mutation($id: String!) { initiativeDelete(id: $id) { success } }`, { id: input.id });
|
|
55
58
|
return data.initiativeDelete;
|
|
56
59
|
},
|
|
57
60
|
});
|
|
58
61
|
rl.registerAction("initiative.addProject", {
|
|
59
|
-
description: "Associate a project with an initiative.
|
|
62
|
+
description: "Associate a project with an initiative. Use this action for project-to-initiative linking; project.update does not accept initiativeId. Verify with initiative.get or the returned initiative.projects list.",
|
|
60
63
|
inputSchema: t.Object({
|
|
61
64
|
initiativeId: t.String({ description: "The identifier of the initiative" }),
|
|
62
65
|
projectId: t.String({ description: "The identifier of the project" }),
|
|
@@ -64,14 +67,16 @@ export function registerInitiativeActions(rl) {
|
|
|
64
67
|
id: t.Optional(t.String({ description: "The identifier in UUID v4 format. If none is provided, the backend will generate one" })),
|
|
65
68
|
}),
|
|
66
69
|
async execute(input, ctx) {
|
|
67
|
-
|
|
70
|
+
requireUnscoped(ctx, "initiatives.*");
|
|
71
|
+
const data = await gql(key(ctx), `mutation($input: InitiativeToProjectCreateInput!) { initiativeToProjectCreate(input: $input) { success initiativeToProject { id initiative { id name projects { nodes { id name } } } project { id name } } } }`, { input: input });
|
|
68
72
|
return data.initiativeToProjectCreate;
|
|
69
73
|
},
|
|
70
74
|
});
|
|
71
75
|
rl.registerAction("initiative.removeProject", {
|
|
72
|
-
description: "Remove a project from an initiative. Pass the link id returned by initiative.addProject.",
|
|
76
|
+
description: "Remove a project from an initiative. Pass the link id returned by initiative.addProject, then verify with initiative.get.",
|
|
73
77
|
inputSchema: t.Object({ id: t.String({ description: "The identifier of the initiativeToProject to delete" }) }),
|
|
74
78
|
async execute(input, ctx) {
|
|
79
|
+
requireUnscoped(ctx, "initiatives.*");
|
|
75
80
|
const data = await gql(key(ctx), `mutation($id: String!) { initiativeToProjectDelete(id: $id) { success } }`, { id: input.id });
|
|
76
81
|
return data.initiativeToProjectDelete;
|
|
77
82
|
},
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as t from "typebox";
|
|
2
|
-
import { COMMENT_FIELDS, ISSUE_FIELDS, ISSUE_LITE, LIST_INPUT_SCHEMA, buildConnArgs, gql, key, } from "./shared.js";
|
|
2
|
+
import { COMMENT_FIELDS, ISSUE_FIELDS, ISSUE_LITE, LIST_INPUT_SCHEMA, assertIssueInScope, buildConnArgs, ensureScopeLabelsOnCreateOrReplace, forbidScopeLabelRemoval, gql, issueHasScope, key, mergeIssueScopeFilter, } from "./shared.js";
|
|
3
3
|
export function registerIssueActions(rl) {
|
|
4
4
|
rl.registerAction("issue.create", {
|
|
5
5
|
description: "Create an issue. teamId is required; title is required unless a template is applied.",
|
|
@@ -26,7 +26,8 @@ export function registerIssueActions(rl) {
|
|
|
26
26
|
id: t.Optional(t.String({ description: "The identifier in UUID v4 format. If none is provided, the backend will generate one" })),
|
|
27
27
|
}),
|
|
28
28
|
async execute(input, ctx) {
|
|
29
|
-
const fields = input;
|
|
29
|
+
const fields = { ...input };
|
|
30
|
+
fields.labelIds = ensureScopeLabelsOnCreateOrReplace(ctx, fields.labelIds);
|
|
30
31
|
const data = await gql(key(ctx), `mutation($input: IssueCreateInput!) { issueCreate(input: $input) { success issue { ${ISSUE_FIELDS} } } }`, { input: fields });
|
|
31
32
|
return data.issueCreate?.issue;
|
|
32
33
|
},
|
|
@@ -36,7 +37,10 @@ export function registerIssueActions(rl) {
|
|
|
36
37
|
inputSchema: t.Object({ issueId: t.String() }),
|
|
37
38
|
async execute(input, ctx) {
|
|
38
39
|
const data = await gql(key(ctx), `query($id: String!) { issue(id: $id) { ${ISSUE_FIELDS} } }`, { id: input.issueId });
|
|
39
|
-
|
|
40
|
+
const issue = data.issue;
|
|
41
|
+
if (!issueHasScope(ctx, issue))
|
|
42
|
+
throw new Error("Linear issue is not available to this scoped connection");
|
|
43
|
+
return issue;
|
|
40
44
|
},
|
|
41
45
|
});
|
|
42
46
|
rl.registerAction("issue.list", {
|
|
@@ -54,7 +58,7 @@ export function registerIssueActions(rl) {
|
|
|
54
58
|
merged.team = { id: { eq: opts.teamId } };
|
|
55
59
|
if (opts.assigneeId)
|
|
56
60
|
merged.assignee = { id: { eq: opts.assigneeId } };
|
|
57
|
-
const filter = Object.keys(merged).length > 0 ? merged : undefined;
|
|
61
|
+
const filter = mergeIssueScopeFilter(ctx, Object.keys(merged).length > 0 ? merged : undefined);
|
|
58
62
|
const { argsDecl, argsCall, vars } = buildConnArgs({ ...opts, filter }, "IssueFilter");
|
|
59
63
|
const data = await gql(key(ctx), `query${argsDecl} { issues${argsCall} { nodes { ${ISSUE_LITE} } pageInfo { hasNextPage endCursor } } }`, vars);
|
|
60
64
|
const conn = data.issues;
|
|
@@ -91,6 +95,11 @@ export function registerIssueActions(rl) {
|
|
|
91
95
|
}),
|
|
92
96
|
async execute(input, ctx) {
|
|
93
97
|
const { issueId, ...fields } = input;
|
|
98
|
+
await assertIssueInScope(ctx, String(issueId));
|
|
99
|
+
if (fields.removedLabelIds)
|
|
100
|
+
forbidScopeLabelRemoval(ctx, fields.removedLabelIds);
|
|
101
|
+
if (fields.labelIds)
|
|
102
|
+
fields.labelIds = ensureScopeLabelsOnCreateOrReplace(ctx, fields.labelIds);
|
|
94
103
|
const data = await gql(key(ctx), `mutation($id: String!, $input: IssueUpdateInput!) { issueUpdate(id: $id, input: $input) { success issue { ${ISSUE_FIELDS} } } }`, { id: issueId, input: fields });
|
|
95
104
|
return data.issueUpdate?.issue;
|
|
96
105
|
},
|
|
@@ -103,6 +112,7 @@ export function registerIssueActions(rl) {
|
|
|
103
112
|
}),
|
|
104
113
|
async execute(input, ctx) {
|
|
105
114
|
const { issueId, permanentlyDelete } = input;
|
|
115
|
+
await assertIssueInScope(ctx, issueId);
|
|
106
116
|
const data = await gql(key(ctx), `mutation($id: String!, $perm: Boolean) { issueDelete(id: $id, permanentlyDelete: $perm) { success } }`, { id: issueId, perm: permanentlyDelete ?? null });
|
|
107
117
|
return data.issueDelete;
|
|
108
118
|
},
|
|
@@ -115,6 +125,7 @@ export function registerIssueActions(rl) {
|
|
|
115
125
|
}),
|
|
116
126
|
async execute(input, ctx) {
|
|
117
127
|
const { issueId, trash } = input;
|
|
128
|
+
await assertIssueInScope(ctx, issueId);
|
|
118
129
|
const data = await gql(key(ctx), `mutation($id: String!, $trash: Boolean) { issueArchive(id: $id, trash: $trash) { success } }`, { id: issueId, trash: trash ?? null });
|
|
119
130
|
return data.issueArchive;
|
|
120
131
|
},
|
|
@@ -123,7 +134,9 @@ export function registerIssueActions(rl) {
|
|
|
123
134
|
description: "Unarchive an issue.",
|
|
124
135
|
inputSchema: t.Object({ issueId: t.String() }),
|
|
125
136
|
async execute(input, ctx) {
|
|
126
|
-
const
|
|
137
|
+
const issueId = input.issueId;
|
|
138
|
+
await assertIssueInScope(ctx, issueId);
|
|
139
|
+
const data = await gql(key(ctx), `mutation($id: String!) { issueUnarchive(id: $id) { success } }`, { id: issueId });
|
|
127
140
|
return data.issueUnarchive;
|
|
128
141
|
},
|
|
129
142
|
});
|
|
@@ -151,7 +164,7 @@ export function registerIssueActions(rl) {
|
|
|
151
164
|
}`, {
|
|
152
165
|
term: opts.term,
|
|
153
166
|
first: opts.limit ?? 50,
|
|
154
|
-
filter: opts.filter ?? null,
|
|
167
|
+
filter: mergeIssueScopeFilter(ctx, opts.filter) ?? null,
|
|
155
168
|
includeComments: opts.includeComments ?? null,
|
|
156
169
|
includeArchived: opts.includeArchived ?? null,
|
|
157
170
|
teamId: opts.teamId ?? null,
|
|
@@ -170,6 +183,7 @@ export function registerIssueActions(rl) {
|
|
|
170
183
|
}),
|
|
171
184
|
async execute(input, ctx) {
|
|
172
185
|
const { issueId, labelId } = input;
|
|
186
|
+
await assertIssueInScope(ctx, issueId);
|
|
173
187
|
const data = await gql(key(ctx), `mutation($id: String!, $labelId: String!) { issueAddLabel(id: $id, labelId: $labelId) { success } }`, { id: issueId, labelId });
|
|
174
188
|
return data.issueAddLabel;
|
|
175
189
|
},
|
|
@@ -182,6 +196,8 @@ export function registerIssueActions(rl) {
|
|
|
182
196
|
}),
|
|
183
197
|
async execute(input, ctx) {
|
|
184
198
|
const { issueId, labelId } = input;
|
|
199
|
+
await assertIssueInScope(ctx, issueId);
|
|
200
|
+
forbidScopeLabelRemoval(ctx, labelId);
|
|
185
201
|
const data = await gql(key(ctx), `mutation($id: String!, $labelId: String!) { issueRemoveLabel(id: $id, labelId: $labelId) { success } }`, { id: issueId, labelId });
|
|
186
202
|
return data.issueRemoveLabel;
|
|
187
203
|
},
|
|
@@ -195,6 +211,7 @@ export function registerIssueActions(rl) {
|
|
|
195
211
|
}),
|
|
196
212
|
async execute(input, ctx) {
|
|
197
213
|
const { issueId, userId, userEmail } = input;
|
|
214
|
+
await assertIssueInScope(ctx, String(issueId));
|
|
198
215
|
const data = await gql(key(ctx), `mutation($id: String!, $userId: String, $userEmail: String) {
|
|
199
216
|
issueSubscribe(id: $id, userId: $userId, userEmail: $userEmail) { success }
|
|
200
217
|
}`, { id: issueId, userId: userId ?? null, userEmail: userEmail ?? null });
|
|
@@ -210,6 +227,7 @@ export function registerIssueActions(rl) {
|
|
|
210
227
|
}),
|
|
211
228
|
async execute(input, ctx) {
|
|
212
229
|
const { issueId, userId, userEmail } = input;
|
|
230
|
+
await assertIssueInScope(ctx, String(issueId));
|
|
213
231
|
const data = await gql(key(ctx), `mutation($id: String!, $userId: String, $userEmail: String) {
|
|
214
232
|
issueUnsubscribe(id: $id, userId: $userId, userEmail: $userEmail) { success }
|
|
215
233
|
}`, { id: issueId, userId: userId ?? null, userEmail: userEmail ?? null });
|
|
@@ -224,7 +242,10 @@ export function registerIssueActions(rl) {
|
|
|
224
242
|
type: t.String({ description: "IssueRelationType: blocks | duplicate | related | similar" }),
|
|
225
243
|
}),
|
|
226
244
|
async execute(input, ctx) {
|
|
227
|
-
const
|
|
245
|
+
const fields = input;
|
|
246
|
+
await assertIssueInScope(ctx, String(fields.issueId));
|
|
247
|
+
await assertIssueInScope(ctx, String(fields.relatedIssueId));
|
|
248
|
+
const data = await gql(key(ctx), `mutation($input: IssueRelationCreateInput!) { issueRelationCreate(input: $input) { success issueRelation { id type } } }`, { input: fields });
|
|
228
249
|
return data.issueRelationCreate;
|
|
229
250
|
},
|
|
230
251
|
});
|
|
@@ -236,6 +257,7 @@ export function registerIssueActions(rl) {
|
|
|
236
257
|
}),
|
|
237
258
|
async execute(input, ctx) {
|
|
238
259
|
const { issueId, limit } = input;
|
|
260
|
+
await assertIssueInScope(ctx, issueId);
|
|
239
261
|
const data = await gql(key(ctx), `query($id: String!, $first: Int) {
|
|
240
262
|
issue(id: $id) { comments(first: $first) { nodes { ${COMMENT_FIELDS} } } }
|
|
241
263
|
}`, { id: issueId, first: limit ?? 50 });
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as t from "typebox";
|
|
2
|
-
import { LABEL_FIELDS, bindGetAction, bindListAction, gql, key } from "./shared.js";
|
|
2
|
+
import { LABEL_FIELDS, bindGetAction, bindListAction, gql, key, requireUnscoped } from "./shared.js";
|
|
3
3
|
export function registerLabelActions(rl) {
|
|
4
4
|
const listAction = bindListAction(rl);
|
|
5
5
|
const getAction = bindGetAction(rl);
|
|
@@ -19,6 +19,7 @@ export function registerLabelActions(rl) {
|
|
|
19
19
|
replaceTeamLabels: t.Optional(t.Boolean({ description: "Replace all team-specific labels with the same name with this newly created workspace label (default false)" })),
|
|
20
20
|
}),
|
|
21
21
|
async execute(input, ctx) {
|
|
22
|
+
requireUnscoped(ctx, "label.create");
|
|
22
23
|
const { replaceTeamLabels, ...fields } = input;
|
|
23
24
|
const data = await gql(key(ctx), `mutation($input: IssueLabelCreateInput!, $replaceTeamLabels: Boolean) { issueLabelCreate(input: $input, replaceTeamLabels: $replaceTeamLabels) { success issueLabel { ${LABEL_FIELDS} } } }`, { input: fields, replaceTeamLabels: replaceTeamLabels ?? null });
|
|
24
25
|
return data.issueLabelCreate?.issueLabel;
|
|
@@ -37,6 +38,7 @@ export function registerLabelActions(rl) {
|
|
|
37
38
|
replaceTeamLabels: t.Optional(t.Boolean({ description: "Replace all team-specific labels with the same name with this updated workspace label (default false)" })),
|
|
38
39
|
}),
|
|
39
40
|
async execute(input, ctx) {
|
|
41
|
+
requireUnscoped(ctx, "label.update");
|
|
40
42
|
const { id, replaceTeamLabels, ...fields } = input;
|
|
41
43
|
const data = await gql(key(ctx), `mutation($id: String!, $input: IssueLabelUpdateInput!, $replaceTeamLabels: Boolean) { issueLabelUpdate(id: $id, input: $input, replaceTeamLabels: $replaceTeamLabels) { success issueLabel { ${LABEL_FIELDS} } } }`, { id, input: fields, replaceTeamLabels: replaceTeamLabels ?? null });
|
|
42
44
|
return data.issueLabelUpdate?.issueLabel;
|
|
@@ -46,6 +48,7 @@ export function registerLabelActions(rl) {
|
|
|
46
48
|
description: "Delete a label.",
|
|
47
49
|
inputSchema: t.Object({ id: t.String({ description: "The identifier of the label to delete" }) }),
|
|
48
50
|
async execute(input, ctx) {
|
|
51
|
+
requireUnscoped(ctx, "label.delete");
|
|
49
52
|
const data = await gql(key(ctx), `mutation($id: String!) { issueLabelDelete(id: $id) { success } }`, { id: input.id });
|
|
50
53
|
return data.issueLabelDelete;
|
|
51
54
|
},
|
|
@@ -54,6 +57,7 @@ export function registerLabelActions(rl) {
|
|
|
54
57
|
description: "Retire a label. Retired labels remain visible but cannot be applied to new issues.",
|
|
55
58
|
inputSchema: t.Object({ id: t.String({ description: "The identifier of the label to retire" }) }),
|
|
56
59
|
async execute(input, ctx) {
|
|
60
|
+
requireUnscoped(ctx, "label.retire");
|
|
57
61
|
const data = await gql(key(ctx), `mutation($id: String!) { issueLabelRetire(id: $id) { success issueLabel { ${LABEL_FIELDS} } } }`, { id: input.id });
|
|
58
62
|
return data.issueLabelRetire?.issueLabel;
|
|
59
63
|
},
|
|
@@ -62,6 +66,7 @@ export function registerLabelActions(rl) {
|
|
|
62
66
|
description: "Restore a previously retired label.",
|
|
63
67
|
inputSchema: t.Object({ id: t.String({ description: "The identifier of the label to restore" }) }),
|
|
64
68
|
async execute(input, ctx) {
|
|
69
|
+
requireUnscoped(ctx, "label.restore");
|
|
65
70
|
const data = await gql(key(ctx), `mutation($id: String!) { issueLabelRestore(id: $id) { success issueLabel { ${LABEL_FIELDS} } } }`, { id: input.id });
|
|
66
71
|
return data.issueLabelRestore?.issueLabel;
|
|
67
72
|
},
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import * as t from "typebox";
|
|
2
|
-
import { ORG_FIELDS, gql, key } from "./shared.js";
|
|
2
|
+
import { ORG_FIELDS, gql, key, requireUnscoped } from "./shared.js";
|
|
3
3
|
export function registerOrganizationActions(rl) {
|
|
4
4
|
rl.registerAction("org.get", {
|
|
5
5
|
description: "Get the authenticated workspace.",
|
|
6
6
|
inputSchema: t.Object({}),
|
|
7
7
|
async execute(_input, ctx) {
|
|
8
|
+
requireUnscoped(ctx, "org.get");
|
|
8
9
|
const data = await gql(key(ctx), `query { organization { ${ORG_FIELDS} } }`);
|
|
9
10
|
return data.organization;
|
|
10
11
|
},
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as t from "typebox";
|
|
2
|
-
import { MILESTONE_FIELDS, PROJECT_FIELDS, PROJECT_UPDATE_FIELDS, bindGetAction, bindListAction, gql, key, } from "./shared.js";
|
|
2
|
+
import { MILESTONE_FIELDS, PROJECT_FIELDS, PROJECT_UPDATE_FIELDS, bindGetAction, bindListAction, gql, key, requireUnscoped, } from "./shared.js";
|
|
3
3
|
export function registerProjectActions(rl) {
|
|
4
4
|
const listAction = bindListAction(rl);
|
|
5
5
|
const getAction = bindGetAction(rl);
|
|
@@ -31,6 +31,7 @@ export function registerProjectActions(rl) {
|
|
|
31
31
|
slackChannelName: t.Optional(t.String({ description: "The full name for the Slack channel to create (including prefix). Creates and connects a Slack channel if provided" })),
|
|
32
32
|
}),
|
|
33
33
|
async execute(input, ctx) {
|
|
34
|
+
requireUnscoped(ctx, "projects.*");
|
|
34
35
|
const { slackChannelName, ...fields } = input;
|
|
35
36
|
const data = await gql(key(ctx), `mutation($input: ProjectCreateInput!, $slackChannelName: String) { projectCreate(input: $input, slackChannelName: $slackChannelName) { success project { ${PROJECT_FIELDS} } } }`, { input: fields, slackChannelName: slackChannelName ?? null });
|
|
36
37
|
return data.projectCreate?.project;
|
|
@@ -61,6 +62,7 @@ export function registerProjectActions(rl) {
|
|
|
61
62
|
trashed: t.Optional(t.Boolean({ description: "Whether the project has been trashed. Set to true to trash, or null to restore" })),
|
|
62
63
|
}),
|
|
63
64
|
async execute(input, ctx) {
|
|
65
|
+
requireUnscoped(ctx, "projects.*");
|
|
64
66
|
const { id, ...fields } = input;
|
|
65
67
|
const data = await gql(key(ctx), `mutation($id: String!, $input: ProjectUpdateInput!) { projectUpdate(id: $id, input: $input) { success project { ${PROJECT_FIELDS} } } }`, { id, input: fields });
|
|
66
68
|
return data.projectUpdate?.project;
|
|
@@ -70,6 +72,7 @@ export function registerProjectActions(rl) {
|
|
|
70
72
|
description: "Trash (soft-delete) a project. Restorable via project.unarchive.",
|
|
71
73
|
inputSchema: t.Object({ id: t.String({ description: "The identifier of the project to delete" }), }),
|
|
72
74
|
async execute(input, ctx) {
|
|
75
|
+
requireUnscoped(ctx, "projects.*");
|
|
73
76
|
const data = await gql(key(ctx), `mutation($id: String!) { projectDelete(id: $id) { success } }`, { id: input.id });
|
|
74
77
|
return data.projectDelete;
|
|
75
78
|
},
|
|
@@ -78,6 +81,7 @@ export function registerProjectActions(rl) {
|
|
|
78
81
|
description: "Restore a previously trashed or archived project.",
|
|
79
82
|
inputSchema: t.Object({ id: t.String({ description: "The identifier of the project to restore (UUID or slug)" }), }),
|
|
80
83
|
async execute(input, ctx) {
|
|
84
|
+
requireUnscoped(ctx, "projects.*");
|
|
81
85
|
const data = await gql(key(ctx), `mutation($id: String!) { projectUnarchive(id: $id) { success } }`, { id: input.id });
|
|
82
86
|
return data.projectUnarchive;
|
|
83
87
|
},
|
|
@@ -91,6 +95,7 @@ export function registerProjectActions(rl) {
|
|
|
91
95
|
teamId: t.Optional(t.String({ description: "UUID of a team to boost in search results" })),
|
|
92
96
|
}),
|
|
93
97
|
async execute(input, ctx) {
|
|
98
|
+
requireUnscoped(ctx, "projects.*");
|
|
94
99
|
const opts = input;
|
|
95
100
|
const data = await gql(key(ctx), `query($term: String!, $first: Int, $includeComments: Boolean, $teamId: String) {
|
|
96
101
|
searchProjects(term: $term, first: $first, includeComments: $includeComments, teamId: $teamId) {
|
|
@@ -120,6 +125,7 @@ export function registerProjectActions(rl) {
|
|
|
120
125
|
id: t.Optional(t.String({ description: "The identifier in UUID v4 format. If none is provided, the backend will generate one" })),
|
|
121
126
|
}),
|
|
122
127
|
async execute(input, ctx) {
|
|
128
|
+
requireUnscoped(ctx, "projects.*");
|
|
123
129
|
const data = await gql(key(ctx), `mutation($input: ProjectMilestoneCreateInput!) { projectMilestoneCreate(input: $input) { success projectMilestone { ${MILESTONE_FIELDS} } } }`, { input: input });
|
|
124
130
|
return data.projectMilestoneCreate?.projectMilestone;
|
|
125
131
|
},
|
|
@@ -135,6 +141,7 @@ export function registerProjectActions(rl) {
|
|
|
135
141
|
sortOrder: t.Optional(t.Number({ description: "The sort order for the project milestone within a project (Float)" })),
|
|
136
142
|
}),
|
|
137
143
|
async execute(input, ctx) {
|
|
144
|
+
requireUnscoped(ctx, "projects.*");
|
|
138
145
|
const { id, ...fields } = input;
|
|
139
146
|
const data = await gql(key(ctx), `mutation($id: String!, $input: ProjectMilestoneUpdateInput!) { projectMilestoneUpdate(id: $id, input: $input) { success projectMilestone { ${MILESTONE_FIELDS} } } }`, { id, input: fields });
|
|
140
147
|
return data.projectMilestoneUpdate?.projectMilestone;
|
|
@@ -144,6 +151,7 @@ export function registerProjectActions(rl) {
|
|
|
144
151
|
description: "Delete a project milestone.",
|
|
145
152
|
inputSchema: t.Object({ id: t.String({ description: "The identifier of the project milestone to delete" }), }),
|
|
146
153
|
async execute(input, ctx) {
|
|
154
|
+
requireUnscoped(ctx, "projects.*");
|
|
147
155
|
const data = await gql(key(ctx), `mutation($id: String!) { projectMilestoneDelete(id: $id) { success } }`, { id: input.id });
|
|
148
156
|
return data.projectMilestoneDelete;
|
|
149
157
|
},
|
|
@@ -160,6 +168,7 @@ export function registerProjectActions(rl) {
|
|
|
160
168
|
id: t.Optional(t.String({ description: "The identifier in UUID v4 format. If none is provided, the backend will generate one" })),
|
|
161
169
|
}),
|
|
162
170
|
async execute(input, ctx) {
|
|
171
|
+
requireUnscoped(ctx, "projects.*");
|
|
163
172
|
const data = await gql(key(ctx), `mutation($input: ProjectUpdateCreateInput!) { projectUpdateCreate(input: $input) { success projectUpdate { ${PROJECT_UPDATE_FIELDS} } } }`, { input: input });
|
|
164
173
|
return data.projectUpdateCreate?.projectUpdate;
|
|
165
174
|
},
|
|
@@ -173,6 +182,7 @@ export function registerProjectActions(rl) {
|
|
|
173
182
|
isDiffHidden: t.Optional(t.Boolean({ description: "Whether the diff between the current update and the previous one should be hidden" })),
|
|
174
183
|
}),
|
|
175
184
|
async execute(input, ctx) {
|
|
185
|
+
requireUnscoped(ctx, "projects.*");
|
|
176
186
|
const { id, ...fields } = input;
|
|
177
187
|
const data = await gql(key(ctx), `mutation($id: String!, $input: ProjectUpdateUpdateInput!) { projectUpdateUpdate(id: $id, input: $input) { success projectUpdate { ${PROJECT_UPDATE_FIELDS} } } }`, { id, input: fields });
|
|
178
188
|
return data.projectUpdateUpdate?.projectUpdate;
|
|
@@ -182,6 +192,7 @@ export function registerProjectActions(rl) {
|
|
|
182
192
|
description: "Archive a project status update.",
|
|
183
193
|
inputSchema: t.Object({ id: t.String({ description: "The identifier of the project update to archive" }), }),
|
|
184
194
|
async execute(input, ctx) {
|
|
195
|
+
requireUnscoped(ctx, "projects.*");
|
|
185
196
|
const data = await gql(key(ctx), `mutation($id: String!) { projectUpdateArchive(id: $id) { success } }`, { id: input.id });
|
|
186
197
|
return data.projectUpdateArchive;
|
|
187
198
|
},
|