@ollie-shop/cli 0.1.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.
- package/.turbo/turbo-build.log +13 -0
- package/README.md +15 -0
- package/dist/index.js +236 -0
- package/package.json +22 -0
- package/src/commands/index.ts +13 -0
- package/src/commands/login.ts +340 -0
- package/src/index.ts +21 -0
- package/src/utils/constants.ts +7 -0
- package/tsconfig.json +20 -0
- package/tsup.config.ts +15 -0
- package/vitest.config.ts +7 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
|
|
2
|
+
> @ollie-shop/cli@0.1.1 build /home/runner/work/ollie-shop/ollie-shop/packages/cli
|
|
3
|
+
> tsup
|
|
4
|
+
|
|
5
|
+
[34mCLI[39m Building entry: src/index.ts
|
|
6
|
+
[34mCLI[39m Using tsconfig: tsconfig.json
|
|
7
|
+
[34mCLI[39m tsup v8.5.0
|
|
8
|
+
[34mCLI[39m Using tsup config: /home/runner/work/ollie-shop/ollie-shop/packages/cli/tsup.config.ts
|
|
9
|
+
[34mCLI[39m Target: esnext
|
|
10
|
+
[34mCLI[39m Cleaning output folder
|
|
11
|
+
[34mCJS[39m Build start
|
|
12
|
+
[32mCJS[39m [1mdist/index.js [22m[32m7.47 KB[39m
|
|
13
|
+
[32mCJS[39m ⚡️ Build success in 470ms
|
package/README.md
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Ollie Shop CLI [WIP]
|
|
2
|
+
|
|
3
|
+
[](https://opensource.org/licenses/MIT)
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
`ollie-shop` is a development dependency package that provides a command-line interface (CLI) for local development of Ollie Shop templates and components. This package is currently a work in progress (WIP) and aims to streamline the development workflow for Ollie Shop projects.
|
|
8
|
+
|
|
9
|
+
## Contributing
|
|
10
|
+
|
|
11
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
12
|
+
|
|
13
|
+
## License
|
|
14
|
+
|
|
15
|
+
This project is licensed under the MIT License - see the LICENSE file for details.
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
var commander = require('commander');
|
|
5
|
+
var child_process = require('child_process');
|
|
6
|
+
var crypto = require('crypto');
|
|
7
|
+
var fs = require('fs/promises');
|
|
8
|
+
var http = require('http');
|
|
9
|
+
var os = require('os');
|
|
10
|
+
var path = require('path');
|
|
11
|
+
|
|
12
|
+
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
13
|
+
|
|
14
|
+
var fs__default = /*#__PURE__*/_interopDefault(fs);
|
|
15
|
+
var path__default = /*#__PURE__*/_interopDefault(path);
|
|
16
|
+
|
|
17
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
18
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
19
|
+
}) : x)(function(x) {
|
|
20
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
21
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
22
|
+
});
|
|
23
|
+
var DEFAULT_CALLBACK_PORT = 7777;
|
|
24
|
+
var AUTH_ENDPOINT = "https://admin.ollie.shop/auth/login";
|
|
25
|
+
function configureLoginCommand(program) {
|
|
26
|
+
return program.command("login").description("Log in to your Ollie Shop account").option(
|
|
27
|
+
"-p, --port <port>",
|
|
28
|
+
"Port to use for the local callback server",
|
|
29
|
+
DEFAULT_CALLBACK_PORT.toString()
|
|
30
|
+
).option("--auth-url <url>", "Custom authorization URL", AUTH_ENDPOINT).action(async (options) => {
|
|
31
|
+
console.log("\u{1F510} Initiating Ollie Shop login flow...");
|
|
32
|
+
try {
|
|
33
|
+
const token = await startWebAuthFlow(options);
|
|
34
|
+
if (token) {
|
|
35
|
+
await saveCredentials(token);
|
|
36
|
+
console.log("\u2705 Successfully logged in!");
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
console.error("\u274C Authentication failed. Please try again.");
|
|
40
|
+
process.exit(1);
|
|
41
|
+
} catch (error) {
|
|
42
|
+
console.error(
|
|
43
|
+
`\u274C Login failed: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
44
|
+
);
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
async function handleAuthCallback(req, res, state, resolve, reject) {
|
|
50
|
+
const socket = req.socket;
|
|
51
|
+
const url = new URL(
|
|
52
|
+
req.url || "/",
|
|
53
|
+
`http://localhost:${socket.localPort || 3e3}`
|
|
54
|
+
);
|
|
55
|
+
const params = url.searchParams;
|
|
56
|
+
const returnedState = params.get("state");
|
|
57
|
+
if (returnedState !== state) {
|
|
58
|
+
sendErrorResponse(
|
|
59
|
+
res,
|
|
60
|
+
400,
|
|
61
|
+
"Invalid state parameter",
|
|
62
|
+
"Authentication failed. Please try again."
|
|
63
|
+
);
|
|
64
|
+
reject(new Error("Invalid state parameter"));
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
let formData = "";
|
|
68
|
+
req.on("data", (chunk) => {
|
|
69
|
+
formData += chunk.toString();
|
|
70
|
+
});
|
|
71
|
+
await new Promise((formResolve) => {
|
|
72
|
+
req.on("end", () => {
|
|
73
|
+
formResolve();
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
const formParams = new URLSearchParams(formData);
|
|
77
|
+
const accessToken = formParams.get("access_token");
|
|
78
|
+
const refreshToken = formParams.get("refresh_token") || "";
|
|
79
|
+
const expiresAt = formParams.get("expires_at") || new Date(Date.now() + 36e5).toISOString();
|
|
80
|
+
if (!accessToken) {
|
|
81
|
+
sendErrorResponse(
|
|
82
|
+
res,
|
|
83
|
+
400,
|
|
84
|
+
"Missing authentication token",
|
|
85
|
+
"Authentication failed. Please try again."
|
|
86
|
+
);
|
|
87
|
+
reject(new Error("Missing authentication token"));
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
try {
|
|
91
|
+
const token = {
|
|
92
|
+
accessToken,
|
|
93
|
+
refreshToken,
|
|
94
|
+
expiresAt
|
|
95
|
+
};
|
|
96
|
+
sendSuccessResponse(res);
|
|
97
|
+
resolve(token);
|
|
98
|
+
} catch (error) {
|
|
99
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
100
|
+
sendErrorResponse(
|
|
101
|
+
res,
|
|
102
|
+
500,
|
|
103
|
+
"Authentication failed",
|
|
104
|
+
`Error: ${errorMessage}`
|
|
105
|
+
);
|
|
106
|
+
reject(new Error(errorMessage));
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
function sendErrorResponse(res, statusCode, title, message) {
|
|
110
|
+
res.writeHead(statusCode, { "Content-Type": "text/html" });
|
|
111
|
+
res.end(`<h1>${title}</h1><p>${message}</p>`);
|
|
112
|
+
}
|
|
113
|
+
function sendSuccessResponse(res) {
|
|
114
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
115
|
+
res.end(
|
|
116
|
+
"<h1>Authentication successful!</h1><p>You can now close this window and return to the CLI.</p>"
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
function sendWaitingResponse(res) {
|
|
120
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
121
|
+
res.end(
|
|
122
|
+
"<h1>Ollie Shop CLI Authentication</h1><p>Waiting for authentication response...</p>"
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
async function startWebAuthFlow(options) {
|
|
126
|
+
const state = crypto.randomBytes(16).toString("hex");
|
|
127
|
+
const port = Number.parseInt(options.port, 10);
|
|
128
|
+
const baseUrl = options.authUrl;
|
|
129
|
+
return new Promise((resolve, reject) => {
|
|
130
|
+
const server = http.createServer(async (req, res) => {
|
|
131
|
+
try {
|
|
132
|
+
const url = new URL(req.url || "/", `http://localhost:${port}`);
|
|
133
|
+
if (url.pathname === "/callback") {
|
|
134
|
+
await handleAuthCallback(
|
|
135
|
+
req,
|
|
136
|
+
res,
|
|
137
|
+
state,
|
|
138
|
+
(token) => {
|
|
139
|
+
server.close(() => {
|
|
140
|
+
console.log("\u{1F510} Local authentication server closed");
|
|
141
|
+
resolve(token);
|
|
142
|
+
});
|
|
143
|
+
},
|
|
144
|
+
(error) => {
|
|
145
|
+
server.close(() => {
|
|
146
|
+
console.log("\u{1F510} Local authentication server closed");
|
|
147
|
+
reject(error);
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
);
|
|
151
|
+
} else {
|
|
152
|
+
sendWaitingResponse(res);
|
|
153
|
+
}
|
|
154
|
+
} catch (error) {
|
|
155
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
156
|
+
sendErrorResponse(res, 500, "Server Error", errorMessage);
|
|
157
|
+
server.close(() => {
|
|
158
|
+
console.log("\u{1F510} Local authentication server closed");
|
|
159
|
+
reject(new Error(errorMessage));
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
server.listen(port, () => {
|
|
164
|
+
const redirectUrl = `http://localhost:${port}/callback`;
|
|
165
|
+
const authUrl = new URL(baseUrl);
|
|
166
|
+
authUrl.searchParams.set("flow", "cli");
|
|
167
|
+
authUrl.searchParams.set("state", state);
|
|
168
|
+
authUrl.searchParams.set("redirect_to", redirectUrl);
|
|
169
|
+
console.log("\n\u{1F512} Please authenticate in your browser...\n");
|
|
170
|
+
console.log(`Opening: ${authUrl}
|
|
171
|
+
`);
|
|
172
|
+
openBrowser(authUrl.toString());
|
|
173
|
+
});
|
|
174
|
+
server.on("error", (err) => {
|
|
175
|
+
if (err.code === "EADDRINUSE") {
|
|
176
|
+
reject(
|
|
177
|
+
new Error(
|
|
178
|
+
`Port ${port} is already in use. Please specify a different port using the --port option.`
|
|
179
|
+
)
|
|
180
|
+
);
|
|
181
|
+
} else {
|
|
182
|
+
reject(err);
|
|
183
|
+
}
|
|
184
|
+
server.close();
|
|
185
|
+
});
|
|
186
|
+
const timeoutId = setTimeout(
|
|
187
|
+
() => {
|
|
188
|
+
server.close(() => {
|
|
189
|
+
console.log("\u{1F510} Local authentication server closed due to timeout");
|
|
190
|
+
reject(new Error("Authentication timed out. Please try again."));
|
|
191
|
+
});
|
|
192
|
+
},
|
|
193
|
+
5 * 60 * 1e3
|
|
194
|
+
);
|
|
195
|
+
server.on("close", () => {
|
|
196
|
+
clearTimeout(timeoutId);
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
function openBrowser(url) {
|
|
201
|
+
const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
202
|
+
child_process.spawn(command, [url], { detached: true }).unref();
|
|
203
|
+
}
|
|
204
|
+
async function saveCredentials(token) {
|
|
205
|
+
console.log("Saving credentials...");
|
|
206
|
+
const configDir = path__default.default.join(os.homedir(), ".ollie-shop");
|
|
207
|
+
const credentialsPath = path__default.default.join(configDir, "credentials.json");
|
|
208
|
+
try {
|
|
209
|
+
await fs__default.default.mkdir(configDir, { recursive: true });
|
|
210
|
+
} catch (error) {
|
|
211
|
+
if (!(error instanceof Error && "code" in error && error.code === "EEXIST")) {
|
|
212
|
+
throw error;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
await fs__default.default.writeFile(credentialsPath, JSON.stringify(token, null, 2));
|
|
216
|
+
return true;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// src/commands/index.ts
|
|
220
|
+
function registerCommands(program) {
|
|
221
|
+
configureLoginCommand(program);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// src/index.ts
|
|
225
|
+
function createProgram() {
|
|
226
|
+
const program = new commander.Command();
|
|
227
|
+
program.name("ollie").description("Ollie Shop CLI tools").version("0.1.0");
|
|
228
|
+
registerCommands(program);
|
|
229
|
+
return program;
|
|
230
|
+
}
|
|
231
|
+
if (__require.main === module) {
|
|
232
|
+
const program = createProgram();
|
|
233
|
+
program.parse();
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
exports.createProgram = createProgram;
|
package/package.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ollie-shop/cli",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "CLI tools for Ollie Shop",
|
|
5
|
+
"bin": {
|
|
6
|
+
"ollie-shop": "./dist/index.js"
|
|
7
|
+
},
|
|
8
|
+
"dependencies": {
|
|
9
|
+
"commander": "^11.1.0"
|
|
10
|
+
},
|
|
11
|
+
"devDependencies": {
|
|
12
|
+
"@types/node": "^22.15.23",
|
|
13
|
+
"tsup": "^8.4.0",
|
|
14
|
+
"typescript": "^5.7.3",
|
|
15
|
+
"vitest": "^3.0.4"
|
|
16
|
+
},
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "tsup",
|
|
19
|
+
"test": "vitest",
|
|
20
|
+
"dev": "tsup --watch"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Command } from "commander";
|
|
2
|
+
import { configureLoginCommand } from "./login";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Register all CLI commands with the program
|
|
6
|
+
* @param program The commander program instance
|
|
7
|
+
*/
|
|
8
|
+
export function registerCommands(program: Command): void {
|
|
9
|
+
// Register individual commands
|
|
10
|
+
configureLoginCommand(program);
|
|
11
|
+
|
|
12
|
+
// Add more commands here as they are implemented
|
|
13
|
+
}
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { randomBytes } from "node:crypto";
|
|
3
|
+
import fs from "node:fs/promises";
|
|
4
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
5
|
+
import { createServer } from "node:http";
|
|
6
|
+
import { homedir } from "node:os";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import type { Command } from "commander";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Default port for the local callback server
|
|
12
|
+
*/
|
|
13
|
+
const DEFAULT_CALLBACK_PORT = 7777;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Default Ollie Shop authorization endpoint
|
|
17
|
+
*/
|
|
18
|
+
const AUTH_ENDPOINT = "https://admin.ollie.shop/auth/login";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Configure the login command
|
|
22
|
+
* @param program The commander program instance
|
|
23
|
+
* @returns The configured command
|
|
24
|
+
*/
|
|
25
|
+
export function configureLoginCommand(program: Command): Command {
|
|
26
|
+
return program
|
|
27
|
+
.command("login")
|
|
28
|
+
.description("Log in to your Ollie Shop account")
|
|
29
|
+
.option(
|
|
30
|
+
"-p, --port <port>",
|
|
31
|
+
"Port to use for the local callback server",
|
|
32
|
+
DEFAULT_CALLBACK_PORT.toString(),
|
|
33
|
+
)
|
|
34
|
+
.option("--auth-url <url>", "Custom authorization URL", AUTH_ENDPOINT)
|
|
35
|
+
.action(async (options) => {
|
|
36
|
+
console.log("🔐 Initiating Ollie Shop login flow...");
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
const token = await startWebAuthFlow(options);
|
|
40
|
+
|
|
41
|
+
if (token) {
|
|
42
|
+
await saveCredentials(token);
|
|
43
|
+
|
|
44
|
+
console.log("✅ Successfully logged in!");
|
|
45
|
+
// Process continues normally after login
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
console.error("❌ Authentication failed. Please try again.");
|
|
50
|
+
process.exit(1);
|
|
51
|
+
} catch (error) {
|
|
52
|
+
console.error(
|
|
53
|
+
`❌ Login failed: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
54
|
+
);
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Authentication response type
|
|
62
|
+
*/
|
|
63
|
+
type AuthToken = {
|
|
64
|
+
accessToken: string;
|
|
65
|
+
refreshToken: string;
|
|
66
|
+
expiresAt: string;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Handle the callback endpoint for web authentication flow
|
|
71
|
+
*
|
|
72
|
+
* This now expects to receive tokens directly from the Next.js app with Supabase auth
|
|
73
|
+
*/
|
|
74
|
+
async function handleAuthCallback(
|
|
75
|
+
req: IncomingMessage,
|
|
76
|
+
res: ServerResponse,
|
|
77
|
+
state: string,
|
|
78
|
+
resolve: (token: AuthToken | null) => void,
|
|
79
|
+
reject: (err: Error) => void,
|
|
80
|
+
): Promise<void> {
|
|
81
|
+
// TypeScript doesn't know the socket will always have localPort, so we cast
|
|
82
|
+
const socket = req.socket as { localPort?: number };
|
|
83
|
+
const url = new URL(
|
|
84
|
+
req.url || "/",
|
|
85
|
+
`http://localhost:${socket.localPort || 3000}`,
|
|
86
|
+
);
|
|
87
|
+
const params = url.searchParams;
|
|
88
|
+
|
|
89
|
+
// Verify state from query parameters to prevent CSRF attacks
|
|
90
|
+
const returnedState = params.get("state");
|
|
91
|
+
if (returnedState !== state) {
|
|
92
|
+
sendErrorResponse(
|
|
93
|
+
res,
|
|
94
|
+
400,
|
|
95
|
+
"Invalid state parameter",
|
|
96
|
+
"Authentication failed. Please try again.",
|
|
97
|
+
);
|
|
98
|
+
reject(new Error("Invalid state parameter"));
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Parse the request body to get the access_token from form data
|
|
103
|
+
let formData = "";
|
|
104
|
+
req.on("data", (chunk) => {
|
|
105
|
+
formData += chunk.toString();
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
await new Promise<void>((formResolve) => {
|
|
109
|
+
req.on("end", () => {
|
|
110
|
+
formResolve();
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// Parse form data
|
|
115
|
+
const formParams = new URLSearchParams(formData);
|
|
116
|
+
const accessToken = formParams.get("access_token");
|
|
117
|
+
const refreshToken = formParams.get("refresh_token") || "";
|
|
118
|
+
const expiresAt =
|
|
119
|
+
formParams.get("expires_at") ||
|
|
120
|
+
new Date(Date.now() + 3600000).toISOString();
|
|
121
|
+
|
|
122
|
+
if (!accessToken) {
|
|
123
|
+
sendErrorResponse(
|
|
124
|
+
res,
|
|
125
|
+
400,
|
|
126
|
+
"Missing authentication token",
|
|
127
|
+
"Authentication failed. Please try again.",
|
|
128
|
+
);
|
|
129
|
+
reject(new Error("Missing authentication token"));
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
// Create token object
|
|
135
|
+
const token: AuthToken = {
|
|
136
|
+
accessToken,
|
|
137
|
+
refreshToken,
|
|
138
|
+
expiresAt,
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
// Send success response to browser
|
|
142
|
+
sendSuccessResponse(res);
|
|
143
|
+
|
|
144
|
+
// Resolve the promise with the token
|
|
145
|
+
resolve(token);
|
|
146
|
+
} catch (error) {
|
|
147
|
+
const errorMessage =
|
|
148
|
+
error instanceof Error ? error.message : "Unknown error";
|
|
149
|
+
sendErrorResponse(
|
|
150
|
+
res,
|
|
151
|
+
500,
|
|
152
|
+
"Authentication failed",
|
|
153
|
+
`Error: ${errorMessage}`,
|
|
154
|
+
);
|
|
155
|
+
reject(new Error(errorMessage));
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Send HTML error response
|
|
161
|
+
*/
|
|
162
|
+
function sendErrorResponse(
|
|
163
|
+
res: ServerResponse,
|
|
164
|
+
statusCode: number,
|
|
165
|
+
title: string,
|
|
166
|
+
message: string,
|
|
167
|
+
): void {
|
|
168
|
+
res.writeHead(statusCode, { "Content-Type": "text/html" });
|
|
169
|
+
res.end(`<h1>${title}</h1><p>${message}</p>`);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Send HTML success response
|
|
174
|
+
*/
|
|
175
|
+
function sendSuccessResponse(res: ServerResponse): void {
|
|
176
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
177
|
+
res.end(
|
|
178
|
+
"<h1>Authentication successful!</h1><p>You can now close this window and return to the CLI.</p>",
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Send HTML waiting page response
|
|
184
|
+
*/
|
|
185
|
+
function sendWaitingResponse(res: ServerResponse): void {
|
|
186
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
187
|
+
res.end(
|
|
188
|
+
"<h1>Ollie Shop CLI Authentication</h1><p>Waiting for authentication response...</p>",
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Start the web-based authentication flow
|
|
194
|
+
*/
|
|
195
|
+
async function startWebAuthFlow(options: {
|
|
196
|
+
port: string;
|
|
197
|
+
authUrl: string;
|
|
198
|
+
}): Promise<AuthToken | null> {
|
|
199
|
+
const state = randomBytes(16).toString("hex");
|
|
200
|
+
const port = Number.parseInt(options.port, 10);
|
|
201
|
+
const baseUrl = options.authUrl;
|
|
202
|
+
|
|
203
|
+
return new Promise<AuthToken | null>((resolve, reject) => {
|
|
204
|
+
// Create a local server to receive the callback
|
|
205
|
+
const server = createServer(async (req, res) => {
|
|
206
|
+
try {
|
|
207
|
+
// Parse the URL and query parameters
|
|
208
|
+
const url = new URL(req.url || "/", `http://localhost:${port}`);
|
|
209
|
+
|
|
210
|
+
// Check if this is a callback from the Next.js app
|
|
211
|
+
if (url.pathname === "/callback") {
|
|
212
|
+
await handleAuthCallback(
|
|
213
|
+
req,
|
|
214
|
+
res,
|
|
215
|
+
state,
|
|
216
|
+
(token) => {
|
|
217
|
+
// Wrap the resolve to ensure server is closed
|
|
218
|
+
server.close(() => {
|
|
219
|
+
console.log("🔐 Local authentication server closed");
|
|
220
|
+
resolve(token);
|
|
221
|
+
});
|
|
222
|
+
},
|
|
223
|
+
(error) => {
|
|
224
|
+
// Wrap the reject to ensure server is closed
|
|
225
|
+
server.close(() => {
|
|
226
|
+
console.log("🔐 Local authentication server closed");
|
|
227
|
+
reject(error);
|
|
228
|
+
});
|
|
229
|
+
},
|
|
230
|
+
);
|
|
231
|
+
} else {
|
|
232
|
+
// Serve a simple page for any other path
|
|
233
|
+
sendWaitingResponse(res);
|
|
234
|
+
}
|
|
235
|
+
} catch (error) {
|
|
236
|
+
const errorMessage =
|
|
237
|
+
error instanceof Error ? error.message : "Unknown error";
|
|
238
|
+
sendErrorResponse(res, 500, "Server Error", errorMessage);
|
|
239
|
+
server.close(() => {
|
|
240
|
+
console.log("🔐 Local authentication server closed");
|
|
241
|
+
reject(new Error(errorMessage));
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
// Start the server
|
|
247
|
+
server.listen(port, () => {
|
|
248
|
+
// Build the URL to your Next.js app with necessary parameters
|
|
249
|
+
const redirectUrl = `http://localhost:${port}/callback`;
|
|
250
|
+
|
|
251
|
+
const authUrl = new URL(baseUrl);
|
|
252
|
+
|
|
253
|
+
authUrl.searchParams.set("flow", "cli");
|
|
254
|
+
authUrl.searchParams.set("state", state);
|
|
255
|
+
authUrl.searchParams.set("redirect_to", redirectUrl);
|
|
256
|
+
|
|
257
|
+
console.log("\n🔒 Please authenticate in your browser...\n");
|
|
258
|
+
console.log(`Opening: ${authUrl}\n`);
|
|
259
|
+
|
|
260
|
+
// Open the browser with the authorization URL
|
|
261
|
+
openBrowser(authUrl.toString());
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
// Handle server errors
|
|
265
|
+
server.on("error", (err: Error) => {
|
|
266
|
+
if ((err as NodeJS.ErrnoException).code === "EADDRINUSE") {
|
|
267
|
+
reject(
|
|
268
|
+
new Error(
|
|
269
|
+
`Port ${port} is already in use. Please specify a different port using the --port option.`,
|
|
270
|
+
),
|
|
271
|
+
);
|
|
272
|
+
} else {
|
|
273
|
+
reject(err);
|
|
274
|
+
}
|
|
275
|
+
server.close();
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
// Set a timeout to prevent hanging indefinitely
|
|
279
|
+
const timeoutId = setTimeout(
|
|
280
|
+
() => {
|
|
281
|
+
server.close(() => {
|
|
282
|
+
console.log("🔐 Local authentication server closed due to timeout");
|
|
283
|
+
reject(new Error("Authentication timed out. Please try again."));
|
|
284
|
+
});
|
|
285
|
+
},
|
|
286
|
+
5 * 60 * 1000,
|
|
287
|
+
); // 5 minutes timeout
|
|
288
|
+
|
|
289
|
+
// Clean up the timeout if the server closes for other reasons
|
|
290
|
+
server.on("close", () => {
|
|
291
|
+
clearTimeout(timeoutId);
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Open the default browser with the given URL
|
|
298
|
+
*/
|
|
299
|
+
function openBrowser(url: string) {
|
|
300
|
+
const command =
|
|
301
|
+
process.platform === "darwin"
|
|
302
|
+
? "open"
|
|
303
|
+
: process.platform === "win32"
|
|
304
|
+
? "start"
|
|
305
|
+
: "xdg-open";
|
|
306
|
+
|
|
307
|
+
spawn(command, [url], { detached: true }).unref();
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Save authentication credentials locally
|
|
312
|
+
*
|
|
313
|
+
* @todo Use a more secure storage method like OS keychain
|
|
314
|
+
*/
|
|
315
|
+
async function saveCredentials(token: {
|
|
316
|
+
accessToken: string;
|
|
317
|
+
refreshToken: string;
|
|
318
|
+
expiresAt: string;
|
|
319
|
+
}) {
|
|
320
|
+
console.log("Saving credentials...");
|
|
321
|
+
|
|
322
|
+
const configDir = path.join(homedir(), ".ollie-shop");
|
|
323
|
+
const credentialsPath = path.join(configDir, "credentials.json");
|
|
324
|
+
|
|
325
|
+
// Create the .ollie-shop directory if it doesn't exist
|
|
326
|
+
try {
|
|
327
|
+
await fs.mkdir(configDir, { recursive: true });
|
|
328
|
+
} catch (error) {
|
|
329
|
+
// Ignore error if directory already exists
|
|
330
|
+
if (
|
|
331
|
+
!(error instanceof Error && "code" in error && error.code === "EEXIST")
|
|
332
|
+
) {
|
|
333
|
+
throw error;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
await fs.writeFile(credentialsPath, JSON.stringify(token, null, 2));
|
|
338
|
+
|
|
339
|
+
return true;
|
|
340
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { registerCommands } from "./commands";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Create and configure the CLI program
|
|
6
|
+
*/
|
|
7
|
+
export function createProgram() {
|
|
8
|
+
const program = new Command();
|
|
9
|
+
program.name("ollie").description("Ollie Shop CLI tools").version("0.1.0");
|
|
10
|
+
|
|
11
|
+
// Register all commands
|
|
12
|
+
registerCommands(program);
|
|
13
|
+
|
|
14
|
+
return program;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// If this file is run directly, execute the CLI
|
|
18
|
+
if (require.main === module) {
|
|
19
|
+
const program = createProgram();
|
|
20
|
+
program.parse();
|
|
21
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export const SRC_DIR = "src";
|
|
2
|
+
export const COMPONENTS_DIR = "components";
|
|
3
|
+
export const DIST_DIR = "dist";
|
|
4
|
+
export const META_FILE = "meta.json";
|
|
5
|
+
export const CONFIG_FILE = "ollie.json";
|
|
6
|
+
export const OLLIE_SHOP_ASSETS_DIR = "node_modules/.ollie-shop";
|
|
7
|
+
export const TEMPLATES_DIR = "templates";
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"lib": ["ESNext", "DOM"],
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"target": "ESNext",
|
|
6
|
+
"moduleResolution": "Bundler",
|
|
7
|
+
"jsx": "react-jsx",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"declaration": true,
|
|
10
|
+
"noUncheckedIndexedAccess": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"esModuleInterop": true,
|
|
13
|
+
"resolveJsonModule": true,
|
|
14
|
+
"types": ["node", "vitest/globals"],
|
|
15
|
+
"paths": {
|
|
16
|
+
"@/*": ["./src/*"]
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"exclude": ["node_modules", "dist"]
|
|
20
|
+
}
|
package/tsup.config.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { defineConfig } from "tsup";
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
entry: ["./src/index.ts"],
|
|
5
|
+
bundle: true,
|
|
6
|
+
splitting: false,
|
|
7
|
+
minify: false,
|
|
8
|
+
sourcemap: false,
|
|
9
|
+
platform: "node",
|
|
10
|
+
format: "cjs",
|
|
11
|
+
clean: true,
|
|
12
|
+
treeshake: true,
|
|
13
|
+
dts: false,
|
|
14
|
+
banner: { js: "#!/usr/bin/env node" },
|
|
15
|
+
});
|