team-toon-tack 3.5.0 → 3.6.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/bin/cli.js CHANGED
@@ -2,6 +2,7 @@
2
2
  import { readFileSync } from "node:fs";
3
3
  import { dirname, join, resolve } from "node:path";
4
4
  import { fileURLToPath, pathToFileURL } from "node:url";
5
+ import { loadDotEnv, resolveLinearApiKey } from "../scripts/lib/env.js";
5
6
  const __dirname = dirname(fileURLToPath(import.meta.url));
6
7
  // When running from dist/bin/cli.js, we need to go up two levels to find package.json
7
8
  const pkg = JSON.parse(readFileSync(join(__dirname, "..", "..", "package.json"), "utf-8"));
@@ -107,6 +108,10 @@ async function main() {
107
108
  const { dir, commandArgs } = parseGlobalArgs(restArgs);
108
109
  // Set TOON_DIR for scripts to use
109
110
  process.env.TOON_DIR = dir;
111
+ // Load .ttt/.env (if present) and resolve configured Linear API key env
112
+ // var into LINEAR_API_KEY so downstream code is workspace-aware.
113
+ await loadDotEnv(join(dir, ".env"));
114
+ await resolveLinearApiKey(join(dir, "local.toon"));
110
115
  if (!COMMANDS.includes(command)) {
111
116
  console.error(`Unknown command: ${command}`);
112
117
  console.error(`Run 'ttt help' for usage.`);
@@ -22,6 +22,7 @@ async function init() {
22
22
  localPath: paths.localPath,
23
23
  cyclePath: paths.cyclePath,
24
24
  outputPath: paths.outputPath,
25
+ envPath: paths.envPath,
25
26
  };
26
27
  console.log("šŸš€ Team Toon Tack Initialization\n");
27
28
  // Check existing files
@@ -39,4 +39,4 @@ export declare function buildConfig(teams: LinearTeam[], users: LinearUser[], la
39
39
  export declare function findUserKey(usersConfig: Record<string, UserConfig>, userId: string): string;
40
40
  export declare function findUserKeys(usersConfig: Record<string, UserConfig>, userIds: string[]): string[];
41
41
  export declare function findTeamKey(teamsConfig: Record<string, TeamConfig>, teamId: string): string;
42
- export declare function buildLocalConfig(currentUserKey: string | string[], devTeamKey: string, devTestingStatus?: string, qaPmTeams?: QaPmTeamConfig[], completionMode?: CompletionMode, defaultLabels?: string[], excludeLabels?: string[], statusSource?: "remote" | "local"): LocalConfig;
42
+ export declare function buildLocalConfig(currentUserKey: string | string[], devTeamKey: string, devTestingStatus?: string, qaPmTeams?: QaPmTeamConfig[], completionMode?: CompletionMode, defaultLabels?: string[], excludeLabels?: string[], statusSource?: "remote" | "local", linearApiKeyEnv?: string): LocalConfig;
@@ -113,7 +113,7 @@ export function findTeamKey(teamsConfig, teamId) {
113
113
  return (Object.entries(teamsConfig).find(([_, t]) => t.id === teamId)?.[0] ||
114
114
  Object.keys(teamsConfig)[0]);
115
115
  }
116
- export function buildLocalConfig(currentUserKey, devTeamKey, devTestingStatus, qaPmTeams, completionMode, defaultLabels, excludeLabels, statusSource) {
116
+ export function buildLocalConfig(currentUserKey, devTeamKey, devTestingStatus, qaPmTeams, completionMode, defaultLabels, excludeLabels, statusSource, linearApiKeyEnv) {
117
117
  return {
118
118
  current_user: currentUserKey,
119
119
  team: devTeamKey,
@@ -123,5 +123,8 @@ export function buildLocalConfig(currentUserKey, devTeamKey, devTestingStatus, q
123
123
  labels: defaultLabels && defaultLabels.length > 0 ? defaultLabels : undefined,
124
124
  exclude_labels: excludeLabels && excludeLabels.length > 0 ? excludeLabels : undefined,
125
125
  status_source: statusSource,
126
+ linear_api_key_env: linearApiKeyEnv && linearApiKeyEnv !== "LINEAR_API_KEY"
127
+ ? linearApiKeyEnv
128
+ : undefined,
126
129
  };
127
130
  }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Minimal .env file loader and writer.
3
+ * No external dependency. Values already in process.env take precedence
4
+ * over values loaded from the file.
5
+ */
6
+ /** Populate process.env from envPath. Existing process.env values win. */
7
+ export declare function loadDotEnv(envPath: string): Promise<void>;
8
+ /**
9
+ * Read linear_api_key_env from local.toon and mirror the chosen env var
10
+ * into LINEAR_API_KEY. No-op if local.toon is missing or the field is
11
+ * absent/default. Uses substring parsing to avoid importing the Linear SDK
12
+ * at CLI startup.
13
+ */
14
+ export declare function resolveLinearApiKey(localPath: string): Promise<void>;
15
+ /** Merge vars into envPath, preserving existing entries. Creates with 0600. */
16
+ export declare function writeDotEnv(envPath: string, vars: Record<string, string>): Promise<void>;
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Minimal .env file loader and writer.
3
+ * No external dependency. Values already in process.env take precedence
4
+ * over values loaded from the file.
5
+ */
6
+ import fs from "node:fs/promises";
7
+ const ENV_LINE = /^([A-Z_][A-Z0-9_]*)\s*=\s*(.*)$/i;
8
+ function parseEnv(content) {
9
+ const result = {};
10
+ for (const rawLine of content.split("\n")) {
11
+ const line = rawLine.trim();
12
+ if (!line || line.startsWith("#"))
13
+ continue;
14
+ const match = line.match(ENV_LINE);
15
+ if (!match)
16
+ continue;
17
+ let value = match[2];
18
+ if ((value.startsWith('"') && value.endsWith('"')) ||
19
+ (value.startsWith("'") && value.endsWith("'"))) {
20
+ value = value.slice(1, -1);
21
+ }
22
+ result[match[1]] = value;
23
+ }
24
+ return result;
25
+ }
26
+ function quoteIfNeeded(value) {
27
+ if (/[\s"'#]/.test(value)) {
28
+ return `"${value.replace(/"/g, '\\"')}"`;
29
+ }
30
+ return value;
31
+ }
32
+ /** Populate process.env from envPath. Existing process.env values win. */
33
+ export async function loadDotEnv(envPath) {
34
+ let content;
35
+ try {
36
+ content = await fs.readFile(envPath, "utf-8");
37
+ }
38
+ catch {
39
+ return;
40
+ }
41
+ const parsed = parseEnv(content);
42
+ for (const [key, value] of Object.entries(parsed)) {
43
+ if (process.env[key] === undefined) {
44
+ process.env[key] = value;
45
+ }
46
+ }
47
+ }
48
+ /**
49
+ * Read linear_api_key_env from local.toon and mirror the chosen env var
50
+ * into LINEAR_API_KEY. No-op if local.toon is missing or the field is
51
+ * absent/default. Uses substring parsing to avoid importing the Linear SDK
52
+ * at CLI startup.
53
+ */
54
+ export async function resolveLinearApiKey(localPath) {
55
+ let content;
56
+ try {
57
+ content = await fs.readFile(localPath, "utf-8");
58
+ }
59
+ catch {
60
+ return;
61
+ }
62
+ const match = content.match(/^linear_api_key_env\s*:\s*["']?([A-Z_][A-Z0-9_]*)["']?\s*$/im);
63
+ const envName = match?.[1];
64
+ if (!envName || envName === "LINEAR_API_KEY")
65
+ return;
66
+ const value = process.env[envName];
67
+ if (value) {
68
+ process.env.LINEAR_API_KEY = value;
69
+ }
70
+ }
71
+ /** Merge vars into envPath, preserving existing entries. Creates with 0600. */
72
+ export async function writeDotEnv(envPath, vars) {
73
+ let existing = {};
74
+ try {
75
+ const content = await fs.readFile(envPath, "utf-8");
76
+ existing = parseEnv(content);
77
+ }
78
+ catch {
79
+ // no existing file
80
+ }
81
+ const merged = { ...existing, ...vars };
82
+ const body = `${Object.entries(merged)
83
+ .map(([k, v]) => `${k}=${quoteIfNeeded(v)}`)
84
+ .join("\n")}\n`;
85
+ await fs.writeFile(envPath, body, { mode: 0o600 });
86
+ }
@@ -5,7 +5,8 @@ import fs from "node:fs/promises";
5
5
  import { confirm } from "@inquirer/prompts";
6
6
  export async function updateGitignore(tttDir, interactive) {
7
7
  const gitignorePath = ".gitignore";
8
- const entry = `${tttDir}/`;
8
+ const tttEntry = `${tttDir}/`;
9
+ const envEntry = ".env";
9
10
  try {
10
11
  let content = "";
11
12
  let exists = false;
@@ -16,32 +17,37 @@ export async function updateGitignore(tttDir, interactive) {
16
17
  catch {
17
18
  // .gitignore doesn't exist
18
19
  }
19
- // Check if already ignored
20
- const lines = content.split("\n");
21
- const alreadyIgnored = lines.some((line) => line.trim() === entry ||
22
- line.trim() === tttDir ||
23
- line.trim() === `/${entry}` ||
24
- line.trim() === `/${tttDir}`);
25
- if (alreadyIgnored) {
20
+ const lines = content.split("\n").map((l) => l.trim());
21
+ const hasTtt = lines.some((line) => line === tttEntry ||
22
+ line === tttDir ||
23
+ line === `/${tttEntry}` ||
24
+ line === `/${tttDir}`);
25
+ const hasEnv = lines.some((line) => line === envEntry || line === "/.env");
26
+ const toAdd = [];
27
+ if (!hasTtt)
28
+ toAdd.push(tttEntry);
29
+ if (!hasEnv)
30
+ toAdd.push(envEntry);
31
+ if (toAdd.length === 0)
26
32
  return;
27
- }
28
- // Ask user in interactive mode
29
33
  if (interactive) {
30
34
  const addToGitignore = await confirm({
31
- message: `Add ${entry} to .gitignore?`,
35
+ message: `Add ${toAdd.join(" and ")} to .gitignore?`,
32
36
  default: true,
33
37
  });
34
38
  if (!addToGitignore)
35
39
  return;
36
40
  }
37
- // Add to .gitignore
41
+ const suffix = `${toAdd.join("\n")}\n`;
38
42
  const newContent = exists
39
43
  ? content.endsWith("\n")
40
- ? `${content}${entry}\n`
41
- : `${content}\n${entry}\n`
42
- : `${entry}\n`;
44
+ ? `${content}${suffix}`
45
+ : `${content}\n${suffix}`
46
+ : suffix;
43
47
  await fs.writeFile(gitignorePath, newContent, "utf-8");
44
- console.log(` āœ“ Added ${entry} to .gitignore`);
48
+ for (const entry of toAdd) {
49
+ console.log(` āœ“ Added ${entry} to .gitignore`);
50
+ }
45
51
  }
46
52
  catch (_error) {
47
53
  // Silently ignore gitignore errors
@@ -2,21 +2,43 @@
2
2
  * Linear initialization flow
3
3
  */
4
4
  import fs from "node:fs/promises";
5
+ import { confirm } from "@inquirer/prompts";
5
6
  import { decode, encode } from "@toon-format/toon";
6
7
  import { fileExists, getLinearClient, } from "../../utils.js";
7
8
  import { buildConfig, buildLocalConfig, buildTeamsConfig, findTeamKey, findUserKey, findUserKeys, } from "../config-builder.js";
9
+ import { writeDotEnv } from "../env.js";
8
10
  import { formatTodoStatus } from "../status-helpers.js";
9
11
  import { showPluginInstallInstructions, updateGitignore } from "./file-ops.js";
10
12
  import { promptForApiKey, selectCompletionMode, selectDevTeam, selectDevTestingStatus, selectQaPmTeams, selectStatusMappings, } from "./linear-prompts.js";
11
13
  import { selectLabelFilter, selectStatusSource, selectUsers, } from "./prompts.js";
12
14
  export async function initLinear(options, paths) {
13
- // Get API key
14
- const apiKey = await promptForApiKey(options);
15
- if (!apiKey) {
15
+ // Get API key (workspace-aware)
16
+ const resolved = await promptForApiKey(options);
17
+ if (!resolved) {
16
18
  console.error("Error: LINEAR_API_KEY is required.");
17
19
  console.error("Get your API key from: https://linear.app/settings/api");
18
20
  process.exit(1);
19
21
  }
22
+ const { apiKey, envName, fromSystemEnv } = resolved;
23
+ // Mirror into LINEAR_API_KEY so getLinearClient() picks it up regardless
24
+ // of which env var name the user selected.
25
+ process.env.LINEAR_API_KEY = apiKey;
26
+ // Offer to persist to .ttt/.env. Default yes for freshly entered keys,
27
+ // no for keys already in the system env (user already has them set).
28
+ let saveToEnvFile = false;
29
+ if (options.interactive) {
30
+ saveToEnvFile = await confirm({
31
+ message: fromSystemEnv
32
+ ? `Save ${envName} to ${paths.envPath} for this project?`
33
+ : `Save ${envName} to ${paths.envPath}?`,
34
+ default: !fromSystemEnv,
35
+ });
36
+ }
37
+ if (saveToEnvFile) {
38
+ await fs.mkdir(paths.baseDir, { recursive: true });
39
+ await writeDotEnv(paths.envPath, { [envName]: apiKey });
40
+ console.log(` āœ“ Wrote ${envName} to ${paths.envPath}`);
41
+ }
20
42
  // Create Linear client
21
43
  const client = getLinearClient();
22
44
  console.log("\nšŸ“” Fetching data from Linear...");
@@ -120,7 +142,7 @@ export async function initLinear(options, paths) {
120
142
  const currentUserValue = currentUserKeys.length === 1 ? currentUserKeys[0] : currentUserKeys;
121
143
  const devTeamKey = findTeamKey(config.teams, devTeam.id);
122
144
  const localConfig = buildLocalConfig(currentUserValue, devTeamKey, devTestingStatus, qaPmTeams, completionMode, defaultLabel, undefined, // excludeLabels
123
- statusSource);
145
+ statusSource, envName);
124
146
  // Write config files
125
147
  console.log("\nšŸ“ Writing configuration files...");
126
148
  await fs.mkdir(paths.baseDir, { recursive: true });
@@ -131,7 +153,9 @@ export async function initLinear(options, paths) {
131
153
  if (configExists && !options.force) {
132
154
  try {
133
155
  const existingContent = await fs.readFile(paths.configPath, "utf-8");
134
- const existingConfig = decode(existingContent, { strict: false });
156
+ const existingConfig = decode(existingContent, {
157
+ strict: false,
158
+ });
135
159
  if (existingConfig.cycle_history) {
136
160
  config.cycle_history = existingConfig.cycle_history;
137
161
  }
@@ -152,7 +176,9 @@ export async function initLinear(options, paths) {
152
176
  if (localExists && !options.force) {
153
177
  try {
154
178
  const existingContent = await fs.readFile(paths.localPath, "utf-8");
155
- const existingLocal = decode(existingContent, { strict: false });
179
+ const existingLocal = decode(existingContent, {
180
+ strict: false,
181
+ });
156
182
  if (!options.interactive) {
157
183
  if (existingLocal.current_user)
158
184
  localConfig.current_user = existingLocal.current_user;
@@ -170,6 +196,8 @@ export async function initLinear(options, paths) {
170
196
  localConfig.exclude_labels = existingLocal.exclude_labels;
171
197
  if (existingLocal.status_source)
172
198
  localConfig.status_source = existingLocal.status_source;
199
+ if (existingLocal.linear_api_key_env)
200
+ localConfig.linear_api_key_env = existingLocal.linear_api_key_env;
173
201
  }
174
202
  }
175
203
  catch {
@@ -183,6 +211,12 @@ export async function initLinear(options, paths) {
183
211
  // Summary
184
212
  console.log("\nāœ… Initialization complete!\n");
185
213
  console.log("Configuration summary:");
214
+ if (resolved.organizationName) {
215
+ console.log(` Workspace: ${resolved.organizationName} (via ${envName})`);
216
+ }
217
+ else {
218
+ console.log(` API key env: ${envName}`);
219
+ }
186
220
  console.log(` Dev Team: ${devTeam.name}`);
187
221
  if (selectedUsers.length === 0) {
188
222
  console.log(" Users: (all team members)");
@@ -217,11 +251,11 @@ export async function initLinear(options, paths) {
217
251
  console.log(` Blocked: ${statusTransitions.blocked}`);
218
252
  }
219
253
  console.log("\nNext steps:");
220
- if (!process.env.LINEAR_API_KEY) {
221
- // Show partial key for confirmation (first 12 chars + masked)
254
+ const needsShellExport = !saveToEnvFile && !fromSystemEnv;
255
+ if (needsShellExport) {
222
256
  const maskedKey = `${apiKey.slice(0, 12)}...${"*".repeat(8)}`;
223
- console.log(" 1. Set LINEAR_API_KEY in your shell profile:");
224
- console.log(` export LINEAR_API_KEY="${maskedKey}"`);
257
+ console.log(` 1. Set ${envName} in your shell profile:`);
258
+ console.log(` export ${envName}="${maskedKey}"`);
225
259
  console.log(" 2. Run sync: ttt sync");
226
260
  console.log(" 3. Start working: ttt work-on");
227
261
  }
@@ -4,7 +4,18 @@
4
4
  import type { CompletionMode, QaPmTeamConfig, StatusTransitions } from "../../utils.js";
5
5
  import { type LinearState, type LinearTeam } from "../config-builder.js";
6
6
  import type { InitOptions } from "./types.js";
7
- export declare function promptForApiKey(options: InitOptions): Promise<string | undefined>;
7
+ export interface ResolvedApiKey {
8
+ apiKey: string;
9
+ envName: string;
10
+ fromSystemEnv: boolean;
11
+ organizationName?: string;
12
+ }
13
+ /**
14
+ * Detect available Linear API keys from process.env, probe each for its
15
+ * workspace, and let the user pick. Supports adding a new key inline.
16
+ * Returns the chosen key plus the env var name that holds it.
17
+ */
18
+ export declare function promptForApiKey(options: InitOptions): Promise<ResolvedApiKey | undefined>;
8
19
  export declare function selectDevTeam(teams: LinearTeam[], options: InitOptions): Promise<LinearTeam>;
9
20
  export declare function selectDevTestingStatus(devStates: LinearState[], options: InitOptions): Promise<string | undefined>;
10
21
  export declare function selectQaPmTeams(teams: LinearTeam[], devTeam: LinearTeam, teamStatesMap: Map<string, LinearState[]>, teamsConfig: Record<string, {
@@ -1,20 +1,136 @@
1
1
  /**
2
2
  * Linear-specific prompt functions for init
3
3
  */
4
- import { checkbox, password, select } from "@inquirer/prompts";
4
+ import { checkbox, input, password, select } from "@inquirer/prompts";
5
+ import { LinearClient } from "@linear/sdk";
5
6
  import { getDefaultStatusTransitions, } from "../config-builder.js";
6
7
  import { getFirstTodoStatus } from "../status-helpers.js";
8
+ function collectEnvCandidates() {
9
+ const candidates = [];
10
+ for (const [name, value] of Object.entries(process.env)) {
11
+ if (!value)
12
+ continue;
13
+ if (name === "LINEAR_API_KEY" || name.startsWith("LINEAR_API_KEY_")) {
14
+ if (value.startsWith("lin_api_")) {
15
+ candidates.push({ envName: name, apiKey: value });
16
+ }
17
+ }
18
+ }
19
+ return candidates;
20
+ }
21
+ async function probeWorkspace(candidate) {
22
+ try {
23
+ const client = new LinearClient({ apiKey: candidate.apiKey });
24
+ const org = await client.organization;
25
+ candidate.orgName = org.name;
26
+ candidate.orgUrlKey = org.urlKey;
27
+ }
28
+ catch (err) {
29
+ candidate.error = err instanceof Error ? err.message : String(err);
30
+ }
31
+ }
32
+ function validateApiKey(v) {
33
+ return v.startsWith("lin_api_")
34
+ ? true
35
+ : 'API key should start with "lin_api_"';
36
+ }
37
+ /**
38
+ * Detect available Linear API keys from process.env, probe each for its
39
+ * workspace, and let the user pick. Supports adding a new key inline.
40
+ * Returns the chosen key plus the env var name that holds it.
41
+ */
7
42
  export async function promptForApiKey(options) {
8
- let apiKey = options.apiKey || process.env.LINEAR_API_KEY;
9
- if (!apiKey && options.interactive) {
10
- apiKey = await password({
11
- message: "Enter your Linear API key:",
12
- validate: (v) => v.startsWith("lin_api_")
13
- ? true
14
- : 'API key should start with "lin_api_"',
43
+ // Non-interactive: use --api-key or LINEAR_API_KEY, no selection possible
44
+ if (!options.interactive) {
45
+ const apiKey = options.apiKey || process.env.LINEAR_API_KEY;
46
+ if (!apiKey)
47
+ return undefined;
48
+ return {
49
+ apiKey,
50
+ envName: "LINEAR_API_KEY",
51
+ fromSystemEnv: !options.apiKey && !!process.env.LINEAR_API_KEY,
52
+ };
53
+ }
54
+ // --api-key flag short-circuits the workspace picker
55
+ if (options.apiKey) {
56
+ return {
57
+ apiKey: options.apiKey,
58
+ envName: "LINEAR_API_KEY",
59
+ fromSystemEnv: false,
60
+ };
61
+ }
62
+ const envCandidates = collectEnvCandidates();
63
+ if (envCandidates.length > 0) {
64
+ console.log(`\nšŸ”‘ Found ${envCandidates.length} Linear API key${envCandidates.length > 1 ? "s" : ""} in environment. Probing workspaces...`);
65
+ await Promise.all(envCandidates.map(probeWorkspace));
66
+ for (const c of envCandidates) {
67
+ if (c.error) {
68
+ console.log(` āœ— ${c.envName} — error: ${c.error}`);
69
+ }
70
+ else {
71
+ console.log(` āœ“ ${c.envName} → ${c.orgName ?? "?"}${c.orgUrlKey ? ` (${c.orgUrlKey})` : ""}`);
72
+ }
73
+ }
74
+ const valid = envCandidates.filter((c) => !c.error);
75
+ const choices = [
76
+ ...valid.map((c) => ({
77
+ name: `${c.orgName ?? "(unknown)"} [${c.envName}]`,
78
+ value: c.envName,
79
+ description: c.orgUrlKey ? `${c.orgUrlKey}.linear.app` : undefined,
80
+ })),
81
+ { name: "+ Enter a different API key", value: "__new__" },
82
+ ];
83
+ const chosen = await select({
84
+ message: "Select workspace:",
85
+ choices,
15
86
  });
87
+ if (chosen !== "__new__") {
88
+ const picked = valid.find((c) => c.envName === chosen);
89
+ if (picked) {
90
+ return {
91
+ apiKey: picked.apiKey,
92
+ envName: picked.envName,
93
+ fromSystemEnv: true,
94
+ organizationName: picked.orgName,
95
+ };
96
+ }
97
+ }
16
98
  }
17
- return apiKey;
99
+ // No env keys, or user chose to enter a new one
100
+ const apiKey = await password({
101
+ message: "Enter your Linear API key:",
102
+ validate: validateApiKey,
103
+ });
104
+ const newCandidate = {
105
+ envName: "LINEAR_API_KEY",
106
+ apiKey,
107
+ };
108
+ await probeWorkspace(newCandidate);
109
+ if (newCandidate.error) {
110
+ console.error(` āœ— Failed to fetch workspace: ${newCandidate.error}`);
111
+ return undefined;
112
+ }
113
+ console.log(` āœ“ Workspace: ${newCandidate.orgName}${newCandidate.orgUrlKey ? ` (${newCandidate.orgUrlKey})` : ""}`);
114
+ // Ask for env var name (default to a slugged workspace name if LINEAR_API_KEY is taken)
115
+ const defaultName = process.env.LINEAR_API_KEY
116
+ ? `LINEAR_API_KEY_${(newCandidate.orgUrlKey ??
117
+ newCandidate.orgName ??
118
+ "ALT")
119
+ .toUpperCase()
120
+ .replace(/[^A-Z0-9]/g, "_")}`
121
+ : "LINEAR_API_KEY";
122
+ const envName = await input({
123
+ message: "Env variable name to store this key under:",
124
+ default: defaultName,
125
+ validate: (v) => /^[A-Z_][A-Z0-9_]*$/.test(v) ||
126
+ "Env var names must be uppercase letters, digits, underscore",
127
+ });
128
+ return {
129
+ apiKey,
130
+ envName,
131
+ fromSystemEnv: false,
132
+ organizationName: newCandidate.orgName,
133
+ };
18
134
  }
19
135
  export async function selectDevTeam(teams, options) {
20
136
  let devTeam = teams[0];
@@ -19,5 +19,6 @@ export interface InitPaths {
19
19
  localPath: string;
20
20
  cyclePath: string;
21
21
  outputPath: string;
22
+ envPath: string;
22
23
  }
23
24
  export type { TaskSourceType };
@@ -6,6 +6,7 @@ export declare function getPaths(): {
6
6
  cyclePath: string;
7
7
  localPath: string;
8
8
  outputPath: string;
9
+ envPath: string;
9
10
  };
10
11
  export interface TeamConfig {
11
12
  id: string;
@@ -126,6 +127,7 @@ export interface LocalConfig {
126
127
  exclude_labels?: string[];
127
128
  labels?: string[];
128
129
  status_source?: "remote" | "local";
130
+ linear_api_key_env?: string;
129
131
  teams?: string[];
130
132
  qa_pm_team?: string;
131
133
  }
@@ -22,6 +22,7 @@ const CONFIG_PATH = path.join(BASE_DIR, "config.toon");
22
22
  const CYCLE_PATH = path.join(BASE_DIR, "cycle.toon");
23
23
  const LOCAL_PATH = path.join(BASE_DIR, "local.toon");
24
24
  const OUTPUT_PATH = path.join(BASE_DIR, "output");
25
+ const ENV_PATH = path.join(BASE_DIR, ".env");
25
26
  export function getPaths() {
26
27
  return {
27
28
  baseDir: BASE_DIR,
@@ -29,6 +30,7 @@ export function getPaths() {
29
30
  cyclePath: CYCLE_PATH,
30
31
  localPath: LOCAL_PATH,
31
32
  outputPath: OUTPUT_PATH,
33
+ envPath: ENV_PATH,
32
34
  };
33
35
  }
34
36
  // Linear priority value to name mapping (fixed by Linear API)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "team-toon-tack",
3
- "version": "3.5.0",
3
+ "version": "3.6.0",
4
4
  "description": "Linear & Trello task sync & management CLI with TOON format",
5
5
  "type": "module",
6
6
  "bin": {