@gh-symphony/cli 0.0.1

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.
@@ -0,0 +1,300 @@
1
+ const DEFAULT_API_URL = "https://api.github.com/graphql";
2
+ const REST_API_URL = "https://api.github.com";
3
+ export class GitHubApiError extends Error {
4
+ status;
5
+ constructor(message, status) {
6
+ super(message);
7
+ this.status = status;
8
+ this.name = "GitHubApiError";
9
+ }
10
+ }
11
+ export function createClient(token, options) {
12
+ return {
13
+ token,
14
+ apiUrl: options?.apiUrl ?? DEFAULT_API_URL,
15
+ fetchImpl: options?.fetchImpl ?? fetch,
16
+ };
17
+ }
18
+ // ── 2.1: Token validation & scope check ──────────────────────────────────────
19
+ export async function validateToken(client) {
20
+ // Use REST to get X-OAuth-Scopes header (GraphQL doesn't expose scopes)
21
+ const restUrl = client.apiUrl.replace("/graphql", "");
22
+ const baseUrl = restUrl === client.apiUrl ? REST_API_URL : restUrl;
23
+ const response = await client.fetchImpl(`${baseUrl}/user`, {
24
+ headers: {
25
+ authorization: `Bearer ${client.token}`,
26
+ accept: "application/vnd.github+json",
27
+ },
28
+ });
29
+ if (!response.ok) {
30
+ if (response.status === 401) {
31
+ throw new GitHubApiError("Invalid token: authentication failed.", 401);
32
+ }
33
+ throw new GitHubApiError(`GitHub API error: ${response.status} ${response.statusText}`, response.status);
34
+ }
35
+ const scopes = response.headers
36
+ .get("x-oauth-scopes")
37
+ ?.split(",")
38
+ .map((s) => s.trim())
39
+ .filter(Boolean) ?? [];
40
+ const user = (await response.json());
41
+ return {
42
+ login: user.login,
43
+ name: user.name,
44
+ scopes,
45
+ };
46
+ }
47
+ export function checkRequiredScopes(scopes) {
48
+ const required = ["repo", "read:org", "project"];
49
+ const normalizedScopes = scopes.map((s) => s.toLowerCase());
50
+ const missing = required.filter((r) => !normalizedScopes.includes(r));
51
+ return { valid: missing.length === 0, missing };
52
+ }
53
+ // ── 2.2: Projects v2 list ────────────────────────────────────────────────────
54
+ export async function listUserProjects(client) {
55
+ const data = await graphql(client, VIEWER_PROJECTS_QUERY);
56
+ const projects = [];
57
+ for (const node of data.viewer.projectsV2?.nodes ?? []) {
58
+ if (!node)
59
+ continue;
60
+ projects.push(normalizeProjectSummary(node, {
61
+ login: data.viewer.login,
62
+ type: "User",
63
+ }));
64
+ }
65
+ for (const orgNode of data.viewer.organizations?.nodes ?? []) {
66
+ if (!orgNode)
67
+ continue;
68
+ for (const projNode of orgNode.projectsV2?.nodes ?? []) {
69
+ if (!projNode)
70
+ continue;
71
+ projects.push(normalizeProjectSummary(projNode, {
72
+ login: orgNode.login,
73
+ type: "Organization",
74
+ }));
75
+ }
76
+ }
77
+ return projects;
78
+ }
79
+ function normalizeProjectSummary(node, owner) {
80
+ return {
81
+ id: node.id,
82
+ title: node.title,
83
+ shortDescription: node.shortDescription ?? "",
84
+ url: node.url,
85
+ openItemCount: node.items?.totalCount ?? 0,
86
+ owner,
87
+ };
88
+ }
89
+ // ── 2.3: Project detail (status fields + linked repos) ───────────────────────
90
+ export async function getProjectDetail(client, projectId) {
91
+ const data = await graphql(client, PROJECT_DETAIL_QUERY, { projectId });
92
+ const project = data.node;
93
+ if (!project || project.__typename !== "ProjectV2") {
94
+ throw new GitHubApiError(`Project not found: ${projectId}`);
95
+ }
96
+ const statusFields = [];
97
+ for (const field of project.fields?.nodes ?? []) {
98
+ if (!field || field.__typename !== "ProjectV2SingleSelectField")
99
+ continue;
100
+ statusFields.push({
101
+ id: field.id,
102
+ name: field.name,
103
+ options: (field.options ?? []).map((opt) => ({
104
+ id: opt.id,
105
+ name: opt.name,
106
+ description: opt.description ?? null,
107
+ color: opt.color ?? null,
108
+ })),
109
+ });
110
+ }
111
+ const repoMap = new Map();
112
+ let cursor = null;
113
+ let hasMore = true;
114
+ // Use initial page from the detail query
115
+ for (const item of project.items?.nodes ?? []) {
116
+ const repo = item?.content?.repository;
117
+ if (!repo)
118
+ continue;
119
+ const key = `${repo.owner.login}/${repo.name}`;
120
+ if (!repoMap.has(key)) {
121
+ repoMap.set(key, {
122
+ owner: repo.owner.login,
123
+ name: repo.name,
124
+ url: repo.url,
125
+ cloneUrl: repo.url.endsWith(".git") ? repo.url : `${repo.url}.git`,
126
+ });
127
+ }
128
+ }
129
+ hasMore = project.items?.pageInfo?.hasNextPage ?? false;
130
+ cursor = project.items?.pageInfo?.endCursor ?? null;
131
+ // Paginate remaining items for linked repos
132
+ while (hasMore && cursor) {
133
+ const pageData = await graphql(client, PROJECT_ITEMS_PAGE_QUERY, { projectId, cursor });
134
+ const items = pageData.node?.items;
135
+ if (!items)
136
+ break;
137
+ for (const item of items.nodes ?? []) {
138
+ const repo = item?.content?.repository;
139
+ if (!repo)
140
+ continue;
141
+ const key = `${repo.owner.login}/${repo.name}`;
142
+ if (!repoMap.has(key)) {
143
+ repoMap.set(key, {
144
+ owner: repo.owner.login,
145
+ name: repo.name,
146
+ url: repo.url,
147
+ cloneUrl: repo.url.endsWith(".git") ? repo.url : `${repo.url}.git`,
148
+ });
149
+ }
150
+ }
151
+ hasMore = items.pageInfo?.hasNextPage ?? false;
152
+ cursor = items.pageInfo?.endCursor ?? null;
153
+ }
154
+ return {
155
+ id: project.id,
156
+ title: project.title,
157
+ url: project.url,
158
+ statusFields,
159
+ linkedRepositories: [...repoMap.values()],
160
+ };
161
+ }
162
+ // ── GraphQL helpers ──────────────────────────────────────────────────────────
163
+ async function graphql(client, query, variables) {
164
+ const response = await client.fetchImpl(client.apiUrl, {
165
+ method: "POST",
166
+ headers: {
167
+ "content-type": "application/json",
168
+ authorization: `Bearer ${client.token}`,
169
+ },
170
+ body: JSON.stringify({ query, variables }),
171
+ });
172
+ if (!response.ok) {
173
+ const text = await response.text().catch(() => "");
174
+ throw new GitHubApiError(`GitHub GraphQL request failed: ${response.status} ${response.statusText}. ${text}`, response.status);
175
+ }
176
+ const payload = (await response.json());
177
+ if (payload.errors?.length) {
178
+ throw new GitHubApiError(`GraphQL errors: ${payload.errors.map((e) => e.message).join("; ")}`);
179
+ }
180
+ if (!payload.data) {
181
+ throw new GitHubApiError("GraphQL response missing data.");
182
+ }
183
+ return payload.data;
184
+ }
185
+ // ── GraphQL queries ──────────────────────────────────────────────────────────
186
+ const VIEWER_PROJECTS_QUERY = `
187
+ query ViewerProjects {
188
+ viewer {
189
+ login
190
+ projectsV2(first: 50) {
191
+ nodes {
192
+ id
193
+ title
194
+ shortDescription
195
+ url
196
+ items { totalCount }
197
+ }
198
+ }
199
+ organizations(first: 20) {
200
+ nodes {
201
+ login
202
+ projectsV2(first: 50) {
203
+ nodes {
204
+ id
205
+ title
206
+ shortDescription
207
+ url
208
+ items { totalCount }
209
+ }
210
+ }
211
+ }
212
+ }
213
+ }
214
+ }
215
+ `;
216
+ const PROJECT_DETAIL_QUERY = `
217
+ query ProjectDetail($projectId: ID!) {
218
+ node(id: $projectId) {
219
+ __typename
220
+ ... on ProjectV2 {
221
+ id
222
+ title
223
+ url
224
+ fields(first: 50) {
225
+ nodes {
226
+ __typename
227
+ ... on ProjectV2SingleSelectField {
228
+ id
229
+ name
230
+ options {
231
+ id
232
+ name
233
+ description
234
+ color
235
+ }
236
+ }
237
+ }
238
+ }
239
+ items(first: 100) {
240
+ nodes {
241
+ content {
242
+ __typename
243
+ ... on Issue {
244
+ repository {
245
+ name
246
+ url
247
+ owner { login }
248
+ }
249
+ }
250
+ ... on PullRequest {
251
+ repository {
252
+ name
253
+ url
254
+ owner { login }
255
+ }
256
+ }
257
+ }
258
+ }
259
+ pageInfo {
260
+ endCursor
261
+ hasNextPage
262
+ }
263
+ }
264
+ }
265
+ }
266
+ }
267
+ `;
268
+ const PROJECT_ITEMS_PAGE_QUERY = `
269
+ query ProjectItemsPage($projectId: ID!, $cursor: String) {
270
+ node(id: $projectId) {
271
+ ... on ProjectV2 {
272
+ items(first: 100, after: $cursor) {
273
+ nodes {
274
+ content {
275
+ __typename
276
+ ... on Issue {
277
+ repository {
278
+ name
279
+ url
280
+ owner { login }
281
+ }
282
+ }
283
+ ... on PullRequest {
284
+ repository {
285
+ name
286
+ url
287
+ owner { login }
288
+ }
289
+ }
290
+ }
291
+ }
292
+ pageInfo {
293
+ endCursor
294
+ hasNextPage
295
+ }
296
+ }
297
+ }
298
+ }
299
+ }
300
+ `;
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env node
2
+ export type GlobalOptions = {
3
+ configDir: string;
4
+ verbose: boolean;
5
+ json: boolean;
6
+ noColor: boolean;
7
+ };
8
+ export declare function parseGlobalOptions(argv: string[]): {
9
+ options: GlobalOptions;
10
+ command: string;
11
+ args: string[];
12
+ };
13
+ export type CommandHandler = (args: string[], options: GlobalOptions) => Promise<void>;
14
+ export declare function runCli(argv: string[]): Promise<void>;
package/dist/index.js ADDED
@@ -0,0 +1,88 @@
1
+ #!/usr/bin/env node
2
+ import { pathToFileURL } from "node:url";
3
+ import { resolveConfigDir } from "./config.js";
4
+ export function parseGlobalOptions(argv) {
5
+ const globalFlags = {
6
+ configDir: resolveConfigDir(),
7
+ verbose: false,
8
+ json: false,
9
+ noColor: false,
10
+ };
11
+ const remaining = [];
12
+ let i = 0;
13
+ while (i < argv.length) {
14
+ const arg = argv[i];
15
+ if (arg === "--config" || arg === "--config-dir") {
16
+ globalFlags.configDir = resolveConfigDir(argv[i + 1]);
17
+ i += 2;
18
+ continue;
19
+ }
20
+ if (arg === "--verbose" || arg === "-v") {
21
+ globalFlags.verbose = true;
22
+ i += 1;
23
+ continue;
24
+ }
25
+ if (arg === "--json") {
26
+ globalFlags.json = true;
27
+ i += 1;
28
+ continue;
29
+ }
30
+ if (arg === "--no-color") {
31
+ globalFlags.noColor = true;
32
+ i += 1;
33
+ continue;
34
+ }
35
+ remaining.push(arg);
36
+ i += 1;
37
+ }
38
+ const [command = "help", ...args] = remaining;
39
+ return { options: globalFlags, command, args };
40
+ }
41
+ const COMMANDS = {
42
+ init: () => import("./commands/init.js"),
43
+ start: () => import("./commands/start.js"),
44
+ stop: () => import("./commands/stop.js"),
45
+ status: () => import("./commands/status.js"),
46
+ run: () => import("./commands/run.js"),
47
+ recover: () => import("./commands/recover.js"),
48
+ logs: () => import("./commands/logs.js"),
49
+ project: () => import("./commands/project.js"),
50
+ repo: () => import("./commands/repo.js"),
51
+ config: () => import("./commands/config-cmd.js"),
52
+ help: () => import("./commands/help.js"),
53
+ version: () => import("./commands/version.js"),
54
+ };
55
+ export async function runCli(argv) {
56
+ const { options, command, args } = parseGlobalOptions(argv);
57
+ if (options.noColor) {
58
+ process.env.NO_COLOR = "1";
59
+ }
60
+ if (command === "--help" || command === "-h") {
61
+ const helpModule = await COMMANDS.help();
62
+ await helpModule.default(args, options);
63
+ return;
64
+ }
65
+ if (command === "--version" || command === "-V") {
66
+ const versionModule = await COMMANDS.version();
67
+ await versionModule.default(args, options);
68
+ return;
69
+ }
70
+ const loader = COMMANDS[command];
71
+ if (!loader) {
72
+ process.stderr.write(`Unknown command: ${command}\nRun 'gh-symphony help' for usage.\n`);
73
+ process.exitCode = 2;
74
+ return;
75
+ }
76
+ const module = await loader();
77
+ await module.default(args, options);
78
+ }
79
+ async function main() {
80
+ await runCli(process.argv.slice(2));
81
+ }
82
+ if (process.argv[1] &&
83
+ import.meta.url === pathToFileURL(process.argv[1]).href) {
84
+ main().catch((error) => {
85
+ process.stderr.write(`${error instanceof Error ? error.message : "Unknown error"}\n`);
86
+ process.exitCode = 1;
87
+ });
88
+ }
@@ -0,0 +1,33 @@
1
+ import type { WorkflowLifecycleConfig } from "@hojinzs/gh-symphony-core";
2
+ import type { ColumnRole, HumanReviewMode } from "../config.js";
3
+ export type ColumnMapping = {
4
+ columnName: string;
5
+ role: ColumnRole | null;
6
+ confidence: "high" | "low";
7
+ };
8
+ export declare function inferColumnRole(columnName: string): ColumnMapping;
9
+ export declare function inferAllColumnRoles(columnNames: string[]): ColumnMapping[];
10
+ export type PhaseMapping = {
11
+ planningStates: string[];
12
+ humanReviewStates: string[];
13
+ implementationStates: string[];
14
+ awaitingMergeStates: string[];
15
+ completedStates: string[];
16
+ };
17
+ /**
18
+ * Map column roles to Symphony execution phases based on the human-review mode.
19
+ *
20
+ * Modes:
21
+ * - plan-and-pr: Human reviews both plans and PRs (full review pipeline)
22
+ * - plan-only: Human reviews plans, PRs auto-merge
23
+ * - pr-only: No plan review, human reviews PRs
24
+ * - none: No human review at all (full auto)
25
+ */
26
+ export declare function buildPhaseMapping(roles: Record<string, ColumnRole>, mode: HumanReviewMode): PhaseMapping;
27
+ export declare function toWorkflowLifecycleConfig(stateFieldName: string, roles: Record<string, ColumnRole>, mode: HumanReviewMode): WorkflowLifecycleConfig;
28
+ export type MappingValidationResult = {
29
+ valid: boolean;
30
+ errors: string[];
31
+ warnings: string[];
32
+ };
33
+ export declare function validateMapping(roles: Record<string, ColumnRole>): MappingValidationResult;
@@ -0,0 +1,159 @@
1
+ // ── 3.1: Smart defaults pattern matching ─────────────────────────────────────
2
+ const ROLE_PATTERNS = [
3
+ {
4
+ role: "trigger",
5
+ pattern: /^(todo|to.do|to-do|ready|queued|open|new|triage)$/i,
6
+ },
7
+ {
8
+ role: "working",
9
+ pattern: /^(in.progress|working|active|doing|in.development|developing|wip)$/i,
10
+ },
11
+ {
12
+ role: "human-review",
13
+ pattern: /^(review|in.review|pr.review|needs.review|plan.review|awaiting.review|code.review)$/i,
14
+ },
15
+ {
16
+ role: "done",
17
+ pattern: /^(done|completed?|closed|merged|shipped|resolved|finished)$/i,
18
+ },
19
+ {
20
+ role: "ignored",
21
+ pattern: /^(icebox|someday|later|blocked|on.hold|paused|won.?t.do|cancelled|deferred|draft|backlog)$/i,
22
+ },
23
+ ];
24
+ export function inferColumnRole(columnName) {
25
+ const normalized = columnName.trim();
26
+ for (const { role, pattern } of ROLE_PATTERNS) {
27
+ if (pattern.test(normalized)) {
28
+ return { columnName: normalized, role, confidence: "high" };
29
+ }
30
+ }
31
+ return { columnName: normalized, role: null, confidence: "low" };
32
+ }
33
+ export function inferAllColumnRoles(columnNames) {
34
+ return columnNames.map(inferColumnRole);
35
+ }
36
+ /**
37
+ * Map column roles to Symphony execution phases based on the human-review mode.
38
+ *
39
+ * Modes:
40
+ * - plan-and-pr: Human reviews both plans and PRs (full review pipeline)
41
+ * - plan-only: Human reviews plans, PRs auto-merge
42
+ * - pr-only: No plan review, human reviews PRs
43
+ * - none: No human review at all (full auto)
44
+ */
45
+ export function buildPhaseMapping(roles, mode) {
46
+ const planningStates = [];
47
+ const humanReviewStates = [];
48
+ const implementationStates = [];
49
+ const awaitingMergeStates = [];
50
+ const completedStates = [];
51
+ for (const [columnName, role] of Object.entries(roles)) {
52
+ switch (role) {
53
+ case "trigger":
54
+ planningStates.push(columnName);
55
+ break;
56
+ case "working":
57
+ implementationStates.push(columnName);
58
+ break;
59
+ case "human-review":
60
+ switch (mode) {
61
+ case "plan-and-pr":
62
+ humanReviewStates.push(columnName);
63
+ break;
64
+ case "plan-only":
65
+ humanReviewStates.push(columnName);
66
+ break;
67
+ case "pr-only":
68
+ awaitingMergeStates.push(columnName);
69
+ break;
70
+ case "none":
71
+ // In "none" mode, review columns are treated as implementation
72
+ implementationStates.push(columnName);
73
+ break;
74
+ }
75
+ break;
76
+ case "done":
77
+ completedStates.push(columnName);
78
+ break;
79
+ case "ignored":
80
+ // Ignored columns don't map to any phase
81
+ break;
82
+ }
83
+ }
84
+ return {
85
+ planningStates,
86
+ humanReviewStates,
87
+ implementationStates,
88
+ awaitingMergeStates,
89
+ completedStates,
90
+ };
91
+ }
92
+ // ── 3.3: Mapping → WorkflowLifecycleConfig conversion ───────────────────────
93
+ export function toWorkflowLifecycleConfig(stateFieldName, roles, mode) {
94
+ const phases = buildPhaseMapping(roles, mode);
95
+ // Transition targets: where issues move when a phase completes
96
+ const planningCompleteState = resolveTransitionTarget(phases, "planning", mode);
97
+ const implementationCompleteState = resolveTransitionTarget(phases, "implementation", mode);
98
+ const mergeCompleteState = phases.completedStates[0] ?? phases.awaitingMergeStates[0] ?? "Done";
99
+ return {
100
+ stateFieldName,
101
+ planningStates: phases.planningStates,
102
+ humanReviewStates: phases.humanReviewStates,
103
+ implementationStates: phases.implementationStates,
104
+ awaitingMergeStates: phases.awaitingMergeStates,
105
+ completedStates: phases.completedStates,
106
+ planningCompleteState,
107
+ implementationCompleteState,
108
+ mergeCompleteState,
109
+ };
110
+ }
111
+ function resolveTransitionTarget(phases, fromPhase, mode) {
112
+ if (fromPhase === "planning") {
113
+ // After planning: go to human-review (if exists) or implementation
114
+ if ((mode === "plan-and-pr" || mode === "plan-only") &&
115
+ phases.humanReviewStates.length > 0) {
116
+ return phases.humanReviewStates[0];
117
+ }
118
+ return phases.implementationStates[0] ?? "In Progress";
119
+ }
120
+ // After implementation: go to awaiting-merge (if exists) or completed
121
+ if ((mode === "plan-and-pr" || mode === "pr-only") &&
122
+ phases.awaitingMergeStates.length > 0) {
123
+ return phases.awaitingMergeStates[0];
124
+ }
125
+ return phases.completedStates[0] ?? "Done";
126
+ }
127
+ export function validateMapping(roles) {
128
+ const errors = [];
129
+ const warnings = [];
130
+ const roleEntries = Object.entries(roles);
131
+ const triggerColumns = roleEntries.filter(([, r]) => r === "trigger");
132
+ const workingColumns = roleEntries.filter(([, r]) => r === "working");
133
+ const doneColumns = roleEntries.filter(([, r]) => r === "done");
134
+ const reviewColumns = roleEntries.filter(([, r]) => r === "human-review");
135
+ // Required roles
136
+ if (triggerColumns.length === 0) {
137
+ errors.push("Missing required role: 'trigger' — at least one column must trigger work.");
138
+ }
139
+ if (workingColumns.length === 0) {
140
+ errors.push("Missing required role: 'working' — at least one column must represent active work.");
141
+ }
142
+ if (doneColumns.length === 0) {
143
+ errors.push("Missing required role: 'done' — at least one column must represent completion.");
144
+ }
145
+ // Warnings for unusual setups
146
+ if (triggerColumns.length > 1) {
147
+ warnings.push(`Multiple trigger columns: ${triggerColumns.map(([n]) => n).join(", ")}. ` +
148
+ "All will be treated as planning states.");
149
+ }
150
+ if (doneColumns.length > 1) {
151
+ warnings.push(`Multiple done columns: ${doneColumns.map(([n]) => n).join(", ")}. ` +
152
+ "All will be treated as completed states.");
153
+ }
154
+ if (reviewColumns.length > 2) {
155
+ warnings.push(`${reviewColumns.length} review columns detected. ` +
156
+ "Consider simplifying to one or two review stages.");
157
+ }
158
+ return { valid: errors.length === 0, errors, warnings };
159
+ }
@@ -0,0 +1,5 @@
1
+ import { type CliWorkspaceConfig } from "./config.js";
2
+ export declare function resolveRuntimeRoot(configDir: string): string;
3
+ export declare function resolveWorkspaceConfig(configDir: string, requestedWorkspaceId?: string): Promise<CliWorkspaceConfig | null>;
4
+ export declare function orchestratorWorkspaceConfigPath(runtimeRoot: string, workspaceId: string): string;
5
+ export declare function syncWorkspaceToRuntime(configDir: string, workspaceConfig: CliWorkspaceConfig): Promise<string>;
@@ -0,0 +1,26 @@
1
+ import { mkdir, writeFile } from "node:fs/promises";
2
+ import { dirname, join, resolve } from "node:path";
3
+ import { loadGlobalConfig, loadWorkspaceConfig, } from "./config.js";
4
+ export function resolveRuntimeRoot(configDir) {
5
+ return resolve(configDir);
6
+ }
7
+ export async function resolveWorkspaceConfig(configDir, requestedWorkspaceId) {
8
+ if (requestedWorkspaceId) {
9
+ return loadWorkspaceConfig(configDir, requestedWorkspaceId);
10
+ }
11
+ const global = await loadGlobalConfig(configDir);
12
+ if (!global?.activeWorkspace) {
13
+ return null;
14
+ }
15
+ return loadWorkspaceConfig(configDir, global.activeWorkspace);
16
+ }
17
+ export function orchestratorWorkspaceConfigPath(runtimeRoot, workspaceId) {
18
+ return join(runtimeRoot, "orchestrator", "workspaces", workspaceId, "config.json");
19
+ }
20
+ export async function syncWorkspaceToRuntime(configDir, workspaceConfig) {
21
+ const runtimeRoot = resolveRuntimeRoot(configDir);
22
+ const configPath = orchestratorWorkspaceConfigPath(runtimeRoot, workspaceConfig.workspaceId);
23
+ await mkdir(dirname(configPath), { recursive: true });
24
+ await writeFile(configPath, JSON.stringify(workspaceConfig, null, 2) + "\n");
25
+ return runtimeRoot;
26
+ }
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@gh-symphony/cli",
3
+ "version": "0.0.1",
4
+ "license": "MIT",
5
+ "author": "hojinzs",
6
+ "description": "Interactive CLI for GitHub Symphony orchestration",
7
+ "type": "module",
8
+ "bin": {
9
+ "gh-symphony": "./dist/index.js"
10
+ },
11
+ "main": "./dist/index.js",
12
+ "types": "./dist/index.d.ts",
13
+ "exports": {
14
+ ".": {
15
+ "types": "./dist/index.d.ts",
16
+ "import": "./dist/index.js",
17
+ "default": "./dist/index.js"
18
+ }
19
+ },
20
+ "files": [
21
+ "dist",
22
+ "README.md",
23
+ "LICENSE"
24
+ ],
25
+ "publishConfig": {
26
+ "access": "public"
27
+ },
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "https://github.com/hojinzs/github-symphony.git",
31
+ "directory": "packages/cli"
32
+ },
33
+ "homepage": "https://github.com/hojinzs/github-symphony#readme",
34
+ "bugs": {
35
+ "url": "https://github.com/hojinzs/github-symphony/issues"
36
+ },
37
+ "dependencies": {
38
+ "@clack/prompts": "^0.9.1",
39
+ "@gh-symphony/core": "0.0.1",
40
+ "@gh-symphony/orchestrator": "0.0.1",
41
+ "@gh-symphony/tracker-github": "0.0.1"
42
+ },
43
+ "scripts": {
44
+ "build": "tsc -p tsconfig.json",
45
+ "lint": "eslint src --ext .ts",
46
+ "test": "vitest run --passWithNoTests",
47
+ "typecheck": "tsc -p tsconfig.json --noEmit"
48
+ }
49
+ }