flamecast 0.4.1 → 0.5.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/cli.js +149 -59
- package/dist/commands/cast/add.js +42 -9
- package/dist/commands/login.js +27 -6
- package/dist/commands/task/create.js +1 -1
- package/dist/commands/task/get.js +1 -1
- package/dist/commands/task/list.js +1 -1
- package/dist/commands/workspace/delete.js +1 -1
- package/dist/commands/workspace/get.js +1 -2
- package/dist/commands/workspace/set-default.js +1 -1
- package/dist/commands/workspace/set-secrets.js +1 -1
- package/dist/commands/workspace/sync-workflows.js +1 -1
- package/dist/lib/auth.js +98 -74
- package/dist/lib/callback-server.js +72 -0
- package/dist/lib/resolve-workspace.js +2 -1
- package/package.json +7 -5
- package/dist/lib/api.js +0 -11
package/dist/cli.js
CHANGED
|
@@ -14,69 +14,159 @@ function getVersion() {
|
|
|
14
14
|
return "unknown";
|
|
15
15
|
}
|
|
16
16
|
}
|
|
17
|
-
// Load .env from the CLI package root
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
17
|
+
// Load .env from the CLI package root, but only in dev (cli:local sets
|
|
18
|
+
// FLAMECAST_BASE_URL as a real env var; the .env file is for local dev only).
|
|
19
|
+
if (process.env.NODE_ENV === "development") {
|
|
20
|
+
const __cli_dir = dirname(fileURLToPath(import.meta.url));
|
|
21
|
+
const envPath = resolve(__cli_dir, "..", ".env");
|
|
22
|
+
if (existsSync(envPath)) {
|
|
23
|
+
for (const line of readFileSync(envPath, "utf-8").split("\n")) {
|
|
24
|
+
const match = line.match(/^([^#=]+)=(.*)$/);
|
|
25
|
+
if (match && !process.env[match[1]]) {
|
|
26
|
+
process.env[match[1]] = match[2];
|
|
27
|
+
}
|
|
25
28
|
}
|
|
26
29
|
}
|
|
27
30
|
}
|
|
28
|
-
import {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
31
|
+
import { Command } from "commander";
|
|
32
|
+
const program = new Command()
|
|
33
|
+
.name("flame")
|
|
34
|
+
.version(getVersion())
|
|
35
|
+
.description("Flamecast CLI");
|
|
36
|
+
// ── workspace | ws | cast ──────────────────────────
|
|
37
|
+
function registerWorkspaceCommands(cmd) {
|
|
38
|
+
cmd
|
|
39
|
+
.command("create")
|
|
40
|
+
.alias("c")
|
|
41
|
+
.alias("add")
|
|
42
|
+
.description("Create a new workspace (interactive)")
|
|
43
|
+
.action(async () => {
|
|
44
|
+
const { handleWorkspaceCreate } = await import("./commands/workspace/create.js");
|
|
45
|
+
await handleWorkspaceCreate();
|
|
46
|
+
});
|
|
47
|
+
cmd
|
|
48
|
+
.command("list")
|
|
49
|
+
.alias("ls")
|
|
50
|
+
.description("List all workspaces")
|
|
51
|
+
.action(async () => {
|
|
52
|
+
const { handleWorkspaceList } = await import("./commands/workspace/list.js");
|
|
53
|
+
await handleWorkspaceList();
|
|
54
|
+
});
|
|
55
|
+
// Default action when no subcommand — show list
|
|
56
|
+
cmd.action(async () => {
|
|
57
|
+
const { handleWorkspaceList } = await import("./commands/workspace/list.js");
|
|
58
|
+
await handleWorkspaceList();
|
|
59
|
+
});
|
|
60
|
+
cmd
|
|
61
|
+
.command("get <ref>")
|
|
62
|
+
.alias("g")
|
|
63
|
+
.description("Get workspace details by name or ID")
|
|
64
|
+
.action(async (ref) => {
|
|
65
|
+
const { handleWorkspaceGet } = await import("./commands/workspace/get.js");
|
|
66
|
+
await handleWorkspaceGet(ref);
|
|
67
|
+
});
|
|
68
|
+
cmd
|
|
69
|
+
.command("delete <ref>")
|
|
70
|
+
.alias("rm")
|
|
71
|
+
.description("Delete a workspace")
|
|
72
|
+
.action(async (ref) => {
|
|
73
|
+
const { handleWorkspaceDelete } = await import("./commands/workspace/delete.js");
|
|
74
|
+
await handleWorkspaceDelete(ref);
|
|
75
|
+
});
|
|
76
|
+
cmd
|
|
77
|
+
.command("set-default <ref>")
|
|
78
|
+
.description("Set the default workspace")
|
|
79
|
+
.action(async (ref) => {
|
|
80
|
+
const { handleWorkspaceSetDefault } = await import("./commands/workspace/set-default.js");
|
|
81
|
+
await handleWorkspaceSetDefault(ref);
|
|
82
|
+
});
|
|
83
|
+
cmd
|
|
84
|
+
.command("set-secrets <ref> [secrets...]")
|
|
85
|
+
.description("Set secrets on a workspace (KEY=VALUE ...)")
|
|
86
|
+
.action(async (ref, secrets) => {
|
|
87
|
+
const { handleWorkspaceSetSecrets } = await import("./commands/workspace/set-secrets.js");
|
|
88
|
+
await handleWorkspaceSetSecrets(ref, secrets);
|
|
89
|
+
});
|
|
90
|
+
cmd
|
|
91
|
+
.command("sync <ref>")
|
|
92
|
+
.alias("sync-workflows")
|
|
93
|
+
.description("Sync workflow files to the workspace repo")
|
|
94
|
+
.action(async (ref) => {
|
|
95
|
+
const { handleWorkspaceSyncWorkflows } = await import("./commands/workspace/sync-workflows.js");
|
|
96
|
+
await handleWorkspaceSyncWorkflows(ref);
|
|
97
|
+
});
|
|
42
98
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
99
|
+
const workspaceCmd = program
|
|
100
|
+
.command("workspace")
|
|
101
|
+
.alias("ws")
|
|
102
|
+
.description("Manage workspaces");
|
|
103
|
+
registerWorkspaceCommands(workspaceCmd);
|
|
104
|
+
// "cast" as a top-level alias for "workspace"
|
|
105
|
+
const castCmd = program
|
|
106
|
+
.command("cast")
|
|
107
|
+
.description("Manage workspaces (alias for workspace)");
|
|
108
|
+
registerWorkspaceCommands(castCmd);
|
|
109
|
+
// ── task | t ───────────────────────────────────────
|
|
110
|
+
const taskCmd = program.command("task").alias("t").description("Manage tasks");
|
|
111
|
+
taskCmd
|
|
112
|
+
.command("create <prompt...>")
|
|
113
|
+
.alias("c")
|
|
114
|
+
.description("Create a new task")
|
|
115
|
+
.option("-w, --workspace <ref>", "Workspace name or ID (default: default workspace)")
|
|
116
|
+
.action(async (prompt, opts) => {
|
|
117
|
+
const { handleTaskCreate } = await import("./commands/task/create.js");
|
|
118
|
+
await handleTaskCreate(prompt, opts.workspace);
|
|
119
|
+
});
|
|
120
|
+
taskCmd
|
|
121
|
+
.command("list")
|
|
122
|
+
.alias("ls")
|
|
123
|
+
.description("List tasks for a workspace")
|
|
124
|
+
.option("-w, --workspace <ref>", "Workspace name or ID (default: default workspace)")
|
|
125
|
+
.action(async (opts) => {
|
|
126
|
+
const { handleTaskList } = await import("./commands/task/list.js");
|
|
127
|
+
await handleTaskList(opts.workspace);
|
|
128
|
+
});
|
|
129
|
+
// Default action when no subcommand — show list
|
|
130
|
+
taskCmd.action(async () => {
|
|
131
|
+
const { handleTaskList } = await import("./commands/task/list.js");
|
|
132
|
+
await handleTaskList();
|
|
133
|
+
});
|
|
134
|
+
taskCmd
|
|
135
|
+
.command("get <taskId>")
|
|
136
|
+
.alias("g")
|
|
137
|
+
.description("Get task details")
|
|
138
|
+
.option("-w, --workspace <ref>", "Workspace name or ID (default: default workspace)")
|
|
139
|
+
.action(async (taskId, opts) => {
|
|
140
|
+
const { handleTaskGet } = await import("./commands/task/get.js");
|
|
141
|
+
await handleTaskGet(taskId, opts.workspace);
|
|
142
|
+
});
|
|
143
|
+
// ── top-level commands ─────────────────────────────
|
|
144
|
+
program
|
|
145
|
+
.command("login")
|
|
146
|
+
.description("Authenticate with Flamecast")
|
|
147
|
+
.action(async () => {
|
|
148
|
+
const { handleLogin } = await import("./commands/login.js");
|
|
62
149
|
await handleLogin();
|
|
63
|
-
}
|
|
64
|
-
|
|
150
|
+
});
|
|
151
|
+
program
|
|
152
|
+
.command("logout")
|
|
153
|
+
.description("Clear saved credentials")
|
|
154
|
+
.action(async () => {
|
|
155
|
+
const { handleLogout } = await import("./commands/logout.js");
|
|
65
156
|
await handleLogout();
|
|
66
|
-
}
|
|
67
|
-
|
|
157
|
+
});
|
|
158
|
+
program
|
|
159
|
+
.command("api")
|
|
160
|
+
.description("Print API key")
|
|
161
|
+
.action(async () => {
|
|
162
|
+
const { handleApi } = await import("./commands/api.js");
|
|
163
|
+
handleApi();
|
|
164
|
+
});
|
|
165
|
+
program
|
|
166
|
+
.command("setup-claude")
|
|
167
|
+
.description("Set up Claude Code token")
|
|
168
|
+
.action(async () => {
|
|
169
|
+
const { handleSetupClaude } = await import("./commands/setup-claude.js");
|
|
68
170
|
await handleSetupClaude();
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
console.error("Usage:");
|
|
72
|
-
console.error(" flame cast add Create a new cast member");
|
|
73
|
-
console.error(" flame cast [list] List all cast members");
|
|
74
|
-
console.error(" flame run <name> <prompt> Dispatch a task to a cast member");
|
|
75
|
-
console.error(" flame status Show status of all active tasks");
|
|
76
|
-
console.error(" flame result <name> latest Show latest task result");
|
|
77
|
-
console.error(" flame api Print API key");
|
|
78
|
-
console.error(" flame login Authenticate with Flamecast");
|
|
79
|
-
console.error(" flame setup-claude Set up Claude Code token");
|
|
80
|
-
console.error(" flame logout Clear saved credentials");
|
|
81
|
-
process.exit(1);
|
|
82
|
-
}
|
|
171
|
+
});
|
|
172
|
+
await program.parseAsync();
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import * as p from "@clack/prompts";
|
|
2
2
|
import { saveConfig } from "../../lib/config.js";
|
|
3
3
|
import { spawnSync } from "node:child_process";
|
|
4
|
-
import { client } from "../../lib/auth.js";
|
|
5
|
-
import { authenticateViaBrowser } from "../../lib/auth.js";
|
|
4
|
+
import { client, authenticateViaBrowser, verifyEmailAndExchange, exchangeForApiKey, } from "../../lib/auth.js";
|
|
6
5
|
import { openBrowser } from "../../lib/open-browser.js";
|
|
7
6
|
import { generateEmployeeName } from "../../lib/names.js";
|
|
8
7
|
import { detectClaudeCode, discoverLocalConfig } from "../../lib/detect.js";
|
|
@@ -18,15 +17,38 @@ export async function handleCastAdd() {
|
|
|
18
17
|
const needsSetup = !username;
|
|
19
18
|
if (needsSetup) {
|
|
20
19
|
p.log.step("\u2500\u2500 Setup (one-time) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
21
|
-
// Step 1: Authenticate with Flamecast
|
|
20
|
+
// Step 1: Authenticate with Flamecast
|
|
22
21
|
const s = p.spinner();
|
|
23
|
-
s.start("
|
|
22
|
+
s.start("Starting authentication...");
|
|
24
23
|
try {
|
|
25
|
-
const
|
|
26
|
-
s.
|
|
27
|
-
p.log.info(`If the browser didn't open, visit: ${verificationUri}`);
|
|
28
|
-
s.start("Waiting for confirmation...");
|
|
24
|
+
const result = await authenticateViaBrowser(message => {
|
|
25
|
+
s.message(message);
|
|
29
26
|
});
|
|
27
|
+
let apiKey;
|
|
28
|
+
if (result.pendingAuthenticationToken) {
|
|
29
|
+
s.stop("Email verification required");
|
|
30
|
+
const code = await p.text({
|
|
31
|
+
message: "Check your email for a verification code and enter it here:",
|
|
32
|
+
validate(value) {
|
|
33
|
+
if (!value.trim())
|
|
34
|
+
return "Verification code cannot be empty";
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
if (p.isCancel(code)) {
|
|
38
|
+
p.cancel("Setup cancelled");
|
|
39
|
+
process.exit(0);
|
|
40
|
+
}
|
|
41
|
+
s.start("Verifying...");
|
|
42
|
+
apiKey = await verifyEmailAndExchange(result.pendingAuthenticationToken, code, result.githubToken);
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
apiKey = await exchangeForApiKey({
|
|
46
|
+
refreshToken: result.refreshToken,
|
|
47
|
+
...(result.githubToken && {
|
|
48
|
+
githubToken: result.githubToken,
|
|
49
|
+
}),
|
|
50
|
+
});
|
|
51
|
+
}
|
|
30
52
|
saveConfig({ apiKey });
|
|
31
53
|
s.stop("Authenticated with Flamecast");
|
|
32
54
|
}
|
|
@@ -187,7 +209,18 @@ export async function handleCastAdd() {
|
|
|
187
209
|
});
|
|
188
210
|
const workflowUrl = `https://github.com/${githubRepo}/actions/runs/${task.id}`;
|
|
189
211
|
s6.stop(`Task dispatched \u2192 ${workflowUrl}`);
|
|
190
|
-
|
|
212
|
+
let taskGetCommand = `flame task get ${task.id}`;
|
|
213
|
+
try {
|
|
214
|
+
const { defaultWorkspaceId } = await client.workspaces.default.get();
|
|
215
|
+
if (defaultWorkspaceId !== workspaceId) {
|
|
216
|
+
taskGetCommand += ` --workspace ${workspaceId}`;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
catch {
|
|
220
|
+
// Fall back to explicit workspace if we can't resolve the default.
|
|
221
|
+
taskGetCommand += ` --workspace ${workspaceId}`;
|
|
222
|
+
}
|
|
223
|
+
p.outro(`\u{1F525} Run \`${taskGetCommand}\` to check in.`);
|
|
191
224
|
}
|
|
192
225
|
catch (err) {
|
|
193
226
|
s6.stop("Failed to dispatch task");
|
package/dist/commands/login.js
CHANGED
|
@@ -1,15 +1,36 @@
|
|
|
1
1
|
import * as p from "@clack/prompts";
|
|
2
2
|
import { saveConfig } from "../lib/config.js";
|
|
3
|
-
import { authenticateViaBrowser } from "../lib/auth.js";
|
|
3
|
+
import { authenticateViaBrowser, verifyEmailAndExchange, exchangeForApiKey, } from "../lib/auth.js";
|
|
4
4
|
export async function handleLogin() {
|
|
5
5
|
const s = p.spinner();
|
|
6
|
-
s.start("
|
|
6
|
+
s.start("Starting authentication...");
|
|
7
7
|
try {
|
|
8
|
-
const
|
|
9
|
-
s.
|
|
10
|
-
p.log.info(`If the browser didn't open, visit: ${verificationUri}`);
|
|
11
|
-
s.start("Waiting for confirmation...");
|
|
8
|
+
const result = await authenticateViaBrowser(message => {
|
|
9
|
+
s.message(message);
|
|
12
10
|
});
|
|
11
|
+
let apiKey;
|
|
12
|
+
if (result.pendingAuthenticationToken) {
|
|
13
|
+
s.stop("Email verification required");
|
|
14
|
+
const code = await p.text({
|
|
15
|
+
message: "Check your email for a verification code and enter it here:",
|
|
16
|
+
validate(value) {
|
|
17
|
+
if (!value.trim())
|
|
18
|
+
return "Verification code cannot be empty";
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
if (p.isCancel(code)) {
|
|
22
|
+
p.cancel("Login cancelled");
|
|
23
|
+
process.exit(0);
|
|
24
|
+
}
|
|
25
|
+
s.start("Verifying...");
|
|
26
|
+
apiKey = await verifyEmailAndExchange(result.pendingAuthenticationToken, code, result.githubToken);
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
apiKey = await exchangeForApiKey({
|
|
30
|
+
refreshToken: result.refreshToken,
|
|
31
|
+
...(result.githubToken && { githubToken: result.githubToken }),
|
|
32
|
+
});
|
|
33
|
+
}
|
|
13
34
|
saveConfig({ apiKey });
|
|
14
35
|
s.stop("Logged in");
|
|
15
36
|
}
|
|
@@ -6,7 +6,7 @@ export async function handleTaskCreate(promptParts, workspaceRef) {
|
|
|
6
6
|
console.error("Usage: flame task create <prompt> [-w workspace]");
|
|
7
7
|
process.exit(1);
|
|
8
8
|
}
|
|
9
|
-
const ws = await resolveWorkspace(
|
|
9
|
+
const ws = await resolveWorkspace(workspaceRef);
|
|
10
10
|
const task = await client.workspaces.tasks.create(ws.id, { prompt });
|
|
11
11
|
console.log(`Task dispatched to ${ws.name}.`);
|
|
12
12
|
if (task.workflowRunId) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { client } from "../../lib/auth.js";
|
|
2
2
|
import { resolveWorkspace } from "../../lib/resolve-workspace.js";
|
|
3
3
|
export async function handleTaskGet(taskId, workspaceRef) {
|
|
4
|
-
const ws = await resolveWorkspace(
|
|
4
|
+
const ws = await resolveWorkspace(workspaceRef);
|
|
5
5
|
const { task, outputs } = await client.workspaces.tasks.get(taskId, {
|
|
6
6
|
workspaceId: ws.id,
|
|
7
7
|
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { client } from "../../lib/auth.js";
|
|
2
2
|
import { resolveWorkspace } from "../../lib/resolve-workspace.js";
|
|
3
3
|
export async function handleTaskList(workspaceRef) {
|
|
4
|
-
const ws = await resolveWorkspace(
|
|
4
|
+
const ws = await resolveWorkspace(workspaceRef);
|
|
5
5
|
const page = await client.workspaces.tasks.list(ws.id);
|
|
6
6
|
const tasks = page.tasks ?? [];
|
|
7
7
|
if (tasks.length === 0) {
|
|
@@ -2,7 +2,7 @@ import * as p from "@clack/prompts";
|
|
|
2
2
|
import { client } from "../../lib/auth.js";
|
|
3
3
|
import { resolveWorkspace } from "../../lib/resolve-workspace.js";
|
|
4
4
|
export async function handleWorkspaceDelete(ref) {
|
|
5
|
-
const ws = await resolveWorkspace(
|
|
5
|
+
const ws = await resolveWorkspace(ref);
|
|
6
6
|
const confirmed = await p.confirm({
|
|
7
7
|
message: `Delete workspace "${ws.name}" (${ws.githubRepo})? This will also delete the GitHub repo.`,
|
|
8
8
|
initialValue: false,
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import { client } from "../../lib/auth.js";
|
|
2
1
|
import { resolveWorkspace } from "../../lib/resolve-workspace.js";
|
|
3
2
|
export async function handleWorkspaceGet(ref) {
|
|
4
|
-
const ws = await resolveWorkspace(
|
|
3
|
+
const ws = await resolveWorkspace(ref);
|
|
5
4
|
console.log(` Name: ${ws.name}`);
|
|
6
5
|
console.log(` ID: ${ws.id}`);
|
|
7
6
|
console.log(` Status: ${ws.status}`);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { client } from "../../lib/auth.js";
|
|
2
2
|
import { resolveWorkspace } from "../../lib/resolve-workspace.js";
|
|
3
3
|
export async function handleWorkspaceSetDefault(ref) {
|
|
4
|
-
const ws = await resolveWorkspace(
|
|
4
|
+
const ws = await resolveWorkspace(ref);
|
|
5
5
|
await client.workspaces.default.set({ workspaceId: ws.id });
|
|
6
6
|
console.log(`Default workspace set to "${ws.name}".`);
|
|
7
7
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { client } from "../../lib/auth.js";
|
|
2
2
|
import { resolveWorkspace } from "../../lib/resolve-workspace.js";
|
|
3
3
|
export async function handleWorkspaceSetSecrets(ref, secretArgs) {
|
|
4
|
-
const ws = await resolveWorkspace(
|
|
4
|
+
const ws = await resolveWorkspace(ref);
|
|
5
5
|
const secrets = {};
|
|
6
6
|
for (const arg of secretArgs) {
|
|
7
7
|
const eqIdx = arg.indexOf("=");
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { client } from "../../lib/auth.js";
|
|
2
2
|
import { resolveWorkspace } from "../../lib/resolve-workspace.js";
|
|
3
3
|
export async function handleWorkspaceSyncWorkflows(ref) {
|
|
4
|
-
const ws = await resolveWorkspace(
|
|
4
|
+
const ws = await resolveWorkspace(ref);
|
|
5
5
|
const result = await client.workspaces.syncWorkflows.sync(ws.id);
|
|
6
6
|
console.log(`Synced workflows for "${ws.name}".`);
|
|
7
7
|
console.log(` PR: ${result.pullRequestUrl}`);
|
package/dist/lib/auth.js
CHANGED
|
@@ -1,95 +1,100 @@
|
|
|
1
|
+
import { createWorkOS } from "@workos-inc/node";
|
|
1
2
|
import { openBrowser } from "./open-browser.js";
|
|
2
3
|
import { loadConfig, saveConfig } from "./config.js";
|
|
4
|
+
import { startCallbackServer } from "./callback-server.js";
|
|
3
5
|
import Flamecast from "@flamecast/api";
|
|
4
6
|
import * as dotenv from "dotenv";
|
|
5
7
|
dotenv.config();
|
|
6
8
|
const WORKOS_CLIENT_ID = process.env.WORKOS_CLIENT_ID ?? "client_01KD3FTW7R2QD0NW6QB7QP6Q0B";
|
|
7
9
|
export const FLAMECAST_BASE_URL = process.env.FLAMECAST_BASE_URL || "https://api.flamecast.dev";
|
|
8
|
-
|
|
9
|
-
return new Promise(resolve => setTimeout(resolve, ms));
|
|
10
|
-
}
|
|
10
|
+
const workos = createWorkOS({ clientId: WORKOS_CLIENT_ID });
|
|
11
11
|
/**
|
|
12
|
-
* Authenticates via WorkOS
|
|
12
|
+
* Authenticates via WorkOS Authorization Code flow with PKCE.
|
|
13
13
|
*
|
|
14
|
-
* 1.
|
|
15
|
-
* 2.
|
|
16
|
-
* 3.
|
|
17
|
-
* 4.
|
|
18
|
-
*
|
|
14
|
+
* 1. Start a local callback server
|
|
15
|
+
* 2. Generate authorization URL with PKCE
|
|
16
|
+
* 3. Open browser to WorkOS/GitHub auth
|
|
17
|
+
* 4. Receive callback with authorization code
|
|
18
|
+
* 5. Exchange code for tokens (includes GitHub OAuth token)
|
|
19
|
+
* 6. If email verification is required, return the pending token
|
|
20
|
+
* 7. Otherwise exchange WorkOS refresh token for a Flamecast API key
|
|
19
21
|
*/
|
|
20
|
-
export async function authenticateViaBrowser(
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
22
|
+
export async function authenticateViaBrowser(onStatus) {
|
|
23
|
+
const { redirectUri, waitForCallback, close } = await startCallbackServer();
|
|
24
|
+
try {
|
|
25
|
+
// Generate PKCE authorization URL
|
|
26
|
+
const { url, state, codeVerifier } = await workos.userManagement.getAuthorizationUrlWithPKCE({
|
|
27
|
+
provider: "GitHubOAuth",
|
|
28
|
+
redirectUri,
|
|
29
|
+
providerScopes: ["repo", "read:user"],
|
|
30
|
+
});
|
|
31
|
+
onStatus?.("Opening browser for authentication...");
|
|
32
|
+
openBrowser(url);
|
|
33
|
+
// Wait for the OAuth callback
|
|
34
|
+
onStatus?.("Waiting for browser authentication...");
|
|
35
|
+
const callback = await waitForCallback();
|
|
36
|
+
if (callback.state !== state) {
|
|
37
|
+
throw new Error("OAuth state mismatch");
|
|
38
|
+
}
|
|
39
|
+
// Exchange code for tokens — this returns oauthTokens (GitHub token)
|
|
40
|
+
try {
|
|
41
|
+
const authResult = await workos.userManagement.authenticateWithCodeAndVerifier({
|
|
42
|
+
code: callback.code,
|
|
43
|
+
codeVerifier,
|
|
44
|
+
});
|
|
45
|
+
return {
|
|
46
|
+
refreshToken: authResult.refreshToken,
|
|
47
|
+
githubToken: authResult.oauthTokens?.accessToken,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
catch (err) {
|
|
51
|
+
// WorkOS requires email verification — extract the pending token
|
|
52
|
+
const rawData = err?.rawData;
|
|
53
|
+
const pendingToken = rawData?.pending_authentication_token;
|
|
54
|
+
if (!pendingToken)
|
|
55
|
+
throw err;
|
|
56
|
+
return { refreshToken: "", pendingAuthenticationToken: pendingToken };
|
|
57
|
+
}
|
|
30
58
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const
|
|
37
|
-
deviceCode: device.device_code,
|
|
38
|
-
expiresIn: device.expires_in,
|
|
39
|
-
interval: device.interval,
|
|
40
|
-
});
|
|
41
|
-
// Step 4: Exchange WorkOS refresh token for Flamecast API key
|
|
42
|
-
const exchangeBody = {
|
|
43
|
-
refreshToken: tokens.refresh_token,
|
|
44
|
-
};
|
|
45
|
-
const exchangeRes = await fetch(`${FLAMECAST_BASE_URL}/auth/cli/exchange`, {
|
|
59
|
+
finally {
|
|
60
|
+
close();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
export async function exchangeForApiKey(body) {
|
|
64
|
+
const res = await fetch(`${FLAMECAST_BASE_URL}/auth/cli/exchange`, {
|
|
46
65
|
method: "POST",
|
|
47
66
|
headers: { "Content-Type": "application/json" },
|
|
48
|
-
body: JSON.stringify(
|
|
67
|
+
body: JSON.stringify(body),
|
|
49
68
|
});
|
|
50
|
-
if (!
|
|
51
|
-
const text = await
|
|
69
|
+
if (!res.ok) {
|
|
70
|
+
const text = await res.text();
|
|
52
71
|
throw new Error(`Failed to exchange token: ${text}`);
|
|
53
72
|
}
|
|
54
|
-
const { apiKey } = (await
|
|
73
|
+
const { apiKey } = (await res.json());
|
|
55
74
|
return apiKey;
|
|
56
75
|
}
|
|
57
|
-
async function
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
if (res.ok && data.access_token && data.refresh_token) {
|
|
73
|
-
return data;
|
|
74
|
-
}
|
|
75
|
-
switch (data.error) {
|
|
76
|
-
case "authorization_pending":
|
|
77
|
-
break;
|
|
78
|
-
case "slow_down":
|
|
79
|
-
pollInterval += 1;
|
|
80
|
-
break;
|
|
81
|
-
case "access_denied":
|
|
82
|
-
case "expired_token":
|
|
83
|
-
throw new Error("Authorization was denied or expired");
|
|
84
|
-
default:
|
|
85
|
-
throw new Error(`Unexpected auth error: ${data.error}`);
|
|
86
|
-
}
|
|
76
|
+
export async function verifyEmailAndExchange(pendingAuthenticationToken, verificationCode, githubToken) {
|
|
77
|
+
const body = {
|
|
78
|
+
pendingAuthenticationToken,
|
|
79
|
+
code: verificationCode,
|
|
80
|
+
};
|
|
81
|
+
if (githubToken)
|
|
82
|
+
body.githubToken = githubToken;
|
|
83
|
+
const res = await fetch(`${FLAMECAST_BASE_URL}/auth/cli/verify-email`, {
|
|
84
|
+
method: "POST",
|
|
85
|
+
headers: { "Content-Type": "application/json" },
|
|
86
|
+
body: JSON.stringify(body),
|
|
87
|
+
});
|
|
88
|
+
if (!res.ok) {
|
|
89
|
+
const text = await res.text();
|
|
90
|
+
throw new Error(`Email verification failed: ${text}`);
|
|
87
91
|
}
|
|
88
|
-
|
|
92
|
+
const { apiKey } = (await res.json());
|
|
93
|
+
return apiKey;
|
|
89
94
|
}
|
|
90
95
|
/**
|
|
91
96
|
* Ensures the user is authenticated. Returns a valid API key.
|
|
92
|
-
* If no valid key exists, runs the
|
|
97
|
+
* If no valid key exists, runs the auth flow.
|
|
93
98
|
*/
|
|
94
99
|
export async function ensureAuthenticated() {
|
|
95
100
|
const config = loadConfig();
|
|
@@ -108,10 +113,29 @@ export async function ensureAuthenticated() {
|
|
|
108
113
|
}
|
|
109
114
|
try {
|
|
110
115
|
console.log("Opening browser to authenticate...");
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
116
|
+
const result = await authenticateViaBrowser();
|
|
117
|
+
let apiKey;
|
|
118
|
+
if (result.pendingAuthenticationToken) {
|
|
119
|
+
// Email verification required — prompt in terminal
|
|
120
|
+
const readline = await import("node:readline");
|
|
121
|
+
const rl = readline.createInterface({
|
|
122
|
+
input: process.stdin,
|
|
123
|
+
output: process.stdout,
|
|
124
|
+
});
|
|
125
|
+
const code = await new Promise(resolve => {
|
|
126
|
+
rl.question("Check your email for a verification code and enter it here: ", answer => {
|
|
127
|
+
rl.close();
|
|
128
|
+
resolve(answer.trim());
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
apiKey = await verifyEmailAndExchange(result.pendingAuthenticationToken, code, result.githubToken);
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
apiKey = await exchangeForApiKey({
|
|
135
|
+
refreshToken: result.refreshToken,
|
|
136
|
+
...(result.githubToken && { githubToken: result.githubToken }),
|
|
137
|
+
});
|
|
138
|
+
}
|
|
115
139
|
saveConfig({ apiKey });
|
|
116
140
|
return { apiKey };
|
|
117
141
|
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { createServer } from "node:http";
|
|
2
|
+
import { URL } from "node:url";
|
|
3
|
+
const PORTS = [19284, 19285, 19286];
|
|
4
|
+
const TIMEOUT_MS = 120_000;
|
|
5
|
+
const SUCCESS_HTML = `<!DOCTYPE html>
|
|
6
|
+
<html><body style="font-family:system-ui;display:flex;justify-content:center;align-items:center;height:100vh;margin:0">
|
|
7
|
+
<div style="text-align:center"><h2>Authentication successful</h2><p>You can close this tab and return to the terminal.</p></div>
|
|
8
|
+
</body></html>`;
|
|
9
|
+
function tryListen(server, port) {
|
|
10
|
+
return new Promise((resolve, reject) => {
|
|
11
|
+
server.once("error", reject);
|
|
12
|
+
server.listen(port, "127.0.0.1", () => {
|
|
13
|
+
server.removeListener("error", reject);
|
|
14
|
+
resolve();
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
export async function startCallbackServer() {
|
|
19
|
+
let resolve;
|
|
20
|
+
let reject;
|
|
21
|
+
const callbackPromise = new Promise((res, rej) => {
|
|
22
|
+
resolve = res;
|
|
23
|
+
reject = rej;
|
|
24
|
+
});
|
|
25
|
+
const server = createServer((req, res) => {
|
|
26
|
+
if (!req.url?.startsWith("/callback")) {
|
|
27
|
+
res.writeHead(404);
|
|
28
|
+
res.end();
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const url = new URL(req.url, `http://127.0.0.1`);
|
|
32
|
+
const code = url.searchParams.get("code");
|
|
33
|
+
const state = url.searchParams.get("state");
|
|
34
|
+
const error = url.searchParams.get("error");
|
|
35
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
36
|
+
res.end(SUCCESS_HTML);
|
|
37
|
+
if (error) {
|
|
38
|
+
reject(new Error(`Authentication failed: ${error}`));
|
|
39
|
+
}
|
|
40
|
+
else if (code && state) {
|
|
41
|
+
resolve({ code, state });
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
reject(new Error("Missing code or state in callback"));
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
let boundPort;
|
|
48
|
+
for (const port of PORTS) {
|
|
49
|
+
try {
|
|
50
|
+
await tryListen(server, port);
|
|
51
|
+
boundPort = port;
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
catch { }
|
|
55
|
+
}
|
|
56
|
+
if (!boundPort) {
|
|
57
|
+
throw new Error(`Could not start callback server on any port (${PORTS.join(", ")})`);
|
|
58
|
+
}
|
|
59
|
+
const timeout = setTimeout(() => {
|
|
60
|
+
reject(new Error("Authentication timed out"));
|
|
61
|
+
server.close();
|
|
62
|
+
}, TIMEOUT_MS);
|
|
63
|
+
return {
|
|
64
|
+
port: boundPort,
|
|
65
|
+
redirectUri: `http://localhost:${boundPort}/callback`,
|
|
66
|
+
waitForCallback: () => callbackPromise,
|
|
67
|
+
close: () => {
|
|
68
|
+
clearTimeout(timeout);
|
|
69
|
+
server.close();
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
import { client } from "./auth.js";
|
|
1
2
|
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
2
3
|
/**
|
|
3
4
|
* Resolve a workspace from a name, UUID, or "default".
|
|
4
5
|
* If `ref` is omitted/undefined, resolves the user's default workspace.
|
|
5
6
|
*/
|
|
6
|
-
export async function resolveWorkspace(
|
|
7
|
+
export async function resolveWorkspace(ref) {
|
|
7
8
|
// No ref provided → use default
|
|
8
9
|
if (!ref) {
|
|
9
10
|
const { defaultWorkspaceId } = await client.workspaces.default.get();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "flamecast",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"flame": "./dist/cli.js"
|
|
@@ -16,13 +16,15 @@
|
|
|
16
16
|
"check-types": "tsc --noEmit",
|
|
17
17
|
"cli": "tsx src/cli.ts",
|
|
18
18
|
"cli:local": "FLAMECAST_BASE_URL=http://localhost:6970 tsx src/cli.ts",
|
|
19
|
-
"
|
|
20
|
-
"
|
|
19
|
+
"secrets:pull": "infisical export --path=/packages/cli --env=dev --format=dotenv > .env",
|
|
20
|
+
"secrets:push": "infisical secrets set --file=.env --path=/packages/cli --env=dev"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
23
|
"@clack/prompts": "^0.9",
|
|
24
|
-
"
|
|
25
|
-
"@
|
|
24
|
+
"@flamecast/api": "https://pkg.stainless.com/s/flamecast-typescript/8bcb3971cf86ab592fcb8d2eb121e4f70abff474/dist.tar.gz",
|
|
25
|
+
"@workos-inc/node": "^8",
|
|
26
|
+
"commander": "^14.0.3",
|
|
27
|
+
"dotenv": "^17.3.1"
|
|
26
28
|
},
|
|
27
29
|
"devDependencies": {
|
|
28
30
|
"@flamecast/typescript-config": "workspace:*",
|
package/dist/lib/api.js
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import Flamecast from "@flamecast/api";
|
|
2
|
-
const DEFAULT_API_URL = "https://api.flamecast.dev";
|
|
3
|
-
export function getBaseUrl(apiUrl) {
|
|
4
|
-
return apiUrl || process.env.FLAMECAST_BASE_URL || DEFAULT_API_URL;
|
|
5
|
-
}
|
|
6
|
-
export function createApiClient(apiKey, apiUrl) {
|
|
7
|
-
return new Flamecast({
|
|
8
|
-
apiKey,
|
|
9
|
-
baseURL: getBaseUrl(apiUrl),
|
|
10
|
-
});
|
|
11
|
-
}
|