flowcat 1.6.3 → 1.8.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/core/addTask.ts CHANGED
@@ -1,10 +1,8 @@
1
1
  import { z } from "zod";
2
2
 
3
- import { resolveGithubToken } from "./config";
4
3
  import { withWorkspaceLock } from "./lock";
5
4
  import { emitHook } from "./pluginManager";
6
5
  import { buildPrAttachment, fetchGitHubPr, parseGitHubPrUrl } from "./pr";
7
- import type { PrFetched } from "./schemas/pr";
8
6
  import type { Task } from "./schemas/task";
9
7
  import { taskSchema } from "./schemas/task";
10
8
  import { buildTask } from "./taskFactory";
@@ -59,15 +57,7 @@ export const addTask = async (input: AddTaskInput): Promise<AddTaskResult> => {
59
57
  if (parsedInput.url) {
60
58
  const parsed = parseGitHubPrUrl(parsedInput.url);
61
59
  if (parsed) {
62
- const token = await resolveGithubToken(parsedInput.workspaceRoot);
63
- let fetched: PrFetched | null = null;
64
- if (token) {
65
- try {
66
- fetched = await fetchGitHubPr(parsed, token);
67
- } catch {
68
- fetched = null;
69
- }
70
- }
60
+ const fetched = await fetchGitHubPr(parsed);
71
61
  prAttachment = buildPrAttachment(parsed, fetched ?? undefined);
72
62
  prTitle = fetched?.title ?? `PR #${parsed.number} ${parsed.repo.owner}/${parsed.repo.name}`;
73
63
  } else {
package/core/config.ts CHANGED
@@ -34,9 +34,6 @@ export type PluginsConfig = {
34
34
 
35
35
  export type AppConfig = {
36
36
  autoCommit?: boolean;
37
- github?: {
38
- token?: string;
39
- };
40
37
  plugins?: PluginsConfig;
41
38
  };
42
39
 
@@ -90,21 +87,3 @@ export const resolveAutoCommitEnabled = async (workspaceRoot: string): Promise<b
90
87
  const globalConfig = await readConfigFile(resolveGlobalConfigPath());
91
88
  return globalConfig.autoCommit ?? false;
92
89
  };
93
-
94
- export const resolveGithubToken = async (workspaceRoot: string): Promise<string | null> => {
95
- if (process.env.FLOWCAT_GITHUB_TOKEN) {
96
- return process.env.FLOWCAT_GITHUB_TOKEN;
97
- }
98
-
99
- if (process.env.IL_GITHUB_TOKEN) {
100
- return process.env.IL_GITHUB_TOKEN;
101
- }
102
-
103
- const workspaceConfig = await readConfigFile(resolveWorkspaceConfigPath(workspaceRoot));
104
- if (workspaceConfig.github?.token) {
105
- return workspaceConfig.github.token;
106
- }
107
-
108
- const globalConfig = await readConfigFile(resolveGlobalConfigPath());
109
- return globalConfig.github?.token ?? null;
110
- };
package/core/constants.ts CHANGED
@@ -4,5 +4,6 @@ export const LOCK_DIR = ".lock";
4
4
  export const LOCK_FILE = "store.lock";
5
5
  export const TASKS_DIR = "tasks";
6
6
  export const PLUGINS_DIR = "plugins";
7
+ export const ENV_FILE = ".env";
7
8
 
8
9
  export const STATUS_ORDER = ["backlog", "active", "paused", "completed", "cancelled"] as const;
package/core/gitignore.ts CHANGED
@@ -3,7 +3,7 @@ import path from "node:path";
3
3
 
4
4
  import { APP_DIR } from "./constants";
5
5
 
6
- const ignoredEntries = [`${APP_DIR}/.lock/`, `${APP_DIR}/config.json`];
6
+ const ignoredEntries = [`${APP_DIR}/.lock/`];
7
7
 
8
8
  export const ensureWorkspaceIgnored = async (repoRoot: string): Promise<boolean> => {
9
9
  const gitignorePath = path.join(repoRoot, ".gitignore");
package/core/initFlow.ts CHANGED
@@ -101,17 +101,12 @@ export const runInitFlow = async (options: InitFlowOptions): Promise<InitChoice>
101
101
  const currentConfig = await readConfigFile(configPath);
102
102
 
103
103
  const autoCommit = currentConfig.autoCommit ?? false;
104
- const githubToken = currentConfig.github?.token ?? null;
105
104
 
106
105
  await ensureWorkspaceLayout(choice.workspaceRoot);
107
106
 
108
107
  await updateConfigFile(configPath, (current) => ({
109
108
  ...current,
110
109
  autoCommit,
111
- github: {
112
- ...current.github,
113
- ...(githubToken ? { token: githubToken } : {}),
114
- },
115
110
  }));
116
111
 
117
112
  if (choice.kind === "repo" && choice.repoRoot) {
package/core/pr.ts CHANGED
@@ -1,8 +1,11 @@
1
- import { Octokit } from "octokit";
1
+ import { exec } from "node:child_process";
2
+ import { promisify } from "node:util";
2
3
 
3
4
  import type { PrAttachment, PrFetched } from "./schemas/pr";
4
5
  import { nowIso } from "./time";
5
6
 
7
+ const execAsync = promisify(exec);
8
+
6
9
  export type ParsedPr = {
7
10
  url: string;
8
11
  provider: "github";
@@ -54,36 +57,31 @@ export const buildPrAttachment = (parsed: ParsedPr, fetched?: PrFetched): PrAtta
54
57
  };
55
58
  };
56
59
 
57
- export const fetchGitHubPr = async (
58
- parsed: ParsedPr,
59
- token?: string | null,
60
- ): Promise<PrFetched | null> => {
60
+ export const fetchGitHubPr = async (parsed: ParsedPr): Promise<PrFetched | null> => {
61
61
  if (!parsed.repo || !parsed.number) {
62
62
  return null;
63
63
  }
64
64
 
65
- if (!token) {
66
- return null;
67
- }
68
-
69
- const octokit = new Octokit({ auth: token });
70
- const response = await octokit.rest.pulls.get({
71
- owner: parsed.repo.owner,
72
- repo: parsed.repo.name,
73
- pull_number: parsed.number,
74
- });
65
+ try {
66
+ const { stdout } = await execAsync(
67
+ `gh pr view ${parsed.url} --json title,author,state,isDraft,updatedAt`,
68
+ );
69
+ const data = JSON.parse(stdout);
75
70
 
76
- const data = response.data;
77
- const state = data.merged ? "merged" : data.state === "open" ? "open" : "closed";
71
+ // gh returns state as uppercase: "MERGED", "OPEN", "CLOSED"
72
+ const state = data.state.toLowerCase() as PrFetched["state"];
78
73
 
79
- return {
80
- at: nowIso(),
81
- title: data.title,
82
- author: {
83
- login: data.user?.login ?? "unknown",
84
- },
85
- state,
86
- draft: data.draft ?? false,
87
- updated_at: data.updated_at,
88
- };
74
+ return {
75
+ at: nowIso(),
76
+ title: data.title,
77
+ author: {
78
+ login: data.author.login,
79
+ },
80
+ state,
81
+ draft: data.isDraft,
82
+ updated_at: data.updatedAt,
83
+ };
84
+ } catch {
85
+ return null;
86
+ }
89
87
  };
@@ -1,4 +1,3 @@
1
- import { resolveGithubToken } from "./config";
2
1
  import { withWorkspaceLock } from "./lock";
3
2
  import { emitHook } from "./pluginManager";
4
3
  import { buildPrAttachment, fetchGitHubPr, parseGitHubPrUrl } from "./pr";
@@ -12,9 +11,9 @@ import { nowIso } from "./time";
12
11
  import type { TaskAction, TaskStatus } from "./types";
13
12
 
14
13
  export class PrWorkflowError extends Error {
15
- code: "INVALID_PR_URL" | "MISSING_TOKEN";
14
+ code: "INVALID_PR_URL" | "FETCH_FAILED";
16
15
 
17
- constructor(code: "INVALID_PR_URL" | "MISSING_TOKEN", message: string) {
16
+ constructor(code: "INVALID_PR_URL" | "FETCH_FAILED", message: string) {
18
17
  super(message);
19
18
  this.code = code;
20
19
  this.name = "PrWorkflowError";
@@ -69,15 +68,7 @@ export const attachPrToTask = async (input: {
69
68
  throw new PrWorkflowError("INVALID_PR_URL", "Invalid PR URL");
70
69
  }
71
70
 
72
- const token = await resolveGithubToken(input.workspaceRoot);
73
- let fetched: PrFetched | null = null;
74
- if (token) {
75
- try {
76
- fetched = await fetchGitHubPr(parsed, token);
77
- } catch {
78
- fetched = null;
79
- }
80
- }
71
+ const fetched = await fetchGitHubPr(parsed);
81
72
  const stored = await resolveTask(input.workspaceRoot, input.identifier);
82
73
 
83
74
  const updated: Task = {
@@ -126,10 +117,12 @@ export const refreshPrForTask = async (input: {
126
117
  throw new Error("Invalid PR URL");
127
118
  }
128
119
 
129
- const token = await resolveGithubToken(input.workspaceRoot);
130
- const fetched = await fetchGitHubPr(parsed, token);
120
+ const fetched = await fetchGitHubPr(parsed);
131
121
  if (!fetched) {
132
- throw new PrWorkflowError("MISSING_TOKEN", "No GitHub token configured");
122
+ throw new PrWorkflowError(
123
+ "FETCH_FAILED",
124
+ "Failed to fetch PR. Make sure gh CLI is installed and authenticated.",
125
+ );
133
126
  }
134
127
 
135
128
  const baseUpdated: Task = {
package/core/workspace.ts CHANGED
@@ -1,9 +1,9 @@
1
- import { access, mkdir } from "node:fs/promises";
1
+ import { access, mkdir, writeFile } from "node:fs/promises";
2
2
  import { homedir } from "node:os";
3
3
  import path from "node:path";
4
4
 
5
5
  import { configFileExists, resolveGlobalConfigPath, resolveWorkspaceConfigPath } from "./config";
6
- import { APP_DIR, APP_NAME, LOCK_DIR, PLUGINS_DIR, STATUS_ORDER, TASKS_DIR } from "./constants";
6
+ import { APP_DIR, APP_NAME, ENV_FILE, LOCK_DIR, PLUGINS_DIR, STATUS_ORDER, TASKS_DIR } from "./constants";
7
7
  import { writeJsonAtomic } from "./json";
8
8
 
9
9
  export type WorkspaceKind = "explicit" | "global" | "repo";
@@ -106,22 +106,24 @@ export const ensureWorkspaceLayout = async (workspaceRoot: string): Promise<void
106
106
  ),
107
107
  );
108
108
 
109
- // Create plugins/package.json if it doesn't exist
110
- await ensurePluginsPackageJson(workspaceRoot);
109
+ // Create package.json for TypeScript plugin development
110
+ await ensureWorkspacePackageJson(workspaceRoot);
111
+ // Create .gitignore for workspace-local files
112
+ await ensureWorkspaceGitignore(workspaceRoot);
111
113
  };
112
114
 
113
115
  /**
114
- * Ensure plugins directory has a package.json for TypeScript plugin development
116
+ * Ensure workspace has a package.json for TypeScript plugin development
115
117
  */
116
- export const ensurePluginsPackageJson = async (workspaceRoot: string): Promise<void> => {
117
- const packageJsonPath = path.join(workspaceRoot, PLUGINS_DIR, "package.json");
118
+ export const ensureWorkspacePackageJson = async (workspaceRoot: string): Promise<void> => {
119
+ const packageJsonPath = path.join(workspaceRoot, "package.json");
118
120
 
119
121
  if (await exists(packageJsonPath)) {
120
122
  return;
121
123
  }
122
124
 
123
125
  const packageJson = {
124
- name: "flowcat-plugins",
126
+ name: "flowcat-workspace",
125
127
  type: "module",
126
128
  private: true,
127
129
  dependencies: {
@@ -131,3 +133,21 @@ export const ensurePluginsPackageJson = async (workspaceRoot: string): Promise<v
131
133
 
132
134
  await writeJsonAtomic(packageJsonPath, packageJson);
133
135
  };
136
+
137
+ /**
138
+ * Ensure workspace has a .gitignore for local files that shouldn't be committed
139
+ */
140
+ export const ensureWorkspaceGitignore = async (workspaceRoot: string): Promise<void> => {
141
+ const gitignorePath = path.join(workspaceRoot, ".gitignore");
142
+
143
+ if (await exists(gitignorePath)) {
144
+ return;
145
+ }
146
+
147
+ const gitignoreContent = `# Flowcat workspace local files
148
+ ${ENV_FILE}
149
+ ${LOCK_DIR}/
150
+ `;
151
+
152
+ await writeFile(gitignorePath, gitignoreContent, "utf8");
153
+ };