@gh-symphony/cli 0.0.12 → 0.0.14
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/README.md +32 -0
- package/dist/config.d.ts +2 -1
- package/package.json +5 -5
- package/dist/commands/parse-cli-args.d.ts +0 -6
- package/dist/commands/parse-cli-args.js +0 -20
- package/dist/commands/tenant.d.ts +0 -3
- package/dist/commands/tenant.js +0 -348
package/README.md
CHANGED
|
@@ -62,6 +62,38 @@ You can further customize the agent's behavior by editing `WORKFLOW.md` — this
|
|
|
62
62
|
|
|
63
63
|
> Currently supported runtimes: **Codex**, **Claude Code**
|
|
64
64
|
|
|
65
|
+
### Project `.env` Mapping
|
|
66
|
+
|
|
67
|
+
If your hooks or worker runs need staging hosts, database URLs, Playwright base URLs, or other runtime-only values, store them in the project runtime directory instead of hardcoding them in `WORKFLOW.md`.
|
|
68
|
+
|
|
69
|
+
1. Find the project id from `gh-symphony project list`.
|
|
70
|
+
2. Create the runtime env file:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
mkdir -p ~/.gh-symphony/projects/<project-id>
|
|
74
|
+
cat > ~/.gh-symphony/projects/<project-id>/.env <<'EOF'
|
|
75
|
+
STAGING_API_HOST=https://staging.example.com
|
|
76
|
+
DATABASE_URL=postgres://user:pass@staging-db:5432/app
|
|
77
|
+
PLAYWRIGHT_BASE_URL=http://localhost:3000
|
|
78
|
+
EOF
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
3. Reference those variables from `WORKFLOW.md` hooks or repository setup scripts:
|
|
82
|
+
|
|
83
|
+
```yaml
|
|
84
|
+
hooks:
|
|
85
|
+
after_create: 'echo "API_HOST=$STAGING_API_HOST" >> .env.development'
|
|
86
|
+
before_run: 'echo "BASE_URL=$PLAYWRIGHT_BASE_URL" > playwright.env'
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Env precedence during hook execution and worker spawn is:
|
|
90
|
+
|
|
91
|
+
- `project .env` as the base
|
|
92
|
+
- system env as the override layer
|
|
93
|
+
- Symphony context vars such as `SYMPHONY_*` as the highest-priority layer
|
|
94
|
+
|
|
95
|
+
If you use `--config <dir>`, replace `~/.gh-symphony` with that directory.
|
|
96
|
+
|
|
65
97
|
## 3. Set Orchestrator Runner (Project)
|
|
66
98
|
|
|
67
99
|
On the machine where you want the orchestrator to run, register a project:
|
package/dist/config.d.ts
CHANGED
|
@@ -8,9 +8,10 @@ export type CliGlobalConfig = {
|
|
|
8
8
|
activeProject: string | null;
|
|
9
9
|
projects: string[];
|
|
10
10
|
};
|
|
11
|
-
export type CliProjectTrackerSettings = Record<string, string | boolean> & {
|
|
11
|
+
export type CliProjectTrackerSettings = Record<string, string | number | boolean> & {
|
|
12
12
|
projectId?: string;
|
|
13
13
|
assignedOnly?: boolean;
|
|
14
|
+
timeoutMs?: number;
|
|
14
15
|
};
|
|
15
16
|
export type CliProjectConfig = Omit<OrchestratorProjectConfig, "tracker"> & {
|
|
16
17
|
displayName?: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gh-symphony/cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.14",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"author": "hojinzs",
|
|
6
6
|
"description": "Interactive CLI for GitHub Symphony orchestration",
|
|
@@ -37,10 +37,10 @@
|
|
|
37
37
|
"dependencies": {
|
|
38
38
|
"@clack/prompts": "^0.9.1",
|
|
39
39
|
"commander": "^14.0.1",
|
|
40
|
-
"@gh-symphony/core": "0.0.
|
|
41
|
-
"@gh-symphony/tracker-github": "0.0.
|
|
42
|
-
"@gh-symphony/
|
|
43
|
-
"@gh-symphony/
|
|
40
|
+
"@gh-symphony/core": "0.0.14",
|
|
41
|
+
"@gh-symphony/tracker-github": "0.0.14",
|
|
42
|
+
"@gh-symphony/orchestrator": "0.0.14",
|
|
43
|
+
"@gh-symphony/worker": "0.0.14"
|
|
44
44
|
},
|
|
45
45
|
"scripts": {
|
|
46
46
|
"build": "tsc -p tsconfig.json",
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
import { parseArgs, type ParseArgsOptionsConfig } from "node:util";
|
|
2
|
-
type ParseCliArgsResult = ReturnType<typeof parseArgs> | {
|
|
3
|
-
error: string;
|
|
4
|
-
};
|
|
5
|
-
export declare function parseCliArgs(args: string[], options: ParseArgsOptionsConfig): ParseCliArgsResult;
|
|
6
|
-
export {};
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { parseArgs } from "node:util";
|
|
2
|
-
export function parseCliArgs(args, options) {
|
|
3
|
-
try {
|
|
4
|
-
return parseArgs({
|
|
5
|
-
args,
|
|
6
|
-
options,
|
|
7
|
-
allowPositionals: false,
|
|
8
|
-
strict: true,
|
|
9
|
-
});
|
|
10
|
-
}
|
|
11
|
-
catch (error) {
|
|
12
|
-
return { error: formatParseArgsError(error) };
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
function formatParseArgsError(error) {
|
|
16
|
-
if (error instanceof Error) {
|
|
17
|
-
return error.message;
|
|
18
|
-
}
|
|
19
|
-
return "Invalid arguments";
|
|
20
|
-
}
|
package/dist/commands/tenant.js
DELETED
|
@@ -1,348 +0,0 @@
|
|
|
1
|
-
import * as p from "@clack/prompts";
|
|
2
|
-
import { createClient, validateToken, checkRequiredScopes, listUserProjects, getProjectDetail, GitHubScopeError, } from "../github/client.js";
|
|
3
|
-
import { ensureGhAuth, getGhToken, GhAuthError } from "../github/gh-auth.js";
|
|
4
|
-
import { loadGlobalConfig, saveGlobalConfig, loadTenantConfig, tenantConfigDir, } from "../config.js";
|
|
5
|
-
import { writeConfig, generateTenantId, abortIfCancelled } from "./init.js";
|
|
6
|
-
// ── Scope error display ───────────────────────────────────────────────────────
|
|
7
|
-
const KNOWN_REQUIRED_SCOPES = ["repo", "read:org", "project"];
|
|
8
|
-
function displayScopeError(error, retryCommand) {
|
|
9
|
-
const plural = error.requiredScopes.length === 1 ? "" : "s";
|
|
10
|
-
p.log.error(`Token is missing required scope${plural}: ${error.requiredScopes.join(", ")}`);
|
|
11
|
-
const currentSet = new Set(error.currentScopes.map((s) => s.toLowerCase()));
|
|
12
|
-
const scopesToAdd = KNOWN_REQUIRED_SCOPES.filter((s) => !currentSet.has(s));
|
|
13
|
-
const scopeArg = scopesToAdd.length > 0
|
|
14
|
-
? scopesToAdd.join(",")
|
|
15
|
-
: error.requiredScopes.join(",");
|
|
16
|
-
p.note(`gh auth refresh --scopes ${scopeArg}\n\nThen re-run: ${retryCommand}`, "Fix missing scope");
|
|
17
|
-
}
|
|
18
|
-
function parseTenantAddFlags(args) {
|
|
19
|
-
const flags = { nonInteractive: false, assignedOnly: false };
|
|
20
|
-
for (let i = 0; i < args.length; i += 1) {
|
|
21
|
-
const arg = args[i];
|
|
22
|
-
const next = args[i + 1];
|
|
23
|
-
switch (arg) {
|
|
24
|
-
case "--non-interactive":
|
|
25
|
-
flags.nonInteractive = true;
|
|
26
|
-
break;
|
|
27
|
-
case "--project":
|
|
28
|
-
flags.project = next;
|
|
29
|
-
i += 1;
|
|
30
|
-
break;
|
|
31
|
-
case "--workspace-dir":
|
|
32
|
-
flags.workspaceDir = next;
|
|
33
|
-
i += 1;
|
|
34
|
-
break;
|
|
35
|
-
case "--assigned-only":
|
|
36
|
-
flags.assignedOnly = true;
|
|
37
|
-
break;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
return flags;
|
|
41
|
-
}
|
|
42
|
-
// ── Tenant command handler ────────────────────────────────────────────────────
|
|
43
|
-
const handler = async (args, options) => {
|
|
44
|
-
const [subcommand, ...rest] = args;
|
|
45
|
-
switch (subcommand) {
|
|
46
|
-
case "add":
|
|
47
|
-
await tenantAdd(rest, options);
|
|
48
|
-
return;
|
|
49
|
-
case "list":
|
|
50
|
-
await tenantList(options);
|
|
51
|
-
return;
|
|
52
|
-
case "remove":
|
|
53
|
-
await tenantRemove(rest, options);
|
|
54
|
-
return;
|
|
55
|
-
default:
|
|
56
|
-
process.stdout.write("Usage: gh-symphony tenant <add|list|remove>\n");
|
|
57
|
-
}
|
|
58
|
-
};
|
|
59
|
-
export default handler;
|
|
60
|
-
// ── tenant add ───────────────────────────────────────────────────────────────
|
|
61
|
-
async function tenantAdd(args, options) {
|
|
62
|
-
const flags = parseTenantAddFlags(args);
|
|
63
|
-
if (flags.nonInteractive) {
|
|
64
|
-
await tenantAddNonInteractive(flags, options);
|
|
65
|
-
return;
|
|
66
|
-
}
|
|
67
|
-
await tenantAddInteractive(options);
|
|
68
|
-
}
|
|
69
|
-
// ── Non-interactive tenant add ───────────────────────────────────────────────
|
|
70
|
-
async function tenantAddNonInteractive(flags, options) {
|
|
71
|
-
let token;
|
|
72
|
-
try {
|
|
73
|
-
token = getGhToken();
|
|
74
|
-
}
|
|
75
|
-
catch {
|
|
76
|
-
process.stderr.write("Error: GitHub token not found. Run 'gh auth login --scopes repo,read:org,project' or set GITHUB_GRAPHQL_TOKEN.\n");
|
|
77
|
-
process.exitCode = 1;
|
|
78
|
-
return;
|
|
79
|
-
}
|
|
80
|
-
const client = createClient(token);
|
|
81
|
-
// Validate token
|
|
82
|
-
let viewer;
|
|
83
|
-
try {
|
|
84
|
-
viewer = await validateToken(client);
|
|
85
|
-
}
|
|
86
|
-
catch {
|
|
87
|
-
process.stderr.write("Error: Invalid GitHub token.\n");
|
|
88
|
-
process.exitCode = 1;
|
|
89
|
-
return;
|
|
90
|
-
}
|
|
91
|
-
const scopeCheck = checkRequiredScopes(viewer.scopes);
|
|
92
|
-
if (!scopeCheck.valid) {
|
|
93
|
-
process.stderr.write(`Error: Missing required PAT scopes: ${scopeCheck.missing.join(", ")}\n`);
|
|
94
|
-
process.exitCode = 1;
|
|
95
|
-
return;
|
|
96
|
-
}
|
|
97
|
-
// Find project
|
|
98
|
-
const projects = await listUserProjects(client);
|
|
99
|
-
let project;
|
|
100
|
-
if (flags.project) {
|
|
101
|
-
const match = projects.find((p) => p.id === flags.project || p.url === flags.project);
|
|
102
|
-
if (!match) {
|
|
103
|
-
process.stderr.write(`Error: Project not found: ${flags.project}\n`);
|
|
104
|
-
process.exitCode = 1;
|
|
105
|
-
return;
|
|
106
|
-
}
|
|
107
|
-
project = await getProjectDetail(client, match.id);
|
|
108
|
-
}
|
|
109
|
-
else if (projects.length === 1) {
|
|
110
|
-
project = await getProjectDetail(client, projects[0].id);
|
|
111
|
-
}
|
|
112
|
-
else {
|
|
113
|
-
process.stderr.write("Error: --project is required when multiple projects exist.\n");
|
|
114
|
-
process.exitCode = 1;
|
|
115
|
-
return;
|
|
116
|
-
}
|
|
117
|
-
const tenantId = generateTenantId(project.title, project.id);
|
|
118
|
-
const workspaceDir = flags.workspaceDir ?? `${options.configDir}/workspaces`;
|
|
119
|
-
await writeConfig(options.configDir, {
|
|
120
|
-
tenantId,
|
|
121
|
-
project,
|
|
122
|
-
repos: project.linkedRepositories,
|
|
123
|
-
workspaceDir,
|
|
124
|
-
assignedOnly: flags.assignedOnly,
|
|
125
|
-
});
|
|
126
|
-
if (options.json) {
|
|
127
|
-
process.stdout.write(JSON.stringify({ tenantId, status: "created" }) + "\n");
|
|
128
|
-
}
|
|
129
|
-
else {
|
|
130
|
-
process.stdout.write(`Tenant created: ${tenantId}\n`);
|
|
131
|
-
process.stdout.write(`Run 'gh-symphony start' to begin orchestration.\n`);
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
// ── Interactive tenant add ───────────────────────────────────────────────────
|
|
135
|
-
async function tenantAddInteractive(options) {
|
|
136
|
-
p.intro("gh-symphony — Tenant Setup");
|
|
137
|
-
// Detect existing config
|
|
138
|
-
const existingConfig = await loadGlobalConfig(options.configDir);
|
|
139
|
-
if (existingConfig) {
|
|
140
|
-
const action = await abortIfCancelled(p.select({
|
|
141
|
-
message: "Existing configuration detected. What would you like to do?",
|
|
142
|
-
options: [
|
|
143
|
-
{ value: "add", label: "Add a new tenant" },
|
|
144
|
-
{ value: "overwrite", label: "Start fresh (overwrite)" },
|
|
145
|
-
],
|
|
146
|
-
}));
|
|
147
|
-
if (action === "overwrite") {
|
|
148
|
-
// Continue with fresh setup — will overwrite config
|
|
149
|
-
}
|
|
150
|
-
// "add" continues to create a new tenant alongside existing ones
|
|
151
|
-
}
|
|
152
|
-
// ── Step 1: gh CLI authentication ─────────────────────────────────────────────
|
|
153
|
-
const s1 = p.spinner();
|
|
154
|
-
s1.start("Checking gh CLI authentication...");
|
|
155
|
-
let login;
|
|
156
|
-
let client;
|
|
157
|
-
try {
|
|
158
|
-
const { login: ghLogin, token } = ensureGhAuth();
|
|
159
|
-
login = ghLogin;
|
|
160
|
-
client = createClient(token);
|
|
161
|
-
s1.stop(`Authenticated as ${login}`);
|
|
162
|
-
}
|
|
163
|
-
catch (error) {
|
|
164
|
-
s1.stop("Authentication failed.");
|
|
165
|
-
if (error instanceof GhAuthError) {
|
|
166
|
-
if (error.code === "not_installed") {
|
|
167
|
-
p.log.error("gh CLI가 설치되어 있지 않습니다. https://cli.github.com 에서 설치하세요.");
|
|
168
|
-
}
|
|
169
|
-
else if (error.code === "not_authenticated") {
|
|
170
|
-
p.log.error("gh auth login --scopes repo,read:org,project 를 실행하세요.");
|
|
171
|
-
}
|
|
172
|
-
else if (error.code === "missing_scopes") {
|
|
173
|
-
p.log.error("gh auth refresh --scopes repo,read:org,project 를 실행하세요.");
|
|
174
|
-
}
|
|
175
|
-
else {
|
|
176
|
-
p.log.error(error.message);
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
else {
|
|
180
|
-
p.log.error(error instanceof Error ? error.message : "Unknown error");
|
|
181
|
-
}
|
|
182
|
-
process.exitCode = 1;
|
|
183
|
-
return;
|
|
184
|
-
}
|
|
185
|
-
// ── Step 2: Project selection ───────────────────────────────────────────────
|
|
186
|
-
const s2 = p.spinner();
|
|
187
|
-
s2.start("Loading projects...");
|
|
188
|
-
let projects;
|
|
189
|
-
try {
|
|
190
|
-
projects = await listUserProjects(client);
|
|
191
|
-
s2.stop(`Found ${projects.length} project${projects.length === 1 ? "" : "s"}`);
|
|
192
|
-
}
|
|
193
|
-
catch (error) {
|
|
194
|
-
s2.stop("Failed to load projects.");
|
|
195
|
-
if (error instanceof GitHubScopeError) {
|
|
196
|
-
displayScopeError(error, "gh-symphony tenant add");
|
|
197
|
-
}
|
|
198
|
-
else {
|
|
199
|
-
p.log.error(error instanceof Error ? error.message : "Unknown error");
|
|
200
|
-
}
|
|
201
|
-
process.exitCode = 1;
|
|
202
|
-
return;
|
|
203
|
-
}
|
|
204
|
-
if (projects.length === 0) {
|
|
205
|
-
p.log.error("No GitHub Projects found. Create a project at https://github.com/orgs/YOUR_ORG/projects and re-run.");
|
|
206
|
-
process.exitCode = 1;
|
|
207
|
-
return;
|
|
208
|
-
}
|
|
209
|
-
const selectedProjectId = await abortIfCancelled(p.select({
|
|
210
|
-
message: "Step 1/4 — Select a GitHub Project:",
|
|
211
|
-
options: projects.map((proj) => ({
|
|
212
|
-
value: proj.id,
|
|
213
|
-
label: `${proj.owner.login}/${proj.title}`,
|
|
214
|
-
hint: `${proj.openItemCount} items`,
|
|
215
|
-
})),
|
|
216
|
-
maxItems: 15,
|
|
217
|
-
}));
|
|
218
|
-
const s2d = p.spinner();
|
|
219
|
-
s2d.start("Loading project details...");
|
|
220
|
-
let projectDetail;
|
|
221
|
-
try {
|
|
222
|
-
projectDetail = await getProjectDetail(client, selectedProjectId);
|
|
223
|
-
s2d.stop(`Loaded: ${projectDetail.title}`);
|
|
224
|
-
}
|
|
225
|
-
catch (error) {
|
|
226
|
-
s2d.stop("Failed to load project details.");
|
|
227
|
-
p.log.error(error instanceof Error ? error.message : "Unknown error");
|
|
228
|
-
process.exitCode = 1;
|
|
229
|
-
return;
|
|
230
|
-
}
|
|
231
|
-
// ── Step 2: Repository selection ────────────────────────────────────────────
|
|
232
|
-
if (projectDetail.linkedRepositories.length === 0) {
|
|
233
|
-
p.log.warn("No linked repositories found in this project. Add issues from repositories to the project first.");
|
|
234
|
-
process.exitCode = 1;
|
|
235
|
-
return;
|
|
236
|
-
}
|
|
237
|
-
const selectedRepos = await abortIfCancelled(p.multiselect({
|
|
238
|
-
message: "Step 2/4 — Select repositories to orchestrate:",
|
|
239
|
-
options: projectDetail.linkedRepositories.map((repo) => ({
|
|
240
|
-
value: repo,
|
|
241
|
-
label: `${repo.owner}/${repo.name}`,
|
|
242
|
-
})),
|
|
243
|
-
required: true,
|
|
244
|
-
}));
|
|
245
|
-
// ── Step 3: Assignment filter ────────────────────────────────────────────────
|
|
246
|
-
const assignedOnly = await abortIfCancelled(p.confirm({
|
|
247
|
-
message: "Step 3/4 — Only process issues assigned to the authenticated GitHub user?",
|
|
248
|
-
initialValue: false,
|
|
249
|
-
}));
|
|
250
|
-
const workspaceDir = await abortIfCancelled(p.text({
|
|
251
|
-
message: "Step 4/4 — Workspace root directory:",
|
|
252
|
-
placeholder: `${options.configDir}/workspaces`,
|
|
253
|
-
defaultValue: `${options.configDir}/workspaces`,
|
|
254
|
-
validate(value) {
|
|
255
|
-
return value.trim().length > 0
|
|
256
|
-
? undefined
|
|
257
|
-
: "Workspace directory is required.";
|
|
258
|
-
},
|
|
259
|
-
}));
|
|
260
|
-
// ── Confirmation ─────────────────────────────────────────────────────────────
|
|
261
|
-
p.note([
|
|
262
|
-
`User: ${login}`,
|
|
263
|
-
`Project: ${projectDetail.title}`,
|
|
264
|
-
`Repos: ${selectedRepos.map((r) => `${r.owner}/${r.name}`).join(", ")}`,
|
|
265
|
-
`Assigned: ${assignedOnly ? `Only issues assigned to ${login}` : "All project issues"}`,
|
|
266
|
-
`Workspace: ${workspaceDir}`,
|
|
267
|
-
].join("\n"), "Configuration Summary");
|
|
268
|
-
const confirmed = await abortIfCancelled(p.confirm({ message: "Apply this configuration?" }));
|
|
269
|
-
if (!confirmed) {
|
|
270
|
-
p.cancel("Setup cancelled.");
|
|
271
|
-
process.exitCode = 130;
|
|
272
|
-
return;
|
|
273
|
-
}
|
|
274
|
-
// ── Write config files ────────────────────────────────────────────────────────
|
|
275
|
-
const tenantId = generateTenantId(projectDetail.title, projectDetail.id);
|
|
276
|
-
const s6 = p.spinner();
|
|
277
|
-
s6.start("Writing configuration...");
|
|
278
|
-
try {
|
|
279
|
-
await writeConfig(options.configDir, {
|
|
280
|
-
tenantId,
|
|
281
|
-
project: projectDetail,
|
|
282
|
-
repos: selectedRepos,
|
|
283
|
-
workspaceDir,
|
|
284
|
-
assignedOnly,
|
|
285
|
-
});
|
|
286
|
-
s6.stop("Configuration saved.");
|
|
287
|
-
}
|
|
288
|
-
catch (error) {
|
|
289
|
-
s6.stop("Failed to write configuration.");
|
|
290
|
-
p.log.error(error instanceof Error ? error.message : "Unknown error");
|
|
291
|
-
process.exitCode = 1;
|
|
292
|
-
return;
|
|
293
|
-
}
|
|
294
|
-
p.outro(`Tenant "${tenantId}" created!\n Run 'gh-symphony start' to begin orchestration.`);
|
|
295
|
-
}
|
|
296
|
-
// ── tenant list ───────────────────────────────────────────────────────────────
|
|
297
|
-
async function tenantList(options) {
|
|
298
|
-
const global = await loadGlobalConfig(options.configDir);
|
|
299
|
-
if (!global?.tenants?.length) {
|
|
300
|
-
process.stdout.write("No tenants configured.\n");
|
|
301
|
-
return;
|
|
302
|
-
}
|
|
303
|
-
process.stdout.write("Configured tenants:\n");
|
|
304
|
-
const configs = await Promise.all(global.tenants.map((id) => loadTenantConfig(options.configDir, id)));
|
|
305
|
-
for (let i = 0; i < global.tenants.length; i++) {
|
|
306
|
-
const tenantId = global.tenants[i];
|
|
307
|
-
const config = configs[i];
|
|
308
|
-
const active = global.activeTenant === tenantId ? " (active)" : "";
|
|
309
|
-
const slug = config?.slug ?? tenantId;
|
|
310
|
-
process.stdout.write(` ${slug}${active}\n`);
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
// ── tenant remove ─────────────────────────────────────────────────────────────
|
|
314
|
-
async function tenantRemove(args, options) {
|
|
315
|
-
const tenantId = args[0];
|
|
316
|
-
if (!tenantId) {
|
|
317
|
-
process.stderr.write("Usage: gh-symphony tenant remove <tenant-id>\n");
|
|
318
|
-
process.exitCode = 1;
|
|
319
|
-
return;
|
|
320
|
-
}
|
|
321
|
-
const global = await loadGlobalConfig(options.configDir);
|
|
322
|
-
if (!global) {
|
|
323
|
-
process.stderr.write("No configuration found.\n");
|
|
324
|
-
process.exitCode = 1;
|
|
325
|
-
return;
|
|
326
|
-
}
|
|
327
|
-
const updatedTenants = (global.tenants ?? []).filter((t) => t !== tenantId);
|
|
328
|
-
if (updatedTenants.length === global.tenants.length) {
|
|
329
|
-
process.stderr.write(`Tenant "${tenantId}" not found.\n`);
|
|
330
|
-
process.exitCode = 1;
|
|
331
|
-
return;
|
|
332
|
-
}
|
|
333
|
-
const updatedConfig = {
|
|
334
|
-
...global,
|
|
335
|
-
tenants: updatedTenants,
|
|
336
|
-
activeTenant: global.activeTenant === tenantId ? null : global.activeTenant,
|
|
337
|
-
};
|
|
338
|
-
await saveGlobalConfig(options.configDir, updatedConfig);
|
|
339
|
-
const { rm } = await import("node:fs/promises");
|
|
340
|
-
const dir = tenantConfigDir(options.configDir, tenantId);
|
|
341
|
-
try {
|
|
342
|
-
await rm(dir, { recursive: true, force: true });
|
|
343
|
-
}
|
|
344
|
-
catch {
|
|
345
|
-
// Directory may not exist
|
|
346
|
-
}
|
|
347
|
-
process.stdout.write(`Tenant "${tenantId}" removed.\n`);
|
|
348
|
-
}
|