feedbackbasket-cli 3.0.0 → 3.1.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/CHANGELOG.md +290 -274
- package/README.md +340 -336
- package/dist/src/auth/login.d.ts +4 -3
- package/dist/src/auth/login.js +70 -43
- package/dist/src/capabilities.d.ts +4 -0
- package/dist/src/commands/auth.d.ts +9 -3
- package/dist/src/commands/auth.js +100 -93
- package/dist/src/version.d.ts +1 -1
- package/package.json +54 -54
- package/skills/feedbackbasket/SKILL.md +407 -377
package/dist/src/auth/login.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
interface LoginResult {
|
|
2
2
|
token: string;
|
|
3
|
-
scope:
|
|
3
|
+
scope: "read" | "full";
|
|
4
4
|
}
|
|
5
|
+
export declare function validateReturnedScope(value: string | null, maximum: "read" | "full"): "read" | "full";
|
|
5
6
|
export declare function isCliTokenFormat(token: string): boolean;
|
|
6
|
-
export declare function browserLogin(baseUrl: string, scope?:
|
|
7
|
-
export declare function manualLogin(baseUrl: string, scope?:
|
|
7
|
+
export declare function browserLogin(baseUrl: string, scope?: "read" | "full"): Promise<LoginResult>;
|
|
8
|
+
export declare function manualLogin(baseUrl: string, scope?: "read" | "full"): Promise<LoginResult>;
|
|
8
9
|
export {};
|
package/dist/src/auth/login.js
CHANGED
|
@@ -1,10 +1,19 @@
|
|
|
1
|
-
import { createInterface } from
|
|
2
|
-
import { stdin as input, stdout as output } from
|
|
3
|
-
import { createServer } from
|
|
4
|
-
import { randomUUID } from
|
|
5
|
-
import { URL } from
|
|
6
|
-
import open from
|
|
7
|
-
import { brand, logo } from
|
|
1
|
+
import { createInterface } from "node:readline/promises";
|
|
2
|
+
import { stdin as input, stdout as output } from "node:process";
|
|
3
|
+
import { createServer, } from "node:http";
|
|
4
|
+
import { randomUUID } from "node:crypto";
|
|
5
|
+
import { URL } from "node:url";
|
|
6
|
+
import open from "open";
|
|
7
|
+
import { brand, logo } from "../output/theme.js";
|
|
8
|
+
export function validateReturnedScope(value, maximum) {
|
|
9
|
+
if (value !== "read" && value !== "full") {
|
|
10
|
+
throw new Error("The authorization response did not include a valid access level");
|
|
11
|
+
}
|
|
12
|
+
if (maximum === "read" && value === "full") {
|
|
13
|
+
throw new Error("The authorization response exceeds the requested access maximum");
|
|
14
|
+
}
|
|
15
|
+
return value;
|
|
16
|
+
}
|
|
8
17
|
const TIMEOUT_MS = 120_000;
|
|
9
18
|
const CLI_TOKEN_PATTERN = /^fb_cli_[a-f0-9]{64}$/;
|
|
10
19
|
export function isCliTokenFormat(token) {
|
|
@@ -38,30 +47,31 @@ const ERROR_HTML = (message) => `<!DOCTYPE html>
|
|
|
38
47
|
<h1>Authentication Failed</h1>
|
|
39
48
|
<p>${message}</p>
|
|
40
49
|
</div></body></html>`;
|
|
41
|
-
export async function browserLogin(baseUrl, scope =
|
|
50
|
+
export async function browserLogin(baseUrl, scope = "read") {
|
|
42
51
|
const state = randomUUID();
|
|
43
52
|
return new Promise((resolve, reject) => {
|
|
44
53
|
const server = createServer((req, res) => {
|
|
45
|
-
const url = new URL(req.url ??
|
|
46
|
-
if (url.pathname !==
|
|
54
|
+
const url = new URL(req.url ?? "/", `http://127.0.0.1`);
|
|
55
|
+
if (url.pathname !== "/callback") {
|
|
47
56
|
res.writeHead(404);
|
|
48
|
-
res.end(
|
|
57
|
+
res.end("Not found");
|
|
49
58
|
return;
|
|
50
59
|
}
|
|
51
|
-
const receivedState = url.searchParams.get(
|
|
52
|
-
const token = url.searchParams.get(
|
|
53
|
-
const
|
|
60
|
+
const receivedState = url.searchParams.get("state");
|
|
61
|
+
const token = url.searchParams.get("token");
|
|
62
|
+
const returnedScope = url.searchParams.get("scope");
|
|
63
|
+
const error = url.searchParams.get("error");
|
|
54
64
|
// CSRF validation
|
|
55
65
|
if (receivedState !== state) {
|
|
56
|
-
res.writeHead(400, {
|
|
57
|
-
res.end(ERROR_HTML(
|
|
66
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
67
|
+
res.end(ERROR_HTML("State mismatch — possible CSRF attack. Please try again."));
|
|
58
68
|
cleanup();
|
|
59
|
-
reject(new Error(
|
|
69
|
+
reject(new Error("State mismatch during authentication"));
|
|
60
70
|
return;
|
|
61
71
|
}
|
|
62
72
|
// User denied
|
|
63
73
|
if (error) {
|
|
64
|
-
res.writeHead(200, {
|
|
74
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
65
75
|
res.end(ERROR_HTML(`Authorization was denied: ${error}`));
|
|
66
76
|
cleanup();
|
|
67
77
|
reject(new Error(`Authorization denied: ${error}`));
|
|
@@ -69,32 +79,44 @@ export async function browserLogin(baseUrl, scope = 'read') {
|
|
|
69
79
|
}
|
|
70
80
|
// Missing token
|
|
71
81
|
if (!token) {
|
|
72
|
-
res.writeHead(400, {
|
|
73
|
-
res.end(ERROR_HTML(
|
|
82
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
83
|
+
res.end(ERROR_HTML("No token received. Please try again."));
|
|
84
|
+
cleanup();
|
|
85
|
+
reject(new Error("No token received in callback"));
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
let selectedScope;
|
|
89
|
+
try {
|
|
90
|
+
selectedScope = validateReturnedScope(returnedScope, scope);
|
|
91
|
+
}
|
|
92
|
+
catch (cause) {
|
|
93
|
+
const message = cause instanceof Error ? cause.message : "Invalid access level";
|
|
94
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
95
|
+
res.end(ERROR_HTML(message));
|
|
74
96
|
cleanup();
|
|
75
|
-
reject(new Error(
|
|
97
|
+
reject(new Error(message));
|
|
76
98
|
return;
|
|
77
99
|
}
|
|
78
100
|
// Success
|
|
79
|
-
res.writeHead(200, {
|
|
101
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
80
102
|
res.end(SUCCESS_HTML);
|
|
81
103
|
cleanup();
|
|
82
|
-
resolve({ token, scope });
|
|
104
|
+
resolve({ token, scope: selectedScope });
|
|
83
105
|
});
|
|
84
106
|
const timeout = setTimeout(() => {
|
|
85
107
|
cleanup();
|
|
86
|
-
reject(new Error(
|
|
108
|
+
reject(new Error("Authentication timed out after 2 minutes"));
|
|
87
109
|
}, TIMEOUT_MS);
|
|
88
110
|
function cleanup() {
|
|
89
111
|
clearTimeout(timeout);
|
|
90
112
|
server.close();
|
|
91
113
|
}
|
|
92
114
|
// Listen on random port on loopback
|
|
93
|
-
server.listen(0,
|
|
115
|
+
server.listen(0, "127.0.0.1", () => {
|
|
94
116
|
const addr = server.address();
|
|
95
|
-
if (!addr || typeof addr ===
|
|
117
|
+
if (!addr || typeof addr === "string") {
|
|
96
118
|
cleanup();
|
|
97
|
-
reject(new Error(
|
|
119
|
+
reject(new Error("Failed to start local auth server"));
|
|
98
120
|
return;
|
|
99
121
|
}
|
|
100
122
|
const port = addr.port;
|
|
@@ -102,14 +124,14 @@ export async function browserLogin(baseUrl, scope = 'read') {
|
|
|
102
124
|
console.log();
|
|
103
125
|
console.log(` ${logo()} CLI`);
|
|
104
126
|
console.log();
|
|
105
|
-
console.log(` ${brand.primary(
|
|
127
|
+
console.log(` ${brand.primary("Opening browser for authentication...")}`);
|
|
106
128
|
console.log();
|
|
107
|
-
console.log(` ${brand.muted(
|
|
129
|
+
console.log(` ${brand.muted("If the browser doesn't open, visit:")}`);
|
|
108
130
|
console.log(` ${brand.primary(authorizeUrl)}`);
|
|
109
131
|
console.log();
|
|
110
|
-
console.log(` ${brand.muted(
|
|
111
|
-
console.log(` ${brand.muted(
|
|
112
|
-
console.log(` ${brand.command(
|
|
132
|
+
console.log(` ${brand.muted("Waiting for authentication...")}`);
|
|
133
|
+
console.log(` ${brand.muted("If this machine cannot receive the localhost browser callback, use:")}`);
|
|
134
|
+
console.log(` ${brand.command("feedbackbasket login --manual")}`);
|
|
113
135
|
console.log();
|
|
114
136
|
open(authorizeUrl).catch(() => {
|
|
115
137
|
// Browser open failed; user will use the URL manually.
|
|
@@ -117,33 +139,38 @@ export async function browserLogin(baseUrl, scope = 'read') {
|
|
|
117
139
|
});
|
|
118
140
|
});
|
|
119
141
|
}
|
|
120
|
-
export async function manualLogin(baseUrl, scope =
|
|
142
|
+
export async function manualLogin(baseUrl, scope = "read") {
|
|
121
143
|
const authorizeUrl = `${baseUrl}/cli/authorize?mode=manual&scope=${scope}`;
|
|
122
144
|
console.log();
|
|
123
145
|
console.log(` ${logo()} CLI`);
|
|
124
146
|
console.log();
|
|
125
|
-
console.log(` ${brand.primary(
|
|
147
|
+
console.log(` ${brand.primary("Open this URL on any machine with a browser:")}`);
|
|
126
148
|
console.log();
|
|
127
149
|
console.log(` ${brand.primary(authorizeUrl)}`);
|
|
128
150
|
console.log();
|
|
129
|
-
console.log(` ${brand.muted(
|
|
130
|
-
console.log(` ${brand.muted(
|
|
151
|
+
console.log(` ${brand.muted("Use this when a remote server cannot receive the localhost browser callback.")}`);
|
|
152
|
+
console.log(` ${brand.muted("The CLI still needs outbound HTTPS access to verify and use the token.")}`);
|
|
131
153
|
console.log();
|
|
132
|
-
console.log(` ${brand.muted(
|
|
154
|
+
console.log(` ${brand.muted("After approving access, paste the token shown in your browser.")}`);
|
|
133
155
|
console.log();
|
|
134
156
|
open(authorizeUrl).catch(() => {
|
|
135
157
|
// Browser open failed; user will use the URL manually.
|
|
136
158
|
});
|
|
137
159
|
const rl = createInterface({ input, output });
|
|
138
160
|
try {
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
161
|
+
const rawResult = (await rl.question(" Paste authorization result: ")).trim();
|
|
162
|
+
let parsed;
|
|
163
|
+
try {
|
|
164
|
+
parsed = JSON.parse(rawResult);
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
throw new Error("Expected the authorization result copied from the browser");
|
|
142
168
|
}
|
|
143
|
-
if (!isCliTokenFormat(token)) {
|
|
144
|
-
throw new Error(
|
|
169
|
+
if (typeof parsed.token !== "string" || !isCliTokenFormat(parsed.token)) {
|
|
170
|
+
throw new Error("Expected a FeedbackBasket CLI token beginning with fb_cli_");
|
|
145
171
|
}
|
|
146
|
-
|
|
172
|
+
const selectedScope = validateReturnedScope(typeof parsed.scope === "string" ? parsed.scope : null, scope);
|
|
173
|
+
return { token: parsed.token, scope: selectedScope };
|
|
147
174
|
}
|
|
148
175
|
finally {
|
|
149
176
|
rl.close();
|
|
@@ -4,6 +4,10 @@ export declare const CLI_CAPABILITIES: readonly {
|
|
|
4
4
|
commands: readonly string[];
|
|
5
5
|
}[];
|
|
6
6
|
export declare const CLI_EXEMPTIONS: ({
|
|
7
|
+
readonly id: "browser-oauth";
|
|
8
|
+
readonly surface: "mcp-http";
|
|
9
|
+
readonly reason: "Browser OAuth is a Streamable HTTP transport capability; STDIO uses environment credentials and the CLI keeps its own login flow.";
|
|
10
|
+
} | {
|
|
7
11
|
readonly id: "auth";
|
|
8
12
|
readonly surface: "cli";
|
|
9
13
|
readonly reason: "Authentication is local CLI credential management, not a product operation.";
|
|
@@ -1,6 +1,12 @@
|
|
|
1
|
-
import { Command } from
|
|
2
|
-
import type { OutputWriter } from
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import type { OutputWriter } from "../output/writer.js";
|
|
3
|
+
export declare function authLoginResult(email: string | undefined, scope: "read" | "full", defaultProject: string | undefined): {
|
|
4
|
+
authenticated: boolean;
|
|
5
|
+
email: string | undefined;
|
|
6
|
+
scope: "read" | "full";
|
|
7
|
+
defaultProject: string | undefined;
|
|
8
|
+
};
|
|
3
9
|
export declare function createAuthCommand(getWriter: () => OutputWriter): Command;
|
|
4
|
-
export declare function resolveAuthScope(value: string):
|
|
10
|
+
export declare function resolveAuthScope(value: string): "read" | "full";
|
|
5
11
|
export declare function createLoginCommand(getWriter: () => OutputWriter): Command;
|
|
6
12
|
export declare function createLogoutCommand(getWriter: () => OutputWriter): Command;
|
|
@@ -1,25 +1,27 @@
|
|
|
1
|
-
import { Command } from
|
|
2
|
-
import { existsSync } from
|
|
3
|
-
import { join } from
|
|
4
|
-
import { homedir } from
|
|
5
|
-
import { AuthManager } from
|
|
6
|
-
import { browserLogin, isCliTokenFormat, manualLogin } from
|
|
7
|
-
import { saveCredentials } from
|
|
8
|
-
import { loadConfig, saveConfig } from
|
|
9
|
-
import { FeedbackBasketClient } from
|
|
10
|
-
import { errAuth, errUsage } from
|
|
11
|
-
import { brand, divider, logo } from
|
|
12
|
-
import { select, confirm } from
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { AuthManager } from "../auth/manager.js";
|
|
6
|
+
import { browserLogin, isCliTokenFormat, manualLogin } from "../auth/login.js";
|
|
7
|
+
import { saveCredentials } from "../config/credentials.js";
|
|
8
|
+
import { loadConfig, saveConfig } from "../config/config.js";
|
|
9
|
+
import { FeedbackBasketClient } from "../client.js";
|
|
10
|
+
import { errAuth, errUsage } from "../output/errors.js";
|
|
11
|
+
import { brand, divider, logo } from "../output/theme.js";
|
|
12
|
+
import { select, confirm } from "../prompt.js";
|
|
13
|
+
export function authLoginResult(email, scope, defaultProject) {
|
|
14
|
+
return { authenticated: true, email, scope, defaultProject };
|
|
15
|
+
}
|
|
13
16
|
export function createAuthCommand(getWriter) {
|
|
14
|
-
const auth = new Command(
|
|
15
|
-
.description('Manage authentication');
|
|
17
|
+
const auth = new Command("auth").description("Manage authentication");
|
|
16
18
|
// --- auth login ---
|
|
17
19
|
auth
|
|
18
|
-
.command(
|
|
19
|
-
.description(
|
|
20
|
-
.option(
|
|
21
|
-
.option(
|
|
22
|
-
.option(
|
|
20
|
+
.command("login")
|
|
21
|
+
.description("Authenticate with FeedbackBasket")
|
|
22
|
+
.option("--token <token>", "Use an API token directly (for CI/headless)")
|
|
23
|
+
.option("--manual", "Authenticate without a localhost browser callback")
|
|
24
|
+
.option("--scope <scope>", "Maximum browser access: read or full", "full")
|
|
23
25
|
.action(async (opts) => {
|
|
24
26
|
const writer = getWriter();
|
|
25
27
|
const config = loadConfig();
|
|
@@ -34,19 +36,22 @@ export function createAuthCommand(getWriter) {
|
|
|
34
36
|
console.log();
|
|
35
37
|
}
|
|
36
38
|
let token;
|
|
39
|
+
let selectedScope = scope;
|
|
37
40
|
if (opts.token) {
|
|
38
41
|
token = opts.token;
|
|
39
42
|
}
|
|
40
43
|
else if (opts.manual) {
|
|
41
44
|
const result = await manualLogin(config.baseUrl, scope);
|
|
42
45
|
token = result.token;
|
|
46
|
+
selectedScope = result.scope;
|
|
43
47
|
}
|
|
44
48
|
else {
|
|
45
49
|
const result = await browserLogin(config.baseUrl, scope);
|
|
46
50
|
token = result.token;
|
|
51
|
+
selectedScope = result.scope;
|
|
47
52
|
}
|
|
48
53
|
if (!isCliTokenFormat(token)) {
|
|
49
|
-
throw errUsage(
|
|
54
|
+
throw errUsage("Expected a FeedbackBasket CLI token beginning with fb_cli_", "MCP API keys begin with fb_key_ and are only for MCP server configuration");
|
|
50
55
|
}
|
|
51
56
|
// Verify token + get profile
|
|
52
57
|
const client = new FeedbackBasketClient(token, config.baseUrl);
|
|
@@ -63,22 +68,22 @@ export function createAuthCommand(getWriter) {
|
|
|
63
68
|
if (isInteractive) {
|
|
64
69
|
const msg = err instanceof Error ? err.message : String(err);
|
|
65
70
|
console.log(brand.warning(` Could not verify token: ${msg}`));
|
|
66
|
-
console.log(brand.muted(
|
|
71
|
+
console.log(brand.muted(" Token saved — run feedbackbasket doctor to diagnose"));
|
|
67
72
|
console.log();
|
|
68
73
|
}
|
|
69
74
|
}
|
|
70
75
|
saveCredentials({
|
|
71
76
|
token,
|
|
72
|
-
scope,
|
|
77
|
+
scope: selectedScope,
|
|
73
78
|
userId,
|
|
74
79
|
email,
|
|
75
80
|
organizationId,
|
|
76
81
|
createdAt: new Date().toISOString(),
|
|
77
82
|
});
|
|
78
83
|
if (isInteractive) {
|
|
79
|
-
console.log(` ${brand.success(
|
|
84
|
+
console.log(` ${brand.success("✓")} Authentication successful`);
|
|
80
85
|
if (email)
|
|
81
|
-
console.log(` ${brand.muted(
|
|
86
|
+
console.log(` ${brand.muted("Logged in as")} ${brand.bold(email)}`);
|
|
82
87
|
console.log();
|
|
83
88
|
}
|
|
84
89
|
// ── Step 2: Default Project (interactive only, first login) ──
|
|
@@ -91,7 +96,7 @@ export function createAuthCommand(getWriter) {
|
|
|
91
96
|
const projectsRes = await client.listProjects();
|
|
92
97
|
const projects = projectsRes.projects;
|
|
93
98
|
if (projects.length === 0) {
|
|
94
|
-
console.log(brand.muted(
|
|
99
|
+
console.log(brand.muted(" No projects found. Create one from the dashboard."));
|
|
95
100
|
console.log();
|
|
96
101
|
}
|
|
97
102
|
else if (projects.length === 1) {
|
|
@@ -100,13 +105,13 @@ export function createAuthCommand(getWriter) {
|
|
|
100
105
|
config.defaultProject = p.id;
|
|
101
106
|
saveConfig(config);
|
|
102
107
|
defaultProjectName = p.name;
|
|
103
|
-
console.log(` ${brand.success(
|
|
104
|
-
console.log(` ${brand.muted(
|
|
108
|
+
console.log(` ${brand.success("✓")} Default project: ${brand.bold(p.name)}`);
|
|
109
|
+
console.log(` ${brand.muted("Only project in your organization")}`);
|
|
105
110
|
console.log();
|
|
106
111
|
}
|
|
107
112
|
else {
|
|
108
113
|
console.log(` You have ${brand.bold(String(projects.length))} projects:\n`);
|
|
109
|
-
const choice = await select(
|
|
114
|
+
const choice = await select("Select default project", projects.map((p) => {
|
|
110
115
|
const count = brand.muted(`(${p.totalFeedback} feedback)`);
|
|
111
116
|
return `${p.name} ${count}`;
|
|
112
117
|
}));
|
|
@@ -116,12 +121,12 @@ export function createAuthCommand(getWriter) {
|
|
|
116
121
|
saveConfig(config);
|
|
117
122
|
defaultProjectName = p.name;
|
|
118
123
|
console.log();
|
|
119
|
-
console.log(` ${brand.success(
|
|
120
|
-
console.log(` ${brand.muted(
|
|
124
|
+
console.log(` ${brand.success("✓")} Default project: ${brand.bold(p.name)}`);
|
|
125
|
+
console.log(` ${brand.muted("Use --project to override per-command")}`);
|
|
121
126
|
}
|
|
122
127
|
else {
|
|
123
128
|
console.log();
|
|
124
|
-
console.log(` ${brand.muted(
|
|
129
|
+
console.log(` ${brand.muted("Skipped. Use --project or: feedbackbasket config set defaultProject <id>")}`);
|
|
125
130
|
}
|
|
126
131
|
console.log();
|
|
127
132
|
}
|
|
@@ -129,55 +134,55 @@ export function createAuthCommand(getWriter) {
|
|
|
129
134
|
catch (err) {
|
|
130
135
|
const msg = err instanceof Error ? err.message : String(err);
|
|
131
136
|
console.log(brand.muted(` Could not fetch projects: ${msg}`));
|
|
132
|
-
console.log(brand.muted(
|
|
137
|
+
console.log(brand.muted(" You can set a default later: feedbackbasket config set defaultProject <id>"));
|
|
133
138
|
console.log();
|
|
134
139
|
}
|
|
135
140
|
}
|
|
136
141
|
// ── Step 3: Agent Setup (interactive only) ──
|
|
137
142
|
let agentInstalled = false;
|
|
138
143
|
if (isInteractive) {
|
|
139
|
-
const claudeDir = join(homedir(),
|
|
140
|
-
const skillPath = join(claudeDir,
|
|
144
|
+
const claudeDir = join(homedir(), ".claude");
|
|
145
|
+
const skillPath = join(claudeDir, "skills", "feedbackbasket", "SKILL.md");
|
|
141
146
|
const claudeExists = existsSync(claudeDir);
|
|
142
147
|
const skillExists = existsSync(skillPath);
|
|
143
148
|
if (claudeExists && !skillExists) {
|
|
144
149
|
console.log();
|
|
145
150
|
console.log(brand.muted(` Step 3: Agent Setup`));
|
|
146
151
|
console.log();
|
|
147
|
-
console.log(` ${brand.primary(
|
|
152
|
+
console.log(` ${brand.primary("Detected:")} Claude Code`);
|
|
148
153
|
console.log();
|
|
149
154
|
console.log(` This will:`);
|
|
150
155
|
console.log(` 1. Install FeedbackBasket skill to ~/.claude/skills/`);
|
|
151
156
|
console.log();
|
|
152
|
-
const doSetup = await confirm(
|
|
157
|
+
const doSetup = await confirm(" Set up for Claude Code?");
|
|
153
158
|
if (doSetup) {
|
|
154
159
|
try {
|
|
155
|
-
const { mkdirSync, copyFileSync } = await import(
|
|
156
|
-
const { dirname } = await import(
|
|
157
|
-
const { fileURLToPath } = await import(
|
|
160
|
+
const { mkdirSync, copyFileSync } = await import("node:fs");
|
|
161
|
+
const { dirname } = await import("node:path");
|
|
162
|
+
const { fileURLToPath } = await import("node:url");
|
|
158
163
|
const thisDir = dirname(fileURLToPath(import.meta.url));
|
|
159
|
-
const projectRoot = join(thisDir,
|
|
160
|
-
let skillSrc = join(projectRoot,
|
|
164
|
+
const projectRoot = join(thisDir, "..", "..", "..");
|
|
165
|
+
let skillSrc = join(projectRoot, "skills", "feedbackbasket", "SKILL.md");
|
|
161
166
|
if (!existsSync(skillSrc)) {
|
|
162
|
-
skillSrc = join(thisDir,
|
|
167
|
+
skillSrc = join(thisDir, "..", "..", "skills", "feedbackbasket", "SKILL.md");
|
|
163
168
|
}
|
|
164
169
|
if (existsSync(skillSrc)) {
|
|
165
|
-
const skillDir = join(claudeDir,
|
|
170
|
+
const skillDir = join(claudeDir, "skills", "feedbackbasket");
|
|
166
171
|
mkdirSync(skillDir, { recursive: true });
|
|
167
172
|
copyFileSync(skillSrc, skillPath);
|
|
168
173
|
agentInstalled = true;
|
|
169
|
-
console.log(` ${brand.success(
|
|
174
|
+
console.log(` ${brand.success("✓")} Agent skill installed`);
|
|
170
175
|
}
|
|
171
176
|
else {
|
|
172
|
-
console.log(` ${brand.error(
|
|
177
|
+
console.log(` ${brand.error("✗")} SKILL.md not found in package`);
|
|
173
178
|
}
|
|
174
179
|
}
|
|
175
180
|
catch {
|
|
176
|
-
console.log(` ${brand.error(
|
|
181
|
+
console.log(` ${brand.error("✗")} Failed to install skill`);
|
|
177
182
|
}
|
|
178
183
|
}
|
|
179
184
|
else {
|
|
180
|
-
console.log(brand.muted(
|
|
185
|
+
console.log(brand.muted(" Skipped. Run later: feedbackbasket setup claude"));
|
|
181
186
|
}
|
|
182
187
|
console.log();
|
|
183
188
|
}
|
|
@@ -189,78 +194,80 @@ export function createAuthCommand(getWriter) {
|
|
|
189
194
|
if (isInteractive) {
|
|
190
195
|
console.log();
|
|
191
196
|
console.log(divider(35));
|
|
192
|
-
console.log(brand.bold(
|
|
197
|
+
console.log(brand.bold(" Setup complete!"));
|
|
193
198
|
console.log(divider(35));
|
|
194
199
|
console.log();
|
|
195
|
-
console.log(` ${brand.success(
|
|
200
|
+
console.log(` ${brand.success("✓")} Authenticated${email ? ` as ${email}` : ""}`);
|
|
196
201
|
if (defaultProjectName) {
|
|
197
|
-
console.log(` ${brand.success(
|
|
202
|
+
console.log(` ${brand.success("✓")} Default project: ${defaultProjectName}`);
|
|
198
203
|
}
|
|
199
204
|
else if (config.defaultProject) {
|
|
200
|
-
console.log(` ${brand.success(
|
|
205
|
+
console.log(` ${brand.success("✓")} Default project: ${config.defaultProject}`);
|
|
201
206
|
}
|
|
202
207
|
else {
|
|
203
|
-
console.log(` ${brand.muted(
|
|
208
|
+
console.log(` ${brand.muted("-")} No default project`);
|
|
204
209
|
}
|
|
205
210
|
if (agentInstalled) {
|
|
206
|
-
console.log(` ${brand.success(
|
|
211
|
+
console.log(` ${brand.success("✓")} Claude Code skill`);
|
|
207
212
|
}
|
|
208
213
|
console.log();
|
|
209
|
-
console.log(
|
|
214
|
+
console.log(" Try these commands:");
|
|
210
215
|
console.log();
|
|
211
|
-
console.log(` ${brand.command(
|
|
212
|
-
console.log(` ${brand.command(
|
|
213
|
-
console.log(` ${brand.command(
|
|
214
|
-
console.log(` ${brand.command(
|
|
216
|
+
console.log(` ${brand.command("feedbackbasket projects list")} List your projects`);
|
|
217
|
+
console.log(` ${brand.command("feedbackbasket feedback list")} View recent feedback`);
|
|
218
|
+
console.log(` ${brand.command("feedbackbasket bugs list")} View bug reports`);
|
|
219
|
+
console.log(` ${brand.command("feedbackbasket doctor")} Run diagnostics`);
|
|
215
220
|
console.log();
|
|
216
221
|
return; // Skip the JSON output in interactive wizard mode
|
|
217
222
|
}
|
|
218
223
|
// Machine output
|
|
219
|
-
writer.ok(
|
|
220
|
-
summary: email
|
|
224
|
+
writer.ok(authLoginResult(email, selectedScope, config.defaultProject), {
|
|
225
|
+
summary: email
|
|
226
|
+
? `Logged in as ${email}`
|
|
227
|
+
: "Authenticated successfully",
|
|
221
228
|
breadcrumbs: [
|
|
222
|
-
{ action:
|
|
223
|
-
{ action:
|
|
224
|
-
{ action:
|
|
229
|
+
{ action: "List projects", cmd: "feedbackbasket projects list" },
|
|
230
|
+
{ action: "Check status", cmd: "feedbackbasket auth status" },
|
|
231
|
+
{ action: "Run diagnostics", cmd: "feedbackbasket doctor" },
|
|
225
232
|
],
|
|
226
233
|
});
|
|
227
234
|
});
|
|
228
235
|
// --- auth logout ---
|
|
229
236
|
auth
|
|
230
|
-
.command(
|
|
231
|
-
.description(
|
|
237
|
+
.command("logout")
|
|
238
|
+
.description("Clear stored credentials")
|
|
232
239
|
.action(() => {
|
|
233
240
|
const writer = getWriter();
|
|
234
241
|
const manager = new AuthManager();
|
|
235
242
|
if (!manager.isAuthenticated()) {
|
|
236
|
-
writer.ok({ authenticated: false }, { summary:
|
|
243
|
+
writer.ok({ authenticated: false }, { summary: "Already logged out" });
|
|
237
244
|
return;
|
|
238
245
|
}
|
|
239
|
-
if (manager.getSource() ===
|
|
240
|
-
throw errUsage(
|
|
246
|
+
if (manager.getSource() === "env") {
|
|
247
|
+
throw errUsage("Credentials are set via FEEDBACKBASKET_TOKEN environment variable", "Unset the variable: unset FEEDBACKBASKET_TOKEN");
|
|
241
248
|
}
|
|
242
249
|
manager.logout();
|
|
243
250
|
if (!writer.isMachineOutput()) {
|
|
244
|
-
console.log(` ${brand.success(
|
|
251
|
+
console.log(` ${brand.success("✓")} Logged out`);
|
|
245
252
|
console.log();
|
|
246
253
|
}
|
|
247
254
|
writer.ok({ authenticated: false }, {
|
|
248
|
-
summary:
|
|
255
|
+
summary: "Logged out",
|
|
249
256
|
breadcrumbs: [
|
|
250
|
-
{ action:
|
|
257
|
+
{ action: "Log back in", cmd: "feedbackbasket auth login" },
|
|
251
258
|
],
|
|
252
259
|
});
|
|
253
260
|
});
|
|
254
261
|
// --- auth status ---
|
|
255
262
|
auth
|
|
256
|
-
.command(
|
|
257
|
-
.description(
|
|
263
|
+
.command("status")
|
|
264
|
+
.description("Show authentication status")
|
|
258
265
|
.action(() => {
|
|
259
266
|
const writer = getWriter();
|
|
260
267
|
const manager = new AuthManager();
|
|
261
268
|
const creds = manager.getCredentials();
|
|
262
269
|
if (!creds) {
|
|
263
|
-
throw errAuth(
|
|
270
|
+
throw errAuth("Not authenticated");
|
|
264
271
|
}
|
|
265
272
|
const config = loadConfig();
|
|
266
273
|
const data = {
|
|
@@ -273,57 +280,57 @@ export function createAuthCommand(getWriter) {
|
|
|
273
280
|
defaultProject: config.defaultProject ?? null,
|
|
274
281
|
};
|
|
275
282
|
writer.ok(data, {
|
|
276
|
-
summary: `Authenticated${creds.email ? ` as ${creds.email}` :
|
|
283
|
+
summary: `Authenticated${creds.email ? ` as ${creds.email}` : ""}`,
|
|
277
284
|
breadcrumbs: [
|
|
278
|
-
{ action:
|
|
279
|
-
{ action:
|
|
285
|
+
{ action: "List projects", cmd: "feedbackbasket projects list" },
|
|
286
|
+
{ action: "Log out", cmd: "feedbackbasket auth logout" },
|
|
280
287
|
],
|
|
281
288
|
});
|
|
282
289
|
});
|
|
283
290
|
// --- auth token ---
|
|
284
291
|
auth
|
|
285
|
-
.command(
|
|
286
|
-
.description(
|
|
292
|
+
.command("token")
|
|
293
|
+
.description("Print the current access token (for scripting)")
|
|
287
294
|
.action(() => {
|
|
288
295
|
const manager = new AuthManager();
|
|
289
296
|
const token = manager.resolveToken();
|
|
290
297
|
if (!token) {
|
|
291
|
-
throw errAuth(
|
|
298
|
+
throw errAuth("Not authenticated");
|
|
292
299
|
}
|
|
293
300
|
process.stdout.write(token);
|
|
294
301
|
});
|
|
295
302
|
return auth;
|
|
296
303
|
}
|
|
297
304
|
export function resolveAuthScope(value) {
|
|
298
|
-
if (value ===
|
|
305
|
+
if (value === "read" || value === "full")
|
|
299
306
|
return value;
|
|
300
307
|
throw errUsage('Scope must be "read" or "full"');
|
|
301
308
|
}
|
|
302
309
|
// Top-level aliases: `feedbackbasket login` and `feedbackbasket logout`
|
|
303
310
|
export function createLoginCommand(getWriter) {
|
|
304
|
-
return new Command(
|
|
305
|
-
.description(
|
|
306
|
-
.option(
|
|
307
|
-
.option(
|
|
308
|
-
.option(
|
|
311
|
+
return new Command("login")
|
|
312
|
+
.description("Authenticate with FeedbackBasket (alias for auth login)")
|
|
313
|
+
.option("--token <token>", "Use an API token directly (for CI/headless)")
|
|
314
|
+
.option("--manual", "Authenticate without a localhost browser callback")
|
|
315
|
+
.option("--scope <scope>", "Maximum browser access: read or full", "full")
|
|
309
316
|
.action(async (opts) => {
|
|
310
317
|
// Delegate to auth login by re-parsing
|
|
311
318
|
const authCmd = createAuthCommand(getWriter);
|
|
312
|
-
const args = [
|
|
319
|
+
const args = ["node", "feedbackbasket", "login"];
|
|
313
320
|
if (opts.token)
|
|
314
|
-
args.push(
|
|
321
|
+
args.push("--token", opts.token);
|
|
315
322
|
if (opts.manual)
|
|
316
|
-
args.push(
|
|
323
|
+
args.push("--manual");
|
|
317
324
|
if (opts.scope)
|
|
318
|
-
args.push(
|
|
325
|
+
args.push("--scope", opts.scope);
|
|
319
326
|
await authCmd.parseAsync(args);
|
|
320
327
|
});
|
|
321
328
|
}
|
|
322
329
|
export function createLogoutCommand(getWriter) {
|
|
323
|
-
return new Command(
|
|
324
|
-
.description(
|
|
330
|
+
return new Command("logout")
|
|
331
|
+
.description("Clear stored credentials (alias for auth logout)")
|
|
325
332
|
.action(async () => {
|
|
326
333
|
const authCmd = createAuthCommand(getWriter);
|
|
327
|
-
await authCmd.parseAsync([
|
|
334
|
+
await authCmd.parseAsync(["node", "feedbackbasket", "logout"]);
|
|
328
335
|
});
|
|
329
336
|
}
|