team-toon-tack 3.10.1 → 3.10.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.
@@ -13,7 +13,7 @@
13
13
  "name": "team-toon-tack",
14
14
  "source": "./",
15
15
  "description": "Linear/Trello task sync & management CLI with commands and skills",
16
- "version": "3.10.1"
16
+ "version": "3.10.2"
17
17
  }
18
18
  ]
19
19
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "team-toon-tack",
3
3
  "description": "Linear/Trello task sync & management CLI for Claude Code - saves tokens vs MCP",
4
- "version": "3.10.1",
4
+ "version": "3.10.2",
5
5
  "author": {
6
6
  "name": "wayne930242",
7
7
  "email": "wayne930242@gmail.com"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "team-toon-tack",
3
- "version": "3.10.1",
3
+ "version": "3.10.2",
4
4
  "description": "透過 ttt CLI 同步與管理 Linear、Trello 任務。",
5
5
  "author": {
6
6
  "name": "wayne930242"
package/dist/bin/cli.js CHANGED
@@ -3,6 +3,7 @@ import { readFileSync } from "node:fs";
3
3
  import { dirname, join, resolve } from "node:path";
4
4
  import { fileURLToPath, pathToFileURL } from "node:url";
5
5
  import { loadDotEnv, resolveLinearApiKey } from "../scripts/lib/env.js";
6
+ import { findAncestorWithTtt } from "../scripts/utils.js";
6
7
  const __dirname = dirname(fileURLToPath(import.meta.url));
7
8
  // When running from dist/bin/cli.js, we need to go up two levels to find package.json
8
9
  const pkg = JSON.parse(readFileSync(join(__dirname, "..", "..", "package.json"), "utf-8"));
@@ -51,7 +52,8 @@ COMMANDS:
51
52
  version Show version
52
53
 
53
54
  GLOBAL OPTIONS:
54
- --dir <path> Config directory (default: .ttt)
55
+ --dir <path> Config directory (default: nearest ancestor .ttt,
56
+ searched upward from cwd, stopping at $HOME)
55
57
  Can also set via TOON_DIR environment variable
56
58
  -d <path> Shortcut for --dir; ignored for create/edit/comment
57
59
  (where -d means --description)
@@ -88,19 +90,39 @@ const SHORT_D_RESERVED_FOR_SUBCOMMAND = new Set([
88
90
  "comment",
89
91
  ]);
90
92
  function parseGlobalArgs(command, args) {
91
- let dir = process.env.TOON_DIR || resolve(process.cwd(), ".ttt");
93
+ let explicitDir;
92
94
  const commandArgs = [];
93
95
  const allowShortD = !SHORT_D_RESERVED_FOR_SUBCOMMAND.has(command);
94
96
  for (let i = 0; i < args.length; i++) {
95
97
  const arg = args[i];
96
98
  if (arg === "--dir" || (allowShortD && arg === "-d")) {
97
- dir = resolve(args[++i] || ".");
99
+ explicitDir = resolve(args[++i] || ".");
98
100
  }
99
101
  else {
100
102
  commandArgs.push(arg);
101
103
  }
102
104
  }
103
- return { dir, commandArgs };
105
+ // --dir wins over everything, then TOON_DIR/LINEAR_TOON_DIR, then an
106
+ // upward search from cwd for the nearest ancestor holding `.ttt`.
107
+ if (explicitDir)
108
+ return { dir: explicitDir, commandArgs };
109
+ if (process.env.TOON_DIR) {
110
+ return { dir: resolve(process.env.TOON_DIR), commandArgs };
111
+ }
112
+ if (process.env.LINEAR_TOON_DIR) {
113
+ return { dir: resolve(process.env.LINEAR_TOON_DIR), commandArgs };
114
+ }
115
+ // `init` always targets cwd/.ttt: reusing a discovered ancestor would let
116
+ // it silently rewrite a shared monorepo-root config from inside a
117
+ // worktree instead of creating (or reporting) one where the user stands.
118
+ if (command === "init") {
119
+ return { dir: resolve(process.cwd(), ".ttt"), commandArgs };
120
+ }
121
+ const found = findAncestorWithTtt(process.cwd());
122
+ return {
123
+ dir: found.dir !== null ? join(found.dir, ".ttt") : null,
124
+ commandArgs,
125
+ };
104
126
  }
105
127
  async function main() {
106
128
  const args = process.argv.slice(2);
@@ -118,17 +140,23 @@ async function main() {
118
140
  const command = args[0];
119
141
  const restArgs = args.slice(1);
120
142
  const { dir, commandArgs } = parseGlobalArgs(command, restArgs);
121
- // Set TOON_DIR for scripts to use
122
- process.env.TOON_DIR = dir;
143
+ // Set TOON_DIR for scripts to use. When no explicit override applies and
144
+ // the upward search found nothing, leave it unset so the invoked script's
145
+ // own resolution reports the exact range it searched.
146
+ if (dir !== null) {
147
+ process.env.TOON_DIR = dir;
148
+ }
123
149
  // Load .ttt/.env (if present) and resolve configured Linear API key env
124
150
  // var into LINEAR_API_KEY so downstream code is workspace-aware.
125
151
  // Skip the resolver for `init` so the workspace picker sees the raw
126
152
  // env — otherwise we'd mirror the previously-saved key over LINEAR_API_KEY
127
153
  // and the user's shell-level key would appear to point to the saved
128
154
  // workspace.
129
- await loadDotEnv(join(dir, ".env"));
130
- if (command !== "init") {
131
- await resolveLinearApiKey(join(dir, "local.toon"));
155
+ if (dir !== null) {
156
+ await loadDotEnv(join(dir, ".env"));
157
+ if (command !== "init") {
158
+ await resolveLinearApiKey(join(dir, "local.toon"));
159
+ }
132
160
  }
133
161
  if (!COMMANDS.includes(command)) {
134
162
  console.error(`Unknown command: ${command}`);
@@ -14,7 +14,9 @@ async function init() {
14
14
  process.exit(0);
15
15
  }
16
16
  const options = parseArgs(args);
17
- const paths = getPaths();
17
+ // `init` always targets cwd/.ttt, never a discovered ancestor - see
18
+ // getPaths' doc comment.
19
+ const paths = getPaths({ search: false });
18
20
  // Convert paths to InitPaths format
19
21
  const initPaths = {
20
22
  baseDir: paths.baseDir,
@@ -1,17 +1,43 @@
1
1
  import { LinearClient } from "@linear/sdk";
2
2
  import type { SourceIssue } from "./lib/adapters/types.js";
3
+ export interface AncestorSearch {
4
+ /** Directory the search started from (the process cwd). */
5
+ from: string;
6
+ /** Last directory checked before the search gave up ($HOME or the filesystem root). */
7
+ to: string;
8
+ }
9
+ /**
10
+ * Walk up from `startDir` looking for a `.ttt` directory, stopping at $HOME
11
+ * or the filesystem root, whichever comes first, so the search never escapes
12
+ * the user's home tree. Returns the directory that holds `.ttt` (not the
13
+ * `.ttt` path itself), or `null` with the searched range when none is found.
14
+ */
15
+ export declare function findAncestorWithTtt(startDir: string): {
16
+ dir: string;
17
+ } | {
18
+ dir: null;
19
+ search: AncestorSearch;
20
+ };
3
21
  /**
4
22
  * Resolved on every call, so a TOON_DIR set after this module loads still
5
23
  * applies. Caching it at module load made the base directory depend on import
6
24
  * order.
25
+ *
26
+ * Pass `{ search: false }` to opt out of the upward ancestor search (used by
27
+ * `init`, which must always target cwd/.ttt regardless of what a parent
28
+ * directory holds).
7
29
  */
8
- export declare function getPaths(): {
30
+ export declare function getPaths(opts?: {
31
+ search?: boolean;
32
+ }): {
9
33
  baseDir: string;
10
34
  configPath: string;
11
35
  cyclePath: string;
12
36
  localPath: string;
13
37
  outputPath: string;
14
38
  envPath: string;
39
+ /** Set only when no ancestor `.ttt` was found, for error messages. */
40
+ search: AncestorSearch | null;
15
41
  };
16
42
  export interface TeamConfig {
17
43
  id: string;
@@ -1,29 +1,73 @@
1
+ import { existsSync } from "node:fs";
1
2
  import fs from "node:fs/promises";
3
+ import os from "node:os";
2
4
  import path from "node:path";
3
5
  import { LinearClient } from "@linear/sdk";
4
6
  // decode uses { strict: false } because encode() produces inline arrays
5
7
  // that strict mode rejects (RangeError: Expected 0 inline array items).
6
8
  import { decode, encode } from "@toon-format/toon";
9
+ /**
10
+ * Walk up from `startDir` looking for a `.ttt` directory, stopping at $HOME
11
+ * or the filesystem root, whichever comes first, so the search never escapes
12
+ * the user's home tree. Returns the directory that holds `.ttt` (not the
13
+ * `.ttt` path itself), or `null` with the searched range when none is found.
14
+ */
15
+ export function findAncestorWithTtt(startDir) {
16
+ const home = process.env.HOME || os.homedir();
17
+ let dir = startDir;
18
+ for (;;) {
19
+ if (existsSync(path.join(dir, ".ttt"))) {
20
+ return { dir };
21
+ }
22
+ if (dir === home)
23
+ break;
24
+ const parent = path.dirname(dir);
25
+ if (parent === dir)
26
+ break; // filesystem root
27
+ dir = parent;
28
+ }
29
+ return { dir: null, search: { from: startDir, to: dir } };
30
+ }
7
31
  // Resolve base directory - supports multiple configuration methods
8
- function getBaseDir() {
9
- // 1. Check for TOON_DIR environment variable (set by CLI or user)
32
+ function resolveBaseDir(searchAncestors) {
33
+ // 1. Check for TOON_DIR environment variable (set by CLI or user) - wins
34
+ // over the upward search, same as an explicit --dir.
10
35
  if (process.env.TOON_DIR) {
11
- return path.resolve(process.env.TOON_DIR);
36
+ return { baseDir: path.resolve(process.env.TOON_DIR), search: null };
12
37
  }
13
38
  // 2. Check for legacy LINEAR_TOON_DIR environment variable
14
39
  if (process.env.LINEAR_TOON_DIR) {
15
- return path.resolve(process.env.LINEAR_TOON_DIR);
40
+ return { baseDir: path.resolve(process.env.LINEAR_TOON_DIR), search: null };
16
41
  }
17
- // 3. Default: .ttt directory in current working directory
18
- return path.join(process.cwd(), ".ttt");
42
+ const cwd = process.cwd();
43
+ // 3. `init` (and anything else that opts out) always targets cwd/.ttt:
44
+ // reusing a discovered ancestor would let it silently rewrite a shared
45
+ // monorepo-root config from inside a worktree.
46
+ if (!searchAncestors) {
47
+ return { baseDir: path.join(cwd, ".ttt"), search: null };
48
+ }
49
+ // 4. Walk up from cwd to the nearest ancestor holding `.ttt` (monorepo
50
+ // root, typically), so commands work from nested submodules/worktrees.
51
+ const found = findAncestorWithTtt(cwd);
52
+ if (found.dir !== null) {
53
+ return { baseDir: path.join(found.dir, ".ttt"), search: null };
54
+ }
55
+ // 5. Nothing found anywhere up the chain - fall back to cwd/.ttt so the
56
+ // caller's error message can report the exact directory it tried, plus
57
+ // the range it searched.
58
+ return { baseDir: path.join(cwd, ".ttt"), search: found.search };
19
59
  }
20
60
  /**
21
61
  * Resolved on every call, so a TOON_DIR set after this module loads still
22
62
  * applies. Caching it at module load made the base directory depend on import
23
63
  * order.
64
+ *
65
+ * Pass `{ search: false }` to opt out of the upward ancestor search (used by
66
+ * `init`, which must always target cwd/.ttt regardless of what a parent
67
+ * directory holds).
24
68
  */
25
- export function getPaths() {
26
- const baseDir = getBaseDir();
69
+ export function getPaths(opts = {}) {
70
+ const { baseDir, search } = resolveBaseDir(opts.search ?? true);
27
71
  return {
28
72
  baseDir,
29
73
  configPath: path.join(baseDir, "config.toon"),
@@ -31,6 +75,8 @@ export function getPaths() {
31
75
  localPath: path.join(baseDir, "local.toon"),
32
76
  outputPath: path.join(baseDir, "output"),
33
77
  envPath: path.join(baseDir, ".env"),
78
+ /** Set only when no ancestor `.ttt` was found, for error messages. */
79
+ search,
34
80
  };
35
81
  }
36
82
  // Linear priority value to name mapping (fixed by Linear API)
@@ -63,26 +109,33 @@ export async function fileExists(filePath) {
63
109
  return false;
64
110
  }
65
111
  }
112
+ function reportSearchRange(search) {
113
+ if (search) {
114
+ console.error(`No .ttt directory found from ${search.from} up to ${search.to}.`);
115
+ }
116
+ }
66
117
  export async function loadConfig() {
67
- const { configPath } = getPaths();
118
+ const { configPath, search } = getPaths();
68
119
  try {
69
120
  const fileContent = await fs.readFile(configPath, "utf-8");
70
121
  return decode(fileContent, { strict: false });
71
122
  }
72
123
  catch (error) {
73
124
  console.error(`Error loading config from ${configPath}:`, error);
125
+ reportSearchRange(search);
74
126
  console.error("Run `bun run init` to create configuration files.");
75
127
  process.exit(1);
76
128
  }
77
129
  }
78
130
  export async function loadLocalConfig() {
79
- const { localPath } = getPaths();
131
+ const { localPath, search } = getPaths();
80
132
  try {
81
133
  const fileContent = await fs.readFile(localPath, "utf-8");
82
134
  return decode(fileContent, { strict: false });
83
135
  }
84
136
  catch {
85
137
  console.error(`Error: ${localPath} not found.`);
138
+ reportSearchRange(search);
86
139
  console.error("Run `bun run init` to create local configuration.");
87
140
  process.exit(1);
88
141
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "team-toon-tack",
3
- "version": "3.10.1",
3
+ "version": "3.10.2",
4
4
  "description": "Linear & Trello task sync & management CLI with TOON format",
5
5
  "type": "module",
6
6
  "bin": {