team-toon-tack 2.0.3 ā 2.3.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/.claude-plugin/marketplace.json +19 -0
- package/.claude-plugin/plugin.json +18 -0
- package/README.md +67 -10
- package/README.zh-TW.md +67 -10
- package/commands/ttt-done.md +62 -0
- package/commands/ttt-get-issue.md +71 -0
- package/commands/ttt-status.md +65 -0
- package/commands/ttt-sync.md +51 -0
- package/commands/ttt-work-on.md +48 -0
- package/dist/bin/cli.js +15 -8
- package/dist/scripts/config/teams.js +166 -34
- package/dist/scripts/done-job.js +158 -44
- package/dist/scripts/get-issue.d.ts +1 -0
- package/dist/scripts/get-issue.js +61 -0
- package/dist/scripts/init.js +203 -265
- package/dist/scripts/lib/config-builder.d.ts +2 -2
- package/dist/scripts/lib/config-builder.js +5 -4
- package/dist/scripts/lib/images.js +4 -3
- package/dist/scripts/lib/sync.d.ts +8 -0
- package/dist/scripts/lib/sync.js +41 -29
- package/dist/scripts/sync.js +1 -1
- package/dist/scripts/utils.d.ts +10 -2
- package/dist/scripts/work-on.js +1 -1
- package/package.json +5 -3
- package/skills/linear-task-manager/SKILL.md +170 -0
- package/templates/claude-code-commands/done-job.md +0 -45
- package/templates/claude-code-commands/sync-linear.md +0 -32
- package/templates/claude-code-commands/work-on.md +0 -62
- package/templates/config.example.toon +0 -37
- package/templates/local.example.toon +0 -16
package/dist/scripts/init.js
CHANGED
|
@@ -6,7 +6,7 @@ const __filename = fileURLToPath(import.meta.url);
|
|
|
6
6
|
const __dirname = path.dirname(__filename);
|
|
7
7
|
import { decode, encode } from "@toon-format/toon";
|
|
8
8
|
import prompts from "prompts";
|
|
9
|
-
import { buildConfig, buildLocalConfig, findTeamKey, findUserKey, getDefaultStatusTransitions, } from "./lib/config-builder.js";
|
|
9
|
+
import { buildConfig, buildLocalConfig, buildTeamsConfig, findTeamKey, findUserKey, getDefaultStatusTransitions, } from "./lib/config-builder.js";
|
|
10
10
|
import { fileExists, getLinearClient, getPaths, } from "./utils.js";
|
|
11
11
|
function parseArgs(args) {
|
|
12
12
|
const options = { interactive: true };
|
|
@@ -82,77 +82,157 @@ async function promptForApiKey(options) {
|
|
|
82
82
|
}
|
|
83
83
|
return apiKey;
|
|
84
84
|
}
|
|
85
|
-
async function
|
|
86
|
-
let
|
|
87
|
-
let primaryTeam = teams[0];
|
|
85
|
+
async function selectDevTeam(teams, options) {
|
|
86
|
+
let devTeam = teams[0];
|
|
88
87
|
if (options.team) {
|
|
89
88
|
const found = teams.find((t) => t.name.toLowerCase() === options.team?.toLowerCase());
|
|
90
89
|
if (found) {
|
|
91
|
-
|
|
92
|
-
primaryTeam = found;
|
|
90
|
+
devTeam = found;
|
|
93
91
|
}
|
|
94
92
|
}
|
|
95
93
|
else if (options.interactive && teams.length > 1) {
|
|
94
|
+
console.log("\nšØāš» Dev Team Configuration:");
|
|
96
95
|
const response = await prompts({
|
|
97
|
-
type: "
|
|
98
|
-
name: "
|
|
99
|
-
message: "Select
|
|
96
|
+
type: "select",
|
|
97
|
+
name: "teamId",
|
|
98
|
+
message: "Select your dev team (for work-on/done commands):",
|
|
100
99
|
choices: teams.map((t) => ({ title: t.name, value: t.id })),
|
|
101
|
-
min: 1,
|
|
102
100
|
});
|
|
103
|
-
if (response.
|
|
104
|
-
|
|
105
|
-
if (selectedTeams.length > 1) {
|
|
106
|
-
const primaryResponse = await prompts({
|
|
107
|
-
type: "select",
|
|
108
|
-
name: "primaryTeamId",
|
|
109
|
-
message: "Select your primary team (for work-on/done commands):",
|
|
110
|
-
choices: selectedTeams.map((t) => ({ title: t.name, value: t.id })),
|
|
111
|
-
});
|
|
112
|
-
primaryTeam =
|
|
113
|
-
selectedTeams.find((t) => t.id === primaryResponse.primaryTeamId) ||
|
|
114
|
-
selectedTeams[0];
|
|
115
|
-
}
|
|
116
|
-
else {
|
|
117
|
-
primaryTeam = selectedTeams[0];
|
|
118
|
-
}
|
|
101
|
+
if (response.teamId) {
|
|
102
|
+
devTeam = teams.find((t) => t.id === response.teamId) || teams[0];
|
|
119
103
|
}
|
|
120
104
|
}
|
|
121
|
-
return
|
|
105
|
+
return devTeam;
|
|
106
|
+
}
|
|
107
|
+
async function selectDevTestingStatus(devStates, options) {
|
|
108
|
+
if (!options.interactive || devStates.length === 0) {
|
|
109
|
+
return getDefaultStatusTransitions(devStates).testing;
|
|
110
|
+
}
|
|
111
|
+
console.log("\nš Dev Team Testing/Review Status:");
|
|
112
|
+
const stateChoices = devStates.map((s) => ({
|
|
113
|
+
title: `${s.name} (${s.type})`,
|
|
114
|
+
value: s.name,
|
|
115
|
+
}));
|
|
116
|
+
const response = await prompts({
|
|
117
|
+
type: "select",
|
|
118
|
+
name: "testingStatus",
|
|
119
|
+
message: "Select testing/review status for dev team (used when strict_review mode):",
|
|
120
|
+
choices: [
|
|
121
|
+
{ title: "(Skip - no testing status)", value: undefined },
|
|
122
|
+
...stateChoices,
|
|
123
|
+
],
|
|
124
|
+
initial: 0,
|
|
125
|
+
});
|
|
126
|
+
return response.testingStatus;
|
|
122
127
|
}
|
|
123
|
-
async function
|
|
128
|
+
async function selectQaPmTeams(teams, devTeam, teamStatesMap, teamsConfig, options) {
|
|
124
129
|
// Only ask if there are multiple teams and interactive mode
|
|
125
130
|
if (!options.interactive || teams.length <= 1) {
|
|
126
|
-
return
|
|
131
|
+
return [];
|
|
127
132
|
}
|
|
128
|
-
// Filter out
|
|
129
|
-
const otherTeams = teams.filter((t) => t.id !==
|
|
133
|
+
// Filter out dev team from choices
|
|
134
|
+
const otherTeams = teams.filter((t) => t.id !== devTeam.id);
|
|
130
135
|
if (otherTeams.length === 0) {
|
|
131
|
-
return
|
|
136
|
+
return [];
|
|
132
137
|
}
|
|
133
|
-
console.log("\nš QA/PM
|
|
138
|
+
console.log("\nš QA/PM Teams Configuration:");
|
|
134
139
|
const response = await prompts({
|
|
135
|
-
type: "
|
|
136
|
-
name: "
|
|
137
|
-
message: "Select QA/PM
|
|
140
|
+
type: "multiselect",
|
|
141
|
+
name: "qaPmTeamIds",
|
|
142
|
+
message: "Select QA/PM teams for cross-team parent updates (space to select, enter to confirm):",
|
|
138
143
|
choices: [
|
|
139
|
-
{
|
|
140
|
-
title: "(None - skip)",
|
|
141
|
-
value: undefined,
|
|
142
|
-
description: "No cross-team parent updates",
|
|
143
|
-
},
|
|
144
144
|
...otherTeams.map((t) => ({
|
|
145
145
|
title: t.name,
|
|
146
146
|
value: t.id,
|
|
147
147
|
description: "Parent issues in this team will be updated to Testing",
|
|
148
148
|
})),
|
|
149
149
|
],
|
|
150
|
-
|
|
150
|
+
hint: "- Press space to select, enter to confirm. Leave empty to skip.",
|
|
151
151
|
});
|
|
152
|
-
if (!response.
|
|
153
|
-
return
|
|
152
|
+
if (!response.qaPmTeamIds || response.qaPmTeamIds.length === 0) {
|
|
153
|
+
return [];
|
|
154
|
+
}
|
|
155
|
+
// For each selected QA/PM team, select its testing status
|
|
156
|
+
const qaPmTeams = [];
|
|
157
|
+
for (const teamId of response.qaPmTeamIds) {
|
|
158
|
+
const team = teams.find((t) => t.id === teamId);
|
|
159
|
+
if (!team)
|
|
160
|
+
continue;
|
|
161
|
+
const teamStates = teamStatesMap.get(teamId) || [];
|
|
162
|
+
const defaults = getDefaultStatusTransitions(teamStates);
|
|
163
|
+
// Find team key
|
|
164
|
+
const teamKey = Object.entries(teamsConfig).find(([_, t]) => t.id === teamId)?.[0] ||
|
|
165
|
+
team.name.toLowerCase().replace(/[^a-z0-9]/g, "_");
|
|
166
|
+
if (teamStates.length === 0) {
|
|
167
|
+
// No states available, use default
|
|
168
|
+
if (defaults.testing) {
|
|
169
|
+
qaPmTeams.push({
|
|
170
|
+
team: teamKey,
|
|
171
|
+
testing_status: defaults.testing,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
const stateChoices = teamStates.map((s) => ({
|
|
177
|
+
title: `${s.name} (${s.type})`,
|
|
178
|
+
value: s.name,
|
|
179
|
+
}));
|
|
180
|
+
const statusResponse = await prompts({
|
|
181
|
+
type: "select",
|
|
182
|
+
name: "testingStatus",
|
|
183
|
+
message: `Select testing status for ${team.name}:`,
|
|
184
|
+
choices: [
|
|
185
|
+
{ title: "(Skip this team)", value: undefined },
|
|
186
|
+
...stateChoices,
|
|
187
|
+
],
|
|
188
|
+
initial: defaults.testing
|
|
189
|
+
? stateChoices.findIndex((c) => c.value === defaults.testing) + 1
|
|
190
|
+
: 0,
|
|
191
|
+
});
|
|
192
|
+
if (statusResponse.testingStatus) {
|
|
193
|
+
qaPmTeams.push({
|
|
194
|
+
team: teamKey,
|
|
195
|
+
testing_status: statusResponse.testingStatus,
|
|
196
|
+
});
|
|
197
|
+
}
|
|
154
198
|
}
|
|
155
|
-
return
|
|
199
|
+
return qaPmTeams;
|
|
200
|
+
}
|
|
201
|
+
async function selectCompletionMode(hasQaPmTeams, options) {
|
|
202
|
+
if (!options.interactive) {
|
|
203
|
+
return hasQaPmTeams ? "upstream_strict" : "simple";
|
|
204
|
+
}
|
|
205
|
+
console.log("\nā
Completion Mode Configuration:");
|
|
206
|
+
const defaultMode = hasQaPmTeams ? 2 : 0; // upstream_strict if has QA/PM teams, else simple
|
|
207
|
+
const response = await prompts({
|
|
208
|
+
type: "select",
|
|
209
|
+
name: "mode",
|
|
210
|
+
message: "How should tasks be completed?",
|
|
211
|
+
choices: [
|
|
212
|
+
{
|
|
213
|
+
title: "Simple",
|
|
214
|
+
value: "simple",
|
|
215
|
+
description: "Mark task as done directly",
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
title: "Strict Review",
|
|
219
|
+
value: "strict_review",
|
|
220
|
+
description: "Mark task to dev team's testing status",
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
title: "Upstream Strict (recommended with QA/PM)",
|
|
224
|
+
value: "upstream_strict",
|
|
225
|
+
description: "Done + parent to testing, fallback to testing if no parent",
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
title: "Upstream Not Strict",
|
|
229
|
+
value: "upstream_not_strict",
|
|
230
|
+
description: "Done + parent to testing, no fallback",
|
|
231
|
+
},
|
|
232
|
+
],
|
|
233
|
+
initial: defaultMode,
|
|
234
|
+
});
|
|
235
|
+
return response.mode || (hasQaPmTeams ? "upstream_strict" : "simple");
|
|
156
236
|
}
|
|
157
237
|
async function selectUser(users, options) {
|
|
158
238
|
let currentUser = users[0];
|
|
@@ -220,19 +300,14 @@ async function selectStatusSource(options) {
|
|
|
220
300
|
});
|
|
221
301
|
return response.statusSource || "remote";
|
|
222
302
|
}
|
|
223
|
-
async function selectStatusMappings(devStates,
|
|
303
|
+
async function selectStatusMappings(devStates, options) {
|
|
224
304
|
// Use dev team states for todo, in_progress, done, blocked
|
|
225
|
-
//
|
|
305
|
+
// Testing status is now configured separately (dev_testing_status and qa_pm_teams)
|
|
226
306
|
const devDefaults = getDefaultStatusTransitions(devStates);
|
|
227
|
-
const testingStates = qaStates && qaStates.length > 0 ? qaStates : devStates;
|
|
228
|
-
const testingDefaults = getDefaultStatusTransitions(testingStates);
|
|
229
307
|
if (!options.interactive || devStates.length === 0) {
|
|
230
|
-
return
|
|
231
|
-
...devDefaults,
|
|
232
|
-
testing: testingDefaults.testing,
|
|
233
|
-
};
|
|
308
|
+
return devDefaults;
|
|
234
309
|
}
|
|
235
|
-
console.log("\nš Configure status mappings:");
|
|
310
|
+
console.log("\nš Configure status mappings (dev team):");
|
|
236
311
|
const devStateChoices = devStates.map((s) => ({
|
|
237
312
|
title: `${s.name} (${s.type})`,
|
|
238
313
|
value: s.name,
|
|
@@ -258,29 +333,8 @@ async function selectStatusMappings(devStates, qaStates, options) {
|
|
|
258
333
|
choices: devStateChoices,
|
|
259
334
|
initial: devStateChoices.findIndex((c) => c.value === devDefaults.done),
|
|
260
335
|
});
|
|
261
|
-
// Testing uses qa team states (or dev team if qa not set)
|
|
262
|
-
const testingStateChoices = testingStates.map((s) => ({
|
|
263
|
-
title: `${s.name} (${s.type})`,
|
|
264
|
-
value: s.name,
|
|
265
|
-
}));
|
|
266
|
-
const testingChoices = [
|
|
267
|
-
{ title: "(None)", value: undefined },
|
|
268
|
-
...testingStateChoices,
|
|
269
|
-
];
|
|
270
|
-
const testingMessage = qaStates && qaStates.length > 0
|
|
271
|
-
? 'Select status for "Testing" (from QA team, for parent tasks):'
|
|
272
|
-
: 'Select status for "Testing" (optional, for parent tasks):';
|
|
273
|
-
const testingResponse = await prompts({
|
|
274
|
-
type: "select",
|
|
275
|
-
name: "testing",
|
|
276
|
-
message: testingMessage,
|
|
277
|
-
choices: testingChoices,
|
|
278
|
-
initial: testingDefaults.testing
|
|
279
|
-
? testingChoices.findIndex((c) => c.value === testingDefaults.testing)
|
|
280
|
-
: 0,
|
|
281
|
-
});
|
|
282
336
|
const blockedChoices = [
|
|
283
|
-
{ title: "(
|
|
337
|
+
{ title: "(Skip - no blocked status)", value: undefined },
|
|
284
338
|
...devStateChoices,
|
|
285
339
|
];
|
|
286
340
|
const blockedResponse = await prompts({
|
|
@@ -296,7 +350,7 @@ async function selectStatusMappings(devStates, qaStates, options) {
|
|
|
296
350
|
todo: todoResponse.todo || devDefaults.todo,
|
|
297
351
|
in_progress: inProgressResponse.in_progress || devDefaults.in_progress,
|
|
298
352
|
done: doneResponse.done || devDefaults.done,
|
|
299
|
-
testing:
|
|
353
|
+
testing: devDefaults.testing, // Will be overridden by dev_testing_status in LocalConfig
|
|
300
354
|
blocked: blockedResponse.blocked,
|
|
301
355
|
};
|
|
302
356
|
}
|
|
@@ -346,147 +400,39 @@ async function updateGitignore(tttDir, interactive) {
|
|
|
346
400
|
// Silently ignore gitignore errors
|
|
347
401
|
}
|
|
348
402
|
}
|
|
349
|
-
async function
|
|
403
|
+
async function showPluginInstallInstructions(interactive) {
|
|
350
404
|
if (!interactive) {
|
|
351
|
-
return
|
|
405
|
+
return false;
|
|
352
406
|
}
|
|
353
|
-
console.log("\nš¤ Claude Code
|
|
354
|
-
|
|
355
|
-
const { install } = await prompts({
|
|
407
|
+
console.log("\nš¤ Claude Code Plugin:");
|
|
408
|
+
const { showInstructions } = await prompts({
|
|
356
409
|
type: "confirm",
|
|
357
|
-
name: "
|
|
358
|
-
message: "
|
|
410
|
+
name: "showInstructions",
|
|
411
|
+
message: "Show Claude Code plugin installation instructions? (provides /ttt-* commands)",
|
|
359
412
|
initial: true,
|
|
360
413
|
});
|
|
361
|
-
if (!
|
|
362
|
-
return
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
description: "/linear:work-on, /linear:done-job, /linear:sync-linear",
|
|
384
|
-
},
|
|
385
|
-
{
|
|
386
|
-
title: "Custom...",
|
|
387
|
-
value: "custom",
|
|
388
|
-
description: "Enter your own prefix",
|
|
389
|
-
},
|
|
390
|
-
],
|
|
391
|
-
initial: 0,
|
|
392
|
-
});
|
|
393
|
-
let prefix = prefixChoice || "";
|
|
394
|
-
if (prefixChoice === "custom") {
|
|
395
|
-
const { customPrefix } = await prompts({
|
|
396
|
-
type: "text",
|
|
397
|
-
name: "customPrefix",
|
|
398
|
-
message: "Enter custom prefix (e.g., 'my:'):",
|
|
399
|
-
initial: "",
|
|
400
|
-
});
|
|
401
|
-
prefix = customPrefix || "";
|
|
402
|
-
}
|
|
403
|
-
// Find templates directory
|
|
404
|
-
// Try multiple locations: installed package, local dev
|
|
405
|
-
const possibleTemplatePaths = [
|
|
406
|
-
path.join(__dirname, "..", "templates", "claude-code-commands"),
|
|
407
|
-
path.join(__dirname, "..", "..", "templates", "claude-code-commands"),
|
|
408
|
-
path.join(process.cwd(), "templates", "claude-code-commands"),
|
|
409
|
-
];
|
|
410
|
-
let templateDir = null;
|
|
411
|
-
for (const p of possibleTemplatePaths) {
|
|
412
|
-
try {
|
|
413
|
-
await fs.access(p);
|
|
414
|
-
templateDir = p;
|
|
415
|
-
break;
|
|
416
|
-
}
|
|
417
|
-
catch {
|
|
418
|
-
// Try next path
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
if (!templateDir) {
|
|
422
|
-
// Try to get repo URL from package.json
|
|
423
|
-
let repoUrl = "https://github.com/wayne930242/team-toon-tack";
|
|
424
|
-
try {
|
|
425
|
-
const pkgPaths = [
|
|
426
|
-
path.join(__dirname, "..", "package.json"),
|
|
427
|
-
path.join(__dirname, "..", "..", "package.json"),
|
|
428
|
-
];
|
|
429
|
-
for (const pkgPath of pkgPaths) {
|
|
430
|
-
try {
|
|
431
|
-
const pkgContent = await fs.readFile(pkgPath, "utf-8");
|
|
432
|
-
const pkg = JSON.parse(pkgContent);
|
|
433
|
-
if (pkg.repository?.url) {
|
|
434
|
-
// Parse git+https://github.com/user/repo.git format
|
|
435
|
-
repoUrl = pkg.repository.url
|
|
436
|
-
.replace(/^git\+/, "")
|
|
437
|
-
.replace(/\.git$/, "");
|
|
438
|
-
}
|
|
439
|
-
break;
|
|
440
|
-
}
|
|
441
|
-
catch {
|
|
442
|
-
// Try next path
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
catch {
|
|
447
|
-
// Use default URL
|
|
448
|
-
}
|
|
449
|
-
console.log(" ā Could not find command templates. Please copy manually from:");
|
|
450
|
-
console.log(` ${repoUrl}/tree/main/templates/claude-code-commands`);
|
|
451
|
-
return { installed: false, prefix };
|
|
452
|
-
}
|
|
453
|
-
// Create .claude/commands directory
|
|
454
|
-
const commandsDir = path.join(process.cwd(), ".claude", "commands");
|
|
455
|
-
await fs.mkdir(commandsDir, { recursive: true });
|
|
456
|
-
// Copy and rename template files
|
|
457
|
-
const templateFiles = await fs.readdir(templateDir);
|
|
458
|
-
const commandFiles = templateFiles.filter((f) => f.endsWith(".md"));
|
|
459
|
-
for (const file of commandFiles) {
|
|
460
|
-
const baseName = file.replace(".md", "");
|
|
461
|
-
const newFileName = prefix ? `${prefix}${baseName}.md` : file;
|
|
462
|
-
const srcPath = path.join(templateDir, file);
|
|
463
|
-
const destPath = path.join(commandsDir, newFileName);
|
|
464
|
-
// Read template content
|
|
465
|
-
let content = await fs.readFile(srcPath, "utf-8");
|
|
466
|
-
// Update the name in frontmatter if prefix is used
|
|
467
|
-
if (prefix) {
|
|
468
|
-
content = content.replace(/^(---\s*\n[\s\S]*?name:\s*)(\S+)/m, `$1${prefix}${baseName}`);
|
|
469
|
-
}
|
|
470
|
-
// Modify content based on statusSource for work-on and done-job
|
|
471
|
-
if (statusSource === "local") {
|
|
472
|
-
if (baseName === "work-on" || baseName.endsWith("work-on")) {
|
|
473
|
-
// Update description for local mode
|
|
474
|
-
content = content.replace(/Select a task and update status to "In Progress" on both local and Linear\./, 'Select a task and update local status to "In Progress". (Linear will be updated when you run `sync --update`)');
|
|
475
|
-
// Add reminder after Complete section
|
|
476
|
-
content = content.replace(/Use `?\/done-job`? to mark task as completed/, "Use `/done-job` to mark task as completed\n\n### 7. Sync to Linear\n\nWhen ready to update Linear with all your changes:\n\n```bash\nttt sync --update\n```");
|
|
477
|
-
}
|
|
478
|
-
if (baseName === "done-job" || baseName.endsWith("done-job")) {
|
|
479
|
-
// Update description for local mode
|
|
480
|
-
content = content.replace(/Mark a task as done and update Linear with commit details\./, "Mark a task as done locally. (Run `ttt sync --update` to push changes to Linear)");
|
|
481
|
-
// Add reminder at the end
|
|
482
|
-
content = content.replace(/## What It Does\n\n- Linear issue status ā "Done"/, "## What It Does\n\n- Local status ā `completed`");
|
|
483
|
-
content += `\n## Sync to Linear\n\nAfter completing tasks, push all changes to Linear:\n\n\`\`\`bash\nttt sync --update\n\`\`\`\n`;
|
|
484
|
-
}
|
|
485
|
-
}
|
|
486
|
-
await fs.writeFile(destPath, content, "utf-8");
|
|
487
|
-
console.log(` ā .claude/commands/${newFileName}`);
|
|
488
|
-
}
|
|
489
|
-
return { installed: true, prefix };
|
|
414
|
+
if (!showInstructions) {
|
|
415
|
+
return false;
|
|
416
|
+
}
|
|
417
|
+
console.log("\nāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā");
|
|
418
|
+
console.log("ā Install team-toon-tack plugin in Claude Code: ā");
|
|
419
|
+
console.log("āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¤");
|
|
420
|
+
console.log("ā ā");
|
|
421
|
+
console.log("ā 1. Add marketplace: ā");
|
|
422
|
+
console.log("ā /plugin marketplace add wayne930242/team-toon-tack ā");
|
|
423
|
+
console.log("ā ā");
|
|
424
|
+
console.log("ā 2. Install plugin: ā");
|
|
425
|
+
console.log("ā /plugin install team-toon-tack@wayne930242 ā");
|
|
426
|
+
console.log("ā ā");
|
|
427
|
+
console.log("ā Available commands after install: ā");
|
|
428
|
+
console.log("ā /ttt-sync - Sync Linear issues ā");
|
|
429
|
+
console.log("ā /ttt-work-on - Start working on a task ā");
|
|
430
|
+
console.log("ā /ttt-done - Complete current task ā");
|
|
431
|
+
console.log("ā /ttt-status - Show/modify task status ā");
|
|
432
|
+
console.log("ā /ttt-get-issue - Fetch issue details ā");
|
|
433
|
+
console.log("ā ā");
|
|
434
|
+
console.log("āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā");
|
|
435
|
+
return true;
|
|
490
436
|
}
|
|
491
437
|
async function init() {
|
|
492
438
|
const args = process.argv.slice(2);
|
|
@@ -536,12 +482,9 @@ async function init() {
|
|
|
536
482
|
console.error("Error: No teams found in your Linear workspace.");
|
|
537
483
|
process.exit(1);
|
|
538
484
|
}
|
|
539
|
-
// Select
|
|
540
|
-
const
|
|
541
|
-
console.log(`
|
|
542
|
-
if (selectedTeams.length > 1) {
|
|
543
|
-
console.log(` Primary: ${primaryTeam.name}`);
|
|
544
|
-
}
|
|
485
|
+
// Select dev team (single selection)
|
|
486
|
+
const devTeam = await selectDevTeam(teams, options);
|
|
487
|
+
console.log(` Dev Team: ${devTeam.name}`);
|
|
545
488
|
// Fetch data from ALL teams (not just primary) to support cross-team operations
|
|
546
489
|
console.log(` Fetching data from ${teams.length} teams...`);
|
|
547
490
|
// Collect users from all teams, but labels only from primary team
|
|
@@ -563,8 +506,8 @@ async function init() {
|
|
|
563
506
|
allUsers.push(user);
|
|
564
507
|
}
|
|
565
508
|
}
|
|
566
|
-
// Labels: only from
|
|
567
|
-
if (team.id ===
|
|
509
|
+
// Labels: only from dev team
|
|
510
|
+
if (team.id === devTeam.id) {
|
|
568
511
|
const labelsData = await client.issueLabels({
|
|
569
512
|
filter: { team: { id: { eq: team.id } } },
|
|
570
513
|
});
|
|
@@ -589,7 +532,7 @@ async function init() {
|
|
|
589
532
|
}
|
|
590
533
|
teamStatesMap.set(team.id, teamStates);
|
|
591
534
|
}
|
|
592
|
-
catch
|
|
535
|
+
catch {
|
|
593
536
|
console.warn(` ā Could not fetch data for team ${team.name}, skipping...`);
|
|
594
537
|
}
|
|
595
538
|
}
|
|
@@ -597,34 +540,34 @@ async function init() {
|
|
|
597
540
|
const labels = allLabels;
|
|
598
541
|
const states = allStates;
|
|
599
542
|
// Get team-specific states for status mapping
|
|
600
|
-
const
|
|
543
|
+
const devTeamStates = teamStatesMap.get(devTeam.id) || [];
|
|
601
544
|
console.log(` Users: ${users.length}`);
|
|
602
|
-
console.log(` Labels: ${labels.length} (from ${
|
|
545
|
+
console.log(` Labels: ${labels.length} (from ${devTeam.name})`);
|
|
603
546
|
console.log(` Workflow states: ${states.length}`);
|
|
604
|
-
// Get cycle from
|
|
605
|
-
const selectedTeam = await client.team(
|
|
547
|
+
// Get cycle from dev team (for current work tracking)
|
|
548
|
+
const selectedTeam = await client.team(devTeam.id);
|
|
606
549
|
const currentCycle = (await selectedTeam.activeCycle);
|
|
607
550
|
// User selections
|
|
608
551
|
const currentUser = await selectUser(users, options);
|
|
609
552
|
const defaultLabel = await selectLabelFilter(labels, options);
|
|
610
553
|
const statusSource = await selectStatusSource(options);
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
const
|
|
554
|
+
// Status transitions for dev team (todo, in_progress, done, blocked)
|
|
555
|
+
const statusTransitions = await selectStatusMappings(devTeamStates, options);
|
|
556
|
+
// Dev team testing status (for strict_review mode)
|
|
557
|
+
const devTestingStatus = await selectDevTestingStatus(devTeamStates, options);
|
|
558
|
+
// Build preliminary teams config for selectQaPmTeams
|
|
559
|
+
const teamsConfig = buildTeamsConfig(teams);
|
|
560
|
+
// QA/PM teams selection (multiple, each with its own testing status)
|
|
561
|
+
const qaPmTeams = await selectQaPmTeams(teams, devTeam, teamStatesMap, teamsConfig, options);
|
|
562
|
+
// Completion mode selection
|
|
563
|
+
const completionMode = await selectCompletionMode(qaPmTeams.length > 0, options);
|
|
615
564
|
// Build config
|
|
616
565
|
const config = buildConfig(teams, users, labels, states, statusTransitions, currentCycle ?? undefined);
|
|
617
566
|
// Find keys
|
|
618
567
|
const currentUserKey = findUserKey(config.users, currentUser.id);
|
|
619
|
-
const
|
|
620
|
-
const
|
|
621
|
-
|
|
622
|
-
.filter((key) => key !== undefined);
|
|
623
|
-
const qaPmTeamKey = qaPmTeam
|
|
624
|
-
? findTeamKey(config.teams, qaPmTeam.id)
|
|
625
|
-
: undefined;
|
|
626
|
-
const localConfig = buildLocalConfig(currentUserKey, primaryTeamKey, selectedTeamKeys, defaultLabel, undefined, // excludeLabels
|
|
627
|
-
statusSource, qaPmTeamKey);
|
|
568
|
+
const devTeamKey = findTeamKey(config.teams, devTeam.id);
|
|
569
|
+
const localConfig = buildLocalConfig(currentUserKey, devTeamKey, devTestingStatus, qaPmTeams, completionMode, defaultLabel, undefined, // excludeLabels
|
|
570
|
+
statusSource);
|
|
628
571
|
// Write config files
|
|
629
572
|
console.log("\nš Writing configuration files...");
|
|
630
573
|
await fs.mkdir(paths.baseDir, { recursive: true });
|
|
@@ -659,10 +602,12 @@ async function init() {
|
|
|
659
602
|
localConfig.current_user = existingLocal.current_user;
|
|
660
603
|
if (existingLocal.team)
|
|
661
604
|
localConfig.team = existingLocal.team;
|
|
662
|
-
if (existingLocal.
|
|
663
|
-
localConfig.
|
|
664
|
-
if (existingLocal.
|
|
665
|
-
localConfig.
|
|
605
|
+
if (existingLocal.dev_testing_status)
|
|
606
|
+
localConfig.dev_testing_status = existingLocal.dev_testing_status;
|
|
607
|
+
if (existingLocal.qa_pm_teams)
|
|
608
|
+
localConfig.qa_pm_teams = existingLocal.qa_pm_teams;
|
|
609
|
+
if (existingLocal.completion_mode)
|
|
610
|
+
localConfig.completion_mode = existingLocal.completion_mode;
|
|
666
611
|
if (existingLocal.label)
|
|
667
612
|
localConfig.label = existingLocal.label;
|
|
668
613
|
if (existingLocal.exclude_labels)
|
|
@@ -679,20 +624,24 @@ async function init() {
|
|
|
679
624
|
console.log(` ā ${paths.localPath}`);
|
|
680
625
|
// Update .gitignore (always use relative path .ttt)
|
|
681
626
|
await updateGitignore(".ttt", options.interactive ?? true);
|
|
682
|
-
//
|
|
683
|
-
const
|
|
627
|
+
// Show Claude Code plugin installation instructions
|
|
628
|
+
const pluginInstructionsShown = await showPluginInstallInstructions(options.interactive ?? true);
|
|
684
629
|
// Summary
|
|
685
630
|
console.log("\nā
Initialization complete!\n");
|
|
686
631
|
console.log("Configuration summary:");
|
|
687
|
-
console.log(`
|
|
688
|
-
if (selectedTeams.length > 1) {
|
|
689
|
-
console.log(` Primary team: ${primaryTeam.name}`);
|
|
690
|
-
}
|
|
632
|
+
console.log(` Dev Team: ${devTeam.name}`);
|
|
691
633
|
console.log(` User: ${currentUser.displayName || currentUser.name} (${currentUser.email})`);
|
|
692
634
|
console.log(` Label filter: ${defaultLabel || "(none)"}`);
|
|
693
635
|
console.log(` Status source: ${statusSource === "local" ? "local (use 'sync --update' to push)" : "remote (immediate sync)"}`);
|
|
694
|
-
|
|
695
|
-
|
|
636
|
+
console.log(` Completion mode: ${completionMode}`);
|
|
637
|
+
if (devTestingStatus) {
|
|
638
|
+
console.log(` Dev testing status: ${devTestingStatus}`);
|
|
639
|
+
}
|
|
640
|
+
if (qaPmTeams.length > 0) {
|
|
641
|
+
console.log(` QA/PM teams:`);
|
|
642
|
+
for (const qaPmTeam of qaPmTeams) {
|
|
643
|
+
console.log(` - ${qaPmTeam.team}: ${qaPmTeam.testing_status}`);
|
|
644
|
+
}
|
|
696
645
|
}
|
|
697
646
|
console.log(` (Use 'ttt config filters' to set excluded labels/users)`);
|
|
698
647
|
if (currentCycle) {
|
|
@@ -702,27 +651,16 @@ async function init() {
|
|
|
702
651
|
console.log(` Todo: ${statusTransitions.todo}`);
|
|
703
652
|
console.log(` In Progress: ${statusTransitions.in_progress}`);
|
|
704
653
|
console.log(` Done: ${statusTransitions.done}`);
|
|
705
|
-
if (statusTransitions.testing) {
|
|
706
|
-
console.log(` Testing: ${statusTransitions.testing}`);
|
|
707
|
-
}
|
|
708
654
|
if (statusTransitions.blocked) {
|
|
709
655
|
console.log(` Blocked: ${statusTransitions.blocked}`);
|
|
710
656
|
}
|
|
711
|
-
if (commandsInstalled) {
|
|
712
|
-
const cmdPrefix = commandPrefix ? `${commandPrefix}` : "";
|
|
713
|
-
console.log(` Claude commands: /${cmdPrefix}work-on, /${cmdPrefix}done-job, /${cmdPrefix}sync-linear`);
|
|
714
|
-
}
|
|
715
657
|
console.log("\nNext steps:");
|
|
716
658
|
console.log(" 1. Set LINEAR_API_KEY in your shell profile:");
|
|
717
659
|
console.log(` export LINEAR_API_KEY="${apiKey}"`);
|
|
718
660
|
console.log(" 2. Run sync: ttt sync");
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
console.log(
|
|
722
|
-
console.log(`\nš” Tip: Edit .claude/commands/${cmdPrefix}work-on.md to customize the "Verify" section for your project.`);
|
|
723
|
-
}
|
|
724
|
-
else {
|
|
725
|
-
console.log(" 3. Start working: ttt work-on");
|
|
661
|
+
console.log(" 3. Start working: ttt work-on");
|
|
662
|
+
if (pluginInstructionsShown) {
|
|
663
|
+
console.log("\nš” Tip: Install the Claude Code plugin for /ttt-* commands.");
|
|
726
664
|
}
|
|
727
665
|
}
|
|
728
666
|
init().catch(console.error);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Config, LocalConfig, StatusTransitions, TeamConfig, UserConfig
|
|
1
|
+
import type { CompletionMode, Config, LabelConfig, LocalConfig, QaPmTeamConfig, StatusTransitions, TeamConfig, UserConfig } from "../utils.js";
|
|
2
2
|
export interface LinearTeam {
|
|
3
3
|
id: string;
|
|
4
4
|
name: string;
|
|
@@ -38,4 +38,4 @@ export declare function getDefaultStatusTransitions(states: LinearState[]): Stat
|
|
|
38
38
|
export declare function buildConfig(teams: LinearTeam[], users: LinearUser[], labels: LinearLabel[], states: LinearState[], statusTransitions: StatusTransitions, currentCycle?: LinearCycle): Config;
|
|
39
39
|
export declare function findUserKey(usersConfig: Record<string, UserConfig>, userId: string): string;
|
|
40
40
|
export declare function findTeamKey(teamsConfig: Record<string, TeamConfig>, teamId: string): string;
|
|
41
|
-
export declare function buildLocalConfig(currentUserKey: string,
|
|
41
|
+
export declare function buildLocalConfig(currentUserKey: string, devTeamKey: string, devTestingStatus?: string, qaPmTeams?: QaPmTeamConfig[], completionMode?: CompletionMode, defaultLabel?: string, excludeLabels?: string[], statusSource?: "remote" | "local"): LocalConfig;
|
|
@@ -108,12 +108,13 @@ export function findTeamKey(teamsConfig, teamId) {
|
|
|
108
108
|
return (Object.entries(teamsConfig).find(([_, t]) => t.id === teamId)?.[0] ||
|
|
109
109
|
Object.keys(teamsConfig)[0]);
|
|
110
110
|
}
|
|
111
|
-
export function buildLocalConfig(currentUserKey,
|
|
111
|
+
export function buildLocalConfig(currentUserKey, devTeamKey, devTestingStatus, qaPmTeams, completionMode, defaultLabel, excludeLabels, statusSource) {
|
|
112
112
|
return {
|
|
113
113
|
current_user: currentUserKey,
|
|
114
|
-
team:
|
|
115
|
-
|
|
116
|
-
|
|
114
|
+
team: devTeamKey,
|
|
115
|
+
dev_testing_status: devTestingStatus,
|
|
116
|
+
qa_pm_teams: qaPmTeams && qaPmTeams.length > 0 ? qaPmTeams : undefined,
|
|
117
|
+
completion_mode: completionMode,
|
|
117
118
|
label: defaultLabel,
|
|
118
119
|
exclude_labels: excludeLabels && excludeLabels.length > 0 ? excludeLabels : undefined,
|
|
119
120
|
status_source: statusSource,
|