team-toon-tack 1.6.0 ā 1.6.2
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/scripts/config/filters.d.ts +2 -0
- package/dist/scripts/config/filters.js +46 -0
- package/dist/scripts/config/show.d.ts +2 -0
- package/dist/scripts/config/show.js +32 -0
- package/dist/scripts/config/status.d.ts +2 -0
- package/dist/scripts/config/status.js +73 -0
- package/dist/scripts/config/teams.d.ts +2 -0
- package/dist/scripts/config/teams.js +59 -0
- package/dist/scripts/config.js +21 -245
- package/dist/scripts/done-job.js +41 -119
- package/dist/scripts/init.js +138 -263
- package/dist/scripts/lib/config-builder.d.ts +41 -0
- package/dist/scripts/lib/config-builder.js +118 -0
- package/dist/scripts/lib/display.d.ts +12 -0
- package/dist/scripts/lib/display.js +91 -0
- package/dist/scripts/lib/git.d.ts +10 -0
- package/dist/scripts/lib/git.js +78 -0
- package/dist/scripts/lib/linear.d.ts +11 -0
- package/dist/scripts/lib/linear.js +61 -0
- package/dist/scripts/status.js +26 -110
- package/dist/scripts/sync.js +0 -8
- package/dist/scripts/utils.d.ts +0 -1
- package/dist/scripts/work-on.js +18 -69
- package/package.json +1 -1
- package/templates/config.example.toon +20 -65
- package/templates/local.example.toon +7 -4
package/dist/scripts/init.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
3
|
import { decode, encode } from "@toon-format/toon";
|
|
4
4
|
import prompts from "prompts";
|
|
5
|
+
import { buildConfig, buildLocalConfig, findTeamKey, findUserKey, getDefaultStatusTransitions, } from "./lib/config-builder.js";
|
|
5
6
|
import { fileExists, getLinearClient, getPaths, } from "./utils.js";
|
|
6
7
|
function parseArgs(args) {
|
|
7
8
|
const options = { interactive: true };
|
|
@@ -62,38 +63,7 @@ EXAMPLES:
|
|
|
62
63
|
bun run init -k lin_api_xxx -y
|
|
63
64
|
`);
|
|
64
65
|
}
|
|
65
|
-
async function
|
|
66
|
-
const args = process.argv.slice(2);
|
|
67
|
-
const options = parseArgs(args);
|
|
68
|
-
const paths = getPaths();
|
|
69
|
-
console.log("š Linear-TOON Initialization\n");
|
|
70
|
-
// Check existing files
|
|
71
|
-
const configExists = await fileExists(paths.configPath);
|
|
72
|
-
const localExists = await fileExists(paths.localPath);
|
|
73
|
-
if ((configExists || localExists) && !options.force) {
|
|
74
|
-
console.log("Existing configuration found:");
|
|
75
|
-
if (configExists)
|
|
76
|
-
console.log(` ā ${paths.configPath}`);
|
|
77
|
-
if (localExists)
|
|
78
|
-
console.log(` ā ${paths.localPath}`);
|
|
79
|
-
if (options.interactive) {
|
|
80
|
-
const { proceed } = await prompts({
|
|
81
|
-
type: "confirm",
|
|
82
|
-
name: "proceed",
|
|
83
|
-
message: "Update existing configuration?",
|
|
84
|
-
initial: true,
|
|
85
|
-
});
|
|
86
|
-
if (!proceed) {
|
|
87
|
-
console.log("Cancelled.");
|
|
88
|
-
process.exit(0);
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
else {
|
|
92
|
-
console.log("Use --force to overwrite existing files.");
|
|
93
|
-
process.exit(1);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
// Get API key
|
|
66
|
+
async function promptForApiKey(options) {
|
|
97
67
|
let apiKey = options.apiKey || process.env.LINEAR_API_KEY;
|
|
98
68
|
if (!apiKey && options.interactive) {
|
|
99
69
|
const response = await prompts({
|
|
@@ -106,22 +76,9 @@ async function init() {
|
|
|
106
76
|
});
|
|
107
77
|
apiKey = response.apiKey;
|
|
108
78
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
process.exit(1);
|
|
113
|
-
}
|
|
114
|
-
// Create Linear client
|
|
115
|
-
const client = getLinearClient();
|
|
116
|
-
console.log("\nš” Fetching data from Linear...");
|
|
117
|
-
// Fetch teams
|
|
118
|
-
const teamsData = await client.teams();
|
|
119
|
-
const teams = teamsData.nodes;
|
|
120
|
-
if (teams.length === 0) {
|
|
121
|
-
console.error("Error: No teams found in your Linear workspace.");
|
|
122
|
-
process.exit(1);
|
|
123
|
-
}
|
|
124
|
-
// Select teams (multi-select)
|
|
79
|
+
return apiKey;
|
|
80
|
+
}
|
|
81
|
+
async function selectTeams(teams, options) {
|
|
125
82
|
let selectedTeams = [teams[0]];
|
|
126
83
|
let primaryTeam = teams[0];
|
|
127
84
|
if (options.team) {
|
|
@@ -141,7 +98,6 @@ async function init() {
|
|
|
141
98
|
});
|
|
142
99
|
if (response.teamIds && response.teamIds.length > 0) {
|
|
143
100
|
selectedTeams = teams.filter((t) => response.teamIds.includes(t.id));
|
|
144
|
-
// If multiple teams selected, ask for primary team
|
|
145
101
|
if (selectedTeams.length > 1) {
|
|
146
102
|
const primaryResponse = await prompts({
|
|
147
103
|
type: "select",
|
|
@@ -158,30 +114,9 @@ async function init() {
|
|
|
158
114
|
}
|
|
159
115
|
}
|
|
160
116
|
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
}
|
|
165
|
-
// Use primary team for fetching members, labels, states (they may differ per team)
|
|
166
|
-
const selectedTeam = primaryTeam;
|
|
167
|
-
// Fetch team members
|
|
168
|
-
const members = await selectedTeam.members();
|
|
169
|
-
const users = members.nodes;
|
|
170
|
-
console.log(` Users: ${users.length}`);
|
|
171
|
-
// Fetch labels
|
|
172
|
-
const labelsData = await client.issueLabels({
|
|
173
|
-
filter: { team: { id: { eq: selectedTeam.id } } },
|
|
174
|
-
});
|
|
175
|
-
const labels = labelsData.nodes;
|
|
176
|
-
console.log(` Labels: ${labels.length}`);
|
|
177
|
-
// Fetch workflow states
|
|
178
|
-
const statesData = await client.workflowStates({
|
|
179
|
-
filter: { team: { id: { eq: selectedTeam.id } } },
|
|
180
|
-
});
|
|
181
|
-
const states = statesData.nodes;
|
|
182
|
-
// Fetch current cycle using activeCycle (direct and accurate)
|
|
183
|
-
const currentCycle = await selectedTeam.activeCycle;
|
|
184
|
-
// Select current user
|
|
117
|
+
return { selected: selectedTeams, primary: primaryTeam };
|
|
118
|
+
}
|
|
119
|
+
async function selectUser(users, options) {
|
|
185
120
|
let currentUser = users[0];
|
|
186
121
|
if (options.user) {
|
|
187
122
|
const found = users.find((u) => u.email?.toLowerCase() === options.user?.toLowerCase() ||
|
|
@@ -201,12 +136,13 @@ async function init() {
|
|
|
201
136
|
});
|
|
202
137
|
currentUser = users.find((u) => u.id === response.userId) || users[0];
|
|
203
138
|
}
|
|
204
|
-
|
|
205
|
-
|
|
139
|
+
return currentUser;
|
|
140
|
+
}
|
|
141
|
+
async function selectLabelFilter(labels, options) {
|
|
206
142
|
if (options.label) {
|
|
207
|
-
|
|
143
|
+
return options.label;
|
|
208
144
|
}
|
|
209
|
-
|
|
145
|
+
if (options.interactive && labels.length > 0) {
|
|
210
146
|
const labelChoices = [
|
|
211
147
|
{ title: "(No filter - sync all labels)", value: undefined },
|
|
212
148
|
...labels.map((l) => ({ title: l.name, value: l.name })),
|
|
@@ -217,210 +153,157 @@ async function init() {
|
|
|
217
153
|
message: "Select label filter (optional):",
|
|
218
154
|
choices: labelChoices,
|
|
219
155
|
});
|
|
220
|
-
|
|
156
|
+
return response.label;
|
|
221
157
|
}
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
message: "Select labels to exclude (optional, space to select):",
|
|
229
|
-
choices: labels.map((l) => ({ title: l.name, value: l.name })),
|
|
230
|
-
});
|
|
231
|
-
excludeLabels = excludeLabelsResponse.excludeLabels || [];
|
|
232
|
-
}
|
|
233
|
-
// Select excluded users (optional)
|
|
234
|
-
let excludeAssignees = [];
|
|
235
|
-
if (options.interactive && users.length > 0) {
|
|
236
|
-
const excludeUsersResponse = await prompts({
|
|
237
|
-
type: "multiselect",
|
|
238
|
-
name: "excludeUsers",
|
|
239
|
-
message: "Select users to exclude (optional, space to select):",
|
|
240
|
-
choices: users
|
|
241
|
-
.filter((u) => u.id !== currentUser.id) // Don't show current user
|
|
242
|
-
.map((u) => ({
|
|
243
|
-
title: `${u.displayName || u.name} (${u.email})`,
|
|
244
|
-
value: (u.displayName || u.name || u.email?.split("@")[0] || "user")
|
|
245
|
-
.toLowerCase()
|
|
246
|
-
.replace(/[^a-z0-9]/g, "_"),
|
|
247
|
-
})),
|
|
248
|
-
});
|
|
249
|
-
excludeAssignees = excludeUsersResponse.excludeUsers || [];
|
|
158
|
+
return undefined;
|
|
159
|
+
}
|
|
160
|
+
async function selectStatusMappings(states, options) {
|
|
161
|
+
const defaults = getDefaultStatusTransitions(states);
|
|
162
|
+
if (!options.interactive || states.length === 0) {
|
|
163
|
+
return defaults;
|
|
250
164
|
}
|
|
251
|
-
|
|
165
|
+
console.log("\nš Configure status mappings:");
|
|
252
166
|
const stateChoices = states.map((s) => ({
|
|
253
167
|
title: `${s.name} (${s.type})`,
|
|
254
168
|
value: s.name,
|
|
255
169
|
}));
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
"
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
170
|
+
const todoResponse = await prompts({
|
|
171
|
+
type: "select",
|
|
172
|
+
name: "todo",
|
|
173
|
+
message: 'Select status for "Todo" (pending tasks):',
|
|
174
|
+
choices: stateChoices,
|
|
175
|
+
initial: stateChoices.findIndex((c) => c.value === defaults.todo),
|
|
176
|
+
});
|
|
177
|
+
const inProgressResponse = await prompts({
|
|
178
|
+
type: "select",
|
|
179
|
+
name: "in_progress",
|
|
180
|
+
message: 'Select status for "In Progress" (working tasks):',
|
|
181
|
+
choices: stateChoices,
|
|
182
|
+
initial: stateChoices.findIndex((c) => c.value === defaults.in_progress),
|
|
183
|
+
});
|
|
184
|
+
const doneResponse = await prompts({
|
|
185
|
+
type: "select",
|
|
186
|
+
name: "done",
|
|
187
|
+
message: 'Select status for "Done" (completed tasks):',
|
|
188
|
+
choices: stateChoices,
|
|
189
|
+
initial: stateChoices.findIndex((c) => c.value === defaults.done),
|
|
190
|
+
});
|
|
191
|
+
const testingChoices = [
|
|
192
|
+
{ title: "(None)", value: undefined },
|
|
193
|
+
...stateChoices,
|
|
194
|
+
];
|
|
195
|
+
const testingResponse = await prompts({
|
|
196
|
+
type: "select",
|
|
197
|
+
name: "testing",
|
|
198
|
+
message: 'Select status for "Testing" (optional, for parent tasks):',
|
|
199
|
+
choices: testingChoices,
|
|
200
|
+
initial: defaults.testing
|
|
201
|
+
? testingChoices.findIndex((c) => c.value === defaults.testing)
|
|
202
|
+
: 0,
|
|
203
|
+
});
|
|
204
|
+
return {
|
|
205
|
+
todo: todoResponse.todo || defaults.todo,
|
|
206
|
+
in_progress: inProgressResponse.in_progress || defaults.in_progress,
|
|
207
|
+
done: doneResponse.done || defaults.done,
|
|
208
|
+
testing: testingResponse.testing,
|
|
274
209
|
};
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
message: 'Select status for "Testing" (optional, for parent tasks):',
|
|
307
|
-
choices: testingChoices,
|
|
308
|
-
initial: defaultTesting
|
|
309
|
-
? testingChoices.findIndex((c) => c.value === defaultTesting)
|
|
310
|
-
: 0,
|
|
311
|
-
});
|
|
312
|
-
statusTransitions = {
|
|
313
|
-
todo: todoResponse.todo || defaultTodo,
|
|
314
|
-
in_progress: inProgressResponse.in_progress || defaultInProgress,
|
|
315
|
-
done: doneResponse.done || defaultDone,
|
|
316
|
-
testing: testingResponse.testing,
|
|
317
|
-
};
|
|
318
|
-
}
|
|
319
|
-
// Build config
|
|
320
|
-
const teamsConfig = {};
|
|
321
|
-
for (const team of teams) {
|
|
322
|
-
const key = team.name.toLowerCase().replace(/[^a-z0-9]/g, "_");
|
|
323
|
-
teamsConfig[key] = {
|
|
324
|
-
id: team.id,
|
|
325
|
-
name: team.name,
|
|
326
|
-
icon: team.icon || undefined,
|
|
327
|
-
};
|
|
210
|
+
}
|
|
211
|
+
async function init() {
|
|
212
|
+
const args = process.argv.slice(2);
|
|
213
|
+
const options = parseArgs(args);
|
|
214
|
+
const paths = getPaths();
|
|
215
|
+
console.log("š Linear-TOON Initialization\n");
|
|
216
|
+
// Check existing files
|
|
217
|
+
const configExists = await fileExists(paths.configPath);
|
|
218
|
+
const localExists = await fileExists(paths.localPath);
|
|
219
|
+
if ((configExists || localExists) && !options.force) {
|
|
220
|
+
console.log("Existing configuration found:");
|
|
221
|
+
if (configExists)
|
|
222
|
+
console.log(` ā ${paths.configPath}`);
|
|
223
|
+
if (localExists)
|
|
224
|
+
console.log(` ā ${paths.localPath}`);
|
|
225
|
+
if (options.interactive) {
|
|
226
|
+
const { proceed } = await prompts({
|
|
227
|
+
type: "confirm",
|
|
228
|
+
name: "proceed",
|
|
229
|
+
message: "Update existing configuration?",
|
|
230
|
+
initial: true,
|
|
231
|
+
});
|
|
232
|
+
if (!proceed) {
|
|
233
|
+
console.log("Cancelled.");
|
|
234
|
+
process.exit(0);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
else {
|
|
238
|
+
console.log("Use --force to overwrite existing files.");
|
|
239
|
+
process.exit(1);
|
|
240
|
+
}
|
|
328
241
|
}
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
.toLowerCase()
|
|
336
|
-
.replace(/[^a-z0-9]/g, "_");
|
|
337
|
-
usersConfig[key] = {
|
|
338
|
-
id: user.id,
|
|
339
|
-
email: user.email || "",
|
|
340
|
-
displayName: user.displayName || user.name || "",
|
|
341
|
-
};
|
|
242
|
+
// Get API key
|
|
243
|
+
const apiKey = await promptForApiKey(options);
|
|
244
|
+
if (!apiKey) {
|
|
245
|
+
console.error("Error: LINEAR_API_KEY is required.");
|
|
246
|
+
console.error("Get your API key from: https://linear.app/settings/api");
|
|
247
|
+
process.exit(1);
|
|
342
248
|
}
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
249
|
+
// Create Linear client
|
|
250
|
+
const client = getLinearClient();
|
|
251
|
+
console.log("\nš” Fetching data from Linear...");
|
|
252
|
+
// Fetch teams
|
|
253
|
+
const teamsData = await client.teams();
|
|
254
|
+
const teams = teamsData.nodes;
|
|
255
|
+
if (teams.length === 0) {
|
|
256
|
+
console.error("Error: No teams found in your Linear workspace.");
|
|
257
|
+
process.exit(1);
|
|
351
258
|
}
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
type: state.type,
|
|
358
|
-
};
|
|
259
|
+
// Select teams
|
|
260
|
+
const { selected: selectedTeams, primary: primaryTeam } = await selectTeams(teams, options);
|
|
261
|
+
console.log(` Teams: ${selectedTeams.map((t) => t.name).join(", ")}`);
|
|
262
|
+
if (selectedTeams.length > 1) {
|
|
263
|
+
console.log(` Primary: ${primaryTeam.name}`);
|
|
359
264
|
}
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
const currentUserKey = Object.entries(usersConfig).find(([_, u]) => u.id === currentUser.id)?.[0] || "user";
|
|
385
|
-
// Find selected team keys
|
|
386
|
-
const selectedTeamKey = Object.entries(teamsConfig).find(([_, t]) => t.id === primaryTeam.id)?.[0] || Object.keys(teamsConfig)[0];
|
|
265
|
+
// Fetch data from primary team
|
|
266
|
+
const selectedTeam = await client.team(primaryTeam.id);
|
|
267
|
+
const members = await selectedTeam.members();
|
|
268
|
+
const users = members.nodes;
|
|
269
|
+
console.log(` Users: ${users.length}`);
|
|
270
|
+
const labelsData = await client.issueLabels({
|
|
271
|
+
filter: { team: { id: { eq: primaryTeam.id } } },
|
|
272
|
+
});
|
|
273
|
+
const labels = labelsData.nodes;
|
|
274
|
+
console.log(` Labels: ${labels.length}`);
|
|
275
|
+
const statesData = await client.workflowStates({
|
|
276
|
+
filter: { team: { id: { eq: primaryTeam.id } } },
|
|
277
|
+
});
|
|
278
|
+
const states = statesData.nodes;
|
|
279
|
+
const currentCycle = (await selectedTeam.activeCycle);
|
|
280
|
+
// User selections
|
|
281
|
+
const currentUser = await selectUser(users, options);
|
|
282
|
+
const defaultLabel = await selectLabelFilter(labels, options);
|
|
283
|
+
const statusTransitions = await selectStatusMappings(states, options);
|
|
284
|
+
// Build config
|
|
285
|
+
const config = buildConfig(teams, users, labels, states, statusTransitions, currentCycle ?? undefined);
|
|
286
|
+
// Find keys
|
|
287
|
+
const currentUserKey = findUserKey(config.users, currentUser.id);
|
|
288
|
+
const primaryTeamKey = findTeamKey(config.teams, primaryTeam.id);
|
|
387
289
|
const selectedTeamKeys = selectedTeams
|
|
388
|
-
.map((team) =>
|
|
290
|
+
.map((team) => findTeamKey(config.teams, team.id))
|
|
389
291
|
.filter((key) => key !== undefined);
|
|
390
|
-
const localConfig =
|
|
391
|
-
current_user: currentUserKey,
|
|
392
|
-
team: selectedTeamKey,
|
|
393
|
-
teams: selectedTeamKeys.length > 1 ? selectedTeamKeys : undefined,
|
|
394
|
-
label: defaultLabel,
|
|
395
|
-
exclude_labels: excludeLabels.length > 0 ? excludeLabels : undefined,
|
|
396
|
-
exclude_assignees: excludeAssignees.length > 0 ? excludeAssignees : undefined,
|
|
397
|
-
};
|
|
292
|
+
const localConfig = buildLocalConfig(currentUserKey, primaryTeamKey, selectedTeamKeys, defaultLabel);
|
|
398
293
|
// Write config files
|
|
399
294
|
console.log("\nš Writing configuration files...");
|
|
400
|
-
// Ensure directory exists
|
|
401
295
|
await fs.mkdir(paths.baseDir, { recursive: true });
|
|
402
296
|
// Merge with existing config if exists
|
|
403
297
|
if (configExists && !options.force) {
|
|
404
298
|
try {
|
|
405
299
|
const existingContent = await fs.readFile(paths.configPath, "utf-8");
|
|
406
300
|
const existingConfig = decode(existingContent);
|
|
407
|
-
// Merge: preserve existing custom fields
|
|
408
|
-
config.status_transitions = {
|
|
409
|
-
todo: "Todo",
|
|
410
|
-
in_progress: "In Progress",
|
|
411
|
-
done: "Done",
|
|
412
|
-
...existingConfig.status_transitions,
|
|
413
|
-
...config.status_transitions,
|
|
414
|
-
};
|
|
415
|
-
// Preserve cycle history
|
|
416
301
|
if (existingConfig.cycle_history) {
|
|
417
302
|
config.cycle_history = existingConfig.cycle_history;
|
|
418
303
|
}
|
|
419
|
-
// Preserve current_cycle if not fetched fresh
|
|
420
304
|
if (!currentCycle && existingConfig.current_cycle) {
|
|
421
305
|
config.current_cycle = existingConfig.current_cycle;
|
|
422
306
|
}
|
|
423
|
-
// Preserve priority_order if exists
|
|
424
307
|
if (existingConfig.priority_order) {
|
|
425
308
|
config.priority_order = existingConfig.priority_order;
|
|
426
309
|
}
|
|
@@ -436,7 +319,6 @@ async function init() {
|
|
|
436
319
|
try {
|
|
437
320
|
const existingContent = await fs.readFile(paths.localPath, "utf-8");
|
|
438
321
|
const existingLocal = decode(existingContent);
|
|
439
|
-
// Preserve existing values only if not newly set
|
|
440
322
|
if (!options.interactive) {
|
|
441
323
|
if (existingLocal.current_user)
|
|
442
324
|
localConfig.current_user = existingLocal.current_user;
|
|
@@ -446,8 +328,6 @@ async function init() {
|
|
|
446
328
|
localConfig.teams = existingLocal.teams;
|
|
447
329
|
if (existingLocal.label)
|
|
448
330
|
localConfig.label = existingLocal.label;
|
|
449
|
-
if (existingLocal.exclude_assignees)
|
|
450
|
-
localConfig.exclude_assignees = existingLocal.exclude_assignees;
|
|
451
331
|
if (existingLocal.exclude_labels)
|
|
452
332
|
localConfig.exclude_labels = existingLocal.exclude_labels;
|
|
453
333
|
}
|
|
@@ -467,12 +347,7 @@ async function init() {
|
|
|
467
347
|
}
|
|
468
348
|
console.log(` User: ${currentUser.displayName || currentUser.name} (${currentUser.email})`);
|
|
469
349
|
console.log(` Label filter: ${defaultLabel || "(none)"}`);
|
|
470
|
-
|
|
471
|
-
console.log(` Excluded labels: ${excludeLabels.join(", ")}`);
|
|
472
|
-
}
|
|
473
|
-
if (excludeAssignees.length > 0) {
|
|
474
|
-
console.log(` Excluded users: ${excludeAssignees.join(", ")}`);
|
|
475
|
-
}
|
|
350
|
+
console.log(` (Use 'ttt config filters' to set excluded labels/users)`);
|
|
476
351
|
if (currentCycle) {
|
|
477
352
|
console.log(` Cycle: ${currentCycle.name || `Cycle #${currentCycle.number}`}`);
|
|
478
353
|
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { Config, LocalConfig, StatusTransitions, TeamConfig, UserConfig, LabelConfig } from "../utils.js";
|
|
2
|
+
export interface LinearTeam {
|
|
3
|
+
id: string;
|
|
4
|
+
name: string;
|
|
5
|
+
icon?: string | null;
|
|
6
|
+
}
|
|
7
|
+
export interface LinearUser {
|
|
8
|
+
id: string;
|
|
9
|
+
email?: string | null;
|
|
10
|
+
displayName?: string | null;
|
|
11
|
+
name?: string | null;
|
|
12
|
+
}
|
|
13
|
+
export interface LinearLabel {
|
|
14
|
+
id: string;
|
|
15
|
+
name: string;
|
|
16
|
+
color?: string | null;
|
|
17
|
+
}
|
|
18
|
+
export interface LinearState {
|
|
19
|
+
id: string;
|
|
20
|
+
name: string;
|
|
21
|
+
type: string;
|
|
22
|
+
}
|
|
23
|
+
export interface LinearCycle {
|
|
24
|
+
id: string;
|
|
25
|
+
name?: string | null;
|
|
26
|
+
number: number;
|
|
27
|
+
startsAt?: Date | null;
|
|
28
|
+
endsAt?: Date | null;
|
|
29
|
+
}
|
|
30
|
+
export declare function buildTeamsConfig(teams: LinearTeam[]): Record<string, TeamConfig>;
|
|
31
|
+
export declare function buildUsersConfig(users: LinearUser[]): Record<string, UserConfig>;
|
|
32
|
+
export declare function buildLabelsConfig(labels: LinearLabel[]): Record<string, LabelConfig>;
|
|
33
|
+
export declare function buildStatusesConfig(states: LinearState[]): Record<string, {
|
|
34
|
+
name: string;
|
|
35
|
+
type: string;
|
|
36
|
+
}>;
|
|
37
|
+
export declare function getDefaultStatusTransitions(states: LinearState[]): StatusTransitions;
|
|
38
|
+
export declare function buildConfig(teams: LinearTeam[], users: LinearUser[], labels: LinearLabel[], states: LinearState[], statusTransitions: StatusTransitions, currentCycle?: LinearCycle): Config;
|
|
39
|
+
export declare function findUserKey(usersConfig: Record<string, UserConfig>, userId: string): string;
|
|
40
|
+
export declare function findTeamKey(teamsConfig: Record<string, TeamConfig>, teamId: string): string;
|
|
41
|
+
export declare function buildLocalConfig(currentUserKey: string, primaryTeamKey: string, selectedTeamKeys: string[], defaultLabel?: string, excludeLabels?: string[]): LocalConfig;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
export function buildTeamsConfig(teams) {
|
|
2
|
+
const config = {};
|
|
3
|
+
for (const team of teams) {
|
|
4
|
+
const key = team.name.toLowerCase().replace(/[^a-z0-9]/g, "_");
|
|
5
|
+
config[key] = {
|
|
6
|
+
id: team.id,
|
|
7
|
+
name: team.name,
|
|
8
|
+
icon: team.icon || undefined,
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
return config;
|
|
12
|
+
}
|
|
13
|
+
export function buildUsersConfig(users) {
|
|
14
|
+
const config = {};
|
|
15
|
+
for (const user of users) {
|
|
16
|
+
const key = (user.displayName ||
|
|
17
|
+
user.name ||
|
|
18
|
+
user.email?.split("@")[0] ||
|
|
19
|
+
"user")
|
|
20
|
+
.toLowerCase()
|
|
21
|
+
.replace(/[^a-z0-9]/g, "_");
|
|
22
|
+
config[key] = {
|
|
23
|
+
id: user.id,
|
|
24
|
+
email: user.email || "",
|
|
25
|
+
displayName: user.displayName || user.name || "",
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
return config;
|
|
29
|
+
}
|
|
30
|
+
export function buildLabelsConfig(labels) {
|
|
31
|
+
const config = {};
|
|
32
|
+
for (const label of labels) {
|
|
33
|
+
const key = label.name.toLowerCase().replace(/[^a-z0-9]/g, "_");
|
|
34
|
+
config[key] = {
|
|
35
|
+
id: label.id,
|
|
36
|
+
name: label.name,
|
|
37
|
+
color: label.color || undefined,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
return config;
|
|
41
|
+
}
|
|
42
|
+
export function buildStatusesConfig(states) {
|
|
43
|
+
const config = {};
|
|
44
|
+
for (const state of states) {
|
|
45
|
+
const key = state.name.toLowerCase().replace(/[^a-z0-9]/g, "_");
|
|
46
|
+
config[key] = {
|
|
47
|
+
name: state.name,
|
|
48
|
+
type: state.type,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
return config;
|
|
52
|
+
}
|
|
53
|
+
// Helper to find status by keyword (case insensitive)
|
|
54
|
+
function findStatusByKeyword(states, keywords) {
|
|
55
|
+
const lowerKeywords = keywords.map((k) => k.toLowerCase());
|
|
56
|
+
return states.find((s) => lowerKeywords.some((k) => s.name.toLowerCase().includes(k)))?.name;
|
|
57
|
+
}
|
|
58
|
+
export function getDefaultStatusTransitions(states) {
|
|
59
|
+
const defaultTodo = states.find((s) => s.type === "unstarted")?.name ||
|
|
60
|
+
findStatusByKeyword(states, ["todo", "pending"]) ||
|
|
61
|
+
states[0]?.name ||
|
|
62
|
+
"Todo";
|
|
63
|
+
const defaultInProgress = states.find((s) => s.type === "started")?.name ||
|
|
64
|
+
findStatusByKeyword(states, ["in progress", "progress"]) ||
|
|
65
|
+
"In Progress";
|
|
66
|
+
const defaultDone = states.find((s) => s.type === "completed")?.name ||
|
|
67
|
+
findStatusByKeyword(states, ["done", "complete"]) ||
|
|
68
|
+
"Done";
|
|
69
|
+
const defaultTesting = findStatusByKeyword(states, ["testing", "review"]) ||
|
|
70
|
+
undefined;
|
|
71
|
+
return {
|
|
72
|
+
todo: defaultTodo,
|
|
73
|
+
in_progress: defaultInProgress,
|
|
74
|
+
done: defaultDone,
|
|
75
|
+
testing: defaultTesting,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
export function buildConfig(teams, users, labels, states, statusTransitions, currentCycle) {
|
|
79
|
+
return {
|
|
80
|
+
teams: buildTeamsConfig(teams),
|
|
81
|
+
users: buildUsersConfig(users),
|
|
82
|
+
labels: buildLabelsConfig(labels),
|
|
83
|
+
priorities: {
|
|
84
|
+
urgent: { value: 1, name: "Urgent" },
|
|
85
|
+
high: { value: 2, name: "High" },
|
|
86
|
+
medium: { value: 3, name: "Medium" },
|
|
87
|
+
low: { value: 4, name: "Low" },
|
|
88
|
+
},
|
|
89
|
+
statuses: buildStatusesConfig(states),
|
|
90
|
+
status_transitions: statusTransitions,
|
|
91
|
+
priority_order: ["urgent", "high", "medium", "low", "none"],
|
|
92
|
+
current_cycle: currentCycle
|
|
93
|
+
? {
|
|
94
|
+
id: currentCycle.id,
|
|
95
|
+
name: currentCycle.name || `Cycle #${currentCycle.number}`,
|
|
96
|
+
start_date: currentCycle.startsAt?.toISOString().split("T")[0] || "",
|
|
97
|
+
end_date: currentCycle.endsAt?.toISOString().split("T")[0] || "",
|
|
98
|
+
}
|
|
99
|
+
: undefined,
|
|
100
|
+
cycle_history: [],
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
export function findUserKey(usersConfig, userId) {
|
|
104
|
+
return (Object.entries(usersConfig).find(([_, u]) => u.id === userId)?.[0] || "user");
|
|
105
|
+
}
|
|
106
|
+
export function findTeamKey(teamsConfig, teamId) {
|
|
107
|
+
return (Object.entries(teamsConfig).find(([_, t]) => t.id === teamId)?.[0] ||
|
|
108
|
+
Object.keys(teamsConfig)[0]);
|
|
109
|
+
}
|
|
110
|
+
export function buildLocalConfig(currentUserKey, primaryTeamKey, selectedTeamKeys, defaultLabel, excludeLabels) {
|
|
111
|
+
return {
|
|
112
|
+
current_user: currentUserKey,
|
|
113
|
+
team: primaryTeamKey,
|
|
114
|
+
teams: selectedTeamKeys.length > 1 ? selectedTeamKeys : undefined,
|
|
115
|
+
label: defaultLabel,
|
|
116
|
+
exclude_labels: excludeLabels && excludeLabels.length > 0 ? excludeLabels : undefined,
|
|
117
|
+
};
|
|
118
|
+
}
|