@fluid-app/fluid-cli 0.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/dist/bin/fluid.d.mts +1 -0
- package/dist/bin/fluid.mjs +460 -0
- package/dist/bin/fluid.mjs.map +1 -0
- package/dist/index.d.mts +210 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +301 -0
- package/dist/index.mjs.map +1 -0
- package/dist/token-DCpSVmEk.mjs +322 -0
- package/dist/token-DCpSVmEk.mjs.map +1 -0
- package/package.json +50 -0
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { randomBytes } from "node:crypto";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
//#region src/utils/result.ts
|
|
6
|
+
function success(value) {
|
|
7
|
+
return {
|
|
8
|
+
success: true,
|
|
9
|
+
value
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
function failure(error) {
|
|
13
|
+
return {
|
|
14
|
+
success: false,
|
|
15
|
+
error
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
function isSuccess(result) {
|
|
19
|
+
return result.success === true;
|
|
20
|
+
}
|
|
21
|
+
function isFailure(result) {
|
|
22
|
+
return result.success === false;
|
|
23
|
+
}
|
|
24
|
+
function tryCatch(fn) {
|
|
25
|
+
try {
|
|
26
|
+
return success(fn());
|
|
27
|
+
} catch (error) {
|
|
28
|
+
return failure(error instanceof Error ? error : new Error(String(error)));
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
async function tryCatchAsync(fn) {
|
|
32
|
+
try {
|
|
33
|
+
return success(await fn());
|
|
34
|
+
} catch (error) {
|
|
35
|
+
return failure(error instanceof Error ? error : new Error(String(error)));
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function unwrap(result) {
|
|
39
|
+
if (isSuccess(result)) return result.value;
|
|
40
|
+
if (result.error instanceof Error) throw result.error;
|
|
41
|
+
throw new Error(typeof result.error === "object" && result.error !== null ? JSON.stringify(result.error) : String(result.error));
|
|
42
|
+
}
|
|
43
|
+
function unwrapOr(result, defaultValue) {
|
|
44
|
+
if (isSuccess(result)) return result.value;
|
|
45
|
+
return defaultValue;
|
|
46
|
+
}
|
|
47
|
+
function mapResult(result, fn) {
|
|
48
|
+
if (isSuccess(result)) return success(fn(result.value));
|
|
49
|
+
return result;
|
|
50
|
+
}
|
|
51
|
+
function mapError(result, fn) {
|
|
52
|
+
if (isFailure(result)) return failure(fn(result.error));
|
|
53
|
+
return result;
|
|
54
|
+
}
|
|
55
|
+
function isError(value) {
|
|
56
|
+
return value instanceof Error;
|
|
57
|
+
}
|
|
58
|
+
function isNodeError(value) {
|
|
59
|
+
return value instanceof Error && "code" in value;
|
|
60
|
+
}
|
|
61
|
+
function getErrorMessage(error) {
|
|
62
|
+
if (error instanceof Error) return error.message;
|
|
63
|
+
if (typeof error === "string") return error;
|
|
64
|
+
return String(error);
|
|
65
|
+
}
|
|
66
|
+
//#endregion
|
|
67
|
+
//#region src/auth/fluid-api.ts
|
|
68
|
+
const API_TIMEOUT_MS = 1e4;
|
|
69
|
+
function getFluidApiBase() {
|
|
70
|
+
return process.env["FLUID_API_BASE"] ?? "https://api.fluid.app";
|
|
71
|
+
}
|
|
72
|
+
function makeSignal() {
|
|
73
|
+
const controller = new AbortController();
|
|
74
|
+
const timer = setTimeout(() => controller.abort(), API_TIMEOUT_MS);
|
|
75
|
+
return {
|
|
76
|
+
signal: controller.signal,
|
|
77
|
+
cleanup: () => clearTimeout(timer)
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
const FLUID_API_ERROR = {
|
|
81
|
+
INVALID_TOKEN: {
|
|
82
|
+
code: "INVALID_TOKEN",
|
|
83
|
+
message: "Token is invalid or expired"
|
|
84
|
+
},
|
|
85
|
+
API_UNREACHABLE: {
|
|
86
|
+
code: "API_UNREACHABLE",
|
|
87
|
+
message: "Could not reach the Fluid API"
|
|
88
|
+
},
|
|
89
|
+
UUID_NOT_FOUND: {
|
|
90
|
+
code: "UUID_NOT_FOUND",
|
|
91
|
+
message: "Verification session not found"
|
|
92
|
+
},
|
|
93
|
+
CODE_EXPIRED: {
|
|
94
|
+
code: "CODE_EXPIRED",
|
|
95
|
+
message: "Verification code has expired — please request a new one"
|
|
96
|
+
},
|
|
97
|
+
INVALID_CODE: {
|
|
98
|
+
code: "INVALID_CODE",
|
|
99
|
+
message: "Invalid verification code"
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
function createApiError(template, details) {
|
|
103
|
+
return {
|
|
104
|
+
code: template.code,
|
|
105
|
+
message: template.message,
|
|
106
|
+
details
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Validate a token against the Fluid API and return company info
|
|
111
|
+
*/
|
|
112
|
+
async function validateToken(token) {
|
|
113
|
+
const { signal, cleanup } = makeSignal();
|
|
114
|
+
try {
|
|
115
|
+
const response = await fetch(`${getFluidApiBase()}/api/company/v1/companies/me`, {
|
|
116
|
+
method: "GET",
|
|
117
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
118
|
+
signal
|
|
119
|
+
});
|
|
120
|
+
if (response.ok) {
|
|
121
|
+
const name = (await response.json())?.data?.company?.name;
|
|
122
|
+
if (!name) return failure(createApiError(FLUID_API_ERROR.API_UNREACHABLE, "Unexpected response shape from /companies/me"));
|
|
123
|
+
return success({ name });
|
|
124
|
+
}
|
|
125
|
+
if (response.status === 401 || response.status === 403) return failure(createApiError(FLUID_API_ERROR.INVALID_TOKEN, `HTTP ${response.status}: Check that your token is valid and non-expired.`));
|
|
126
|
+
const body = await response.text();
|
|
127
|
+
return failure(createApiError(FLUID_API_ERROR.API_UNREACHABLE, `HTTP ${response.status}: ${body}`));
|
|
128
|
+
} catch (err) {
|
|
129
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
130
|
+
return failure(createApiError(FLUID_API_ERROR.API_UNREACHABLE, message));
|
|
131
|
+
} finally {
|
|
132
|
+
cleanup();
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Send a multi-factor authentication code to the given email address.
|
|
137
|
+
* Always returns 201 from the API (anti-enumeration).
|
|
138
|
+
*/
|
|
139
|
+
async function sendMfa(email) {
|
|
140
|
+
const { signal, cleanup } = makeSignal();
|
|
141
|
+
try {
|
|
142
|
+
const response = await fetch(`${getFluidApiBase()}/api/authentication/send_mfa`, {
|
|
143
|
+
method: "POST",
|
|
144
|
+
headers: { "Content-Type": "application/json" },
|
|
145
|
+
body: JSON.stringify({ email }),
|
|
146
|
+
signal
|
|
147
|
+
});
|
|
148
|
+
if (response.status === 201) {
|
|
149
|
+
const data = await response.json();
|
|
150
|
+
const uuid = data.multi_factor_authentication?.uuid;
|
|
151
|
+
const expiresAt = data.multi_factor_authentication?.verification_code_expires_at;
|
|
152
|
+
if (!uuid || !expiresAt) return failure(createApiError(FLUID_API_ERROR.API_UNREACHABLE, "Unexpected response shape from /send_mfa"));
|
|
153
|
+
return success({
|
|
154
|
+
uuid,
|
|
155
|
+
expiresAt
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
const body = await response.text();
|
|
159
|
+
return failure(createApiError(FLUID_API_ERROR.API_UNREACHABLE, `HTTP ${response.status}: ${body}`));
|
|
160
|
+
} catch (err) {
|
|
161
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
162
|
+
return failure(createApiError(FLUID_API_ERROR.API_UNREACHABLE, message));
|
|
163
|
+
} finally {
|
|
164
|
+
cleanup();
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Confirm a multi-factor authentication code and retrieve company JWTs.
|
|
169
|
+
*/
|
|
170
|
+
async function confirmMfa(uuid, code) {
|
|
171
|
+
const { signal, cleanup } = makeSignal();
|
|
172
|
+
try {
|
|
173
|
+
const response = await fetch(`${getFluidApiBase()}/api/authentication/confirm_mfa`, {
|
|
174
|
+
method: "POST",
|
|
175
|
+
headers: { "Content-Type": "application/json" },
|
|
176
|
+
body: JSON.stringify({
|
|
177
|
+
uuid,
|
|
178
|
+
multi_factor_authentication: { verification_code: code }
|
|
179
|
+
}),
|
|
180
|
+
signal
|
|
181
|
+
});
|
|
182
|
+
if (response.ok) {
|
|
183
|
+
const data = await response.json();
|
|
184
|
+
if (!data.authenticated) return failure(createApiError(FLUID_API_ERROR.INVALID_CODE, "Authentication was not confirmed by the server"));
|
|
185
|
+
const companies = Array.isArray(data.companies) ? data.companies : [];
|
|
186
|
+
return success({
|
|
187
|
+
authType: data.auth_type ?? "",
|
|
188
|
+
companies: companies.map((c) => ({
|
|
189
|
+
id: c.id,
|
|
190
|
+
name: c.name,
|
|
191
|
+
shopName: c.shop_name,
|
|
192
|
+
jwt: c.jwt
|
|
193
|
+
}))
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
if (response.status === 404) return failure(createApiError(FLUID_API_ERROR.UUID_NOT_FOUND));
|
|
197
|
+
if (response.status === 410) return failure(createApiError(FLUID_API_ERROR.CODE_EXPIRED));
|
|
198
|
+
if (response.status === 422) return failure(createApiError(FLUID_API_ERROR.INVALID_CODE));
|
|
199
|
+
const body = await response.text();
|
|
200
|
+
return failure(createApiError(FLUID_API_ERROR.API_UNREACHABLE, `HTTP ${response.status}: ${body}`));
|
|
201
|
+
} catch (err) {
|
|
202
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
203
|
+
return failure(createApiError(FLUID_API_ERROR.API_UNREACHABLE, message));
|
|
204
|
+
} finally {
|
|
205
|
+
cleanup();
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
//#endregion
|
|
209
|
+
//#region src/config/types.ts
|
|
210
|
+
function createDefaultConfig() {
|
|
211
|
+
return {
|
|
212
|
+
activeProfile: null,
|
|
213
|
+
profiles: {},
|
|
214
|
+
plugins: {},
|
|
215
|
+
enabledPlugins: null
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
//#endregion
|
|
219
|
+
//#region src/config/paths.ts
|
|
220
|
+
/**
|
|
221
|
+
* XDG-compliant config directory resolution
|
|
222
|
+
*
|
|
223
|
+
* Priority:
|
|
224
|
+
* 1. FLUID_CONFIG_DIR env var (explicit override)
|
|
225
|
+
* 2. ~/.fluid/ on macOS (convention for CLI tools)
|
|
226
|
+
* 3. %APPDATA%/fluid/ on Windows
|
|
227
|
+
* 4. $XDG_CONFIG_HOME/fluid/ on Linux (XDG spec)
|
|
228
|
+
* 5. ~/.config/fluid/ fallback on Linux
|
|
229
|
+
*/
|
|
230
|
+
function getConfigDir() {
|
|
231
|
+
const envOverride = process.env["FLUID_CONFIG_DIR"];
|
|
232
|
+
if (envOverride) return envOverride;
|
|
233
|
+
if (process.platform === "darwin") return join(homedir(), ".fluid");
|
|
234
|
+
if (process.platform === "win32") return join(process.env["APPDATA"] ?? join(homedir(), "AppData", "Roaming"), "fluid");
|
|
235
|
+
const xdgConfig = process.env["XDG_CONFIG_HOME"];
|
|
236
|
+
if (xdgConfig) return join(xdgConfig, "fluid");
|
|
237
|
+
return join(homedir(), ".config", "fluid");
|
|
238
|
+
}
|
|
239
|
+
function getConfigFilePath() {
|
|
240
|
+
return join(getConfigDir(), "config.json");
|
|
241
|
+
}
|
|
242
|
+
//#endregion
|
|
243
|
+
//#region src/config/config.ts
|
|
244
|
+
/**
|
|
245
|
+
* Read/write config.json with atomic writes and safe defaults
|
|
246
|
+
*/
|
|
247
|
+
function readConfig() {
|
|
248
|
+
const configPath = getConfigFilePath();
|
|
249
|
+
if (!existsSync(configPath)) return createDefaultConfig();
|
|
250
|
+
const raw = readFileSync(configPath, "utf-8");
|
|
251
|
+
let parsed;
|
|
252
|
+
try {
|
|
253
|
+
parsed = JSON.parse(raw);
|
|
254
|
+
} catch {
|
|
255
|
+
return createDefaultConfig();
|
|
256
|
+
}
|
|
257
|
+
return {
|
|
258
|
+
...createDefaultConfig(),
|
|
259
|
+
...parsed
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
function writeConfig(config) {
|
|
263
|
+
const configPath = getConfigFilePath();
|
|
264
|
+
const dir = dirname(configPath);
|
|
265
|
+
mkdirSync(dir, {
|
|
266
|
+
recursive: true,
|
|
267
|
+
mode: 448
|
|
268
|
+
});
|
|
269
|
+
const tmpPath = join(dir, `.fluid-config-${randomBytes(6).toString("hex")}.tmp`);
|
|
270
|
+
try {
|
|
271
|
+
writeFileSync(tmpPath, JSON.stringify(config, null, 2) + "\n", {
|
|
272
|
+
encoding: "utf-8",
|
|
273
|
+
mode: 384
|
|
274
|
+
});
|
|
275
|
+
renameSync(tmpPath, configPath);
|
|
276
|
+
} catch (err) {
|
|
277
|
+
try {
|
|
278
|
+
unlinkSync(tmpPath);
|
|
279
|
+
} catch {}
|
|
280
|
+
throw err;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
function updateConfig(updater) {
|
|
284
|
+
const updated = updater(readConfig());
|
|
285
|
+
writeConfig(updated);
|
|
286
|
+
return updated;
|
|
287
|
+
}
|
|
288
|
+
//#endregion
|
|
289
|
+
//#region src/auth/token.ts
|
|
290
|
+
/**
|
|
291
|
+
* Token storage and retrieval from config
|
|
292
|
+
*/
|
|
293
|
+
/**
|
|
294
|
+
* Get the auth token for a named profile, or the active profile when omitted.
|
|
295
|
+
*/
|
|
296
|
+
function getAuthToken(profileName) {
|
|
297
|
+
const config = readConfig();
|
|
298
|
+
const resolvedProfile = profileName ?? config.activeProfile;
|
|
299
|
+
if (!resolvedProfile) return null;
|
|
300
|
+
const profile = config.profiles[resolvedProfile];
|
|
301
|
+
if (!profile) return null;
|
|
302
|
+
return profile.token;
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Get the active profile, or null if not logged in
|
|
306
|
+
*/
|
|
307
|
+
function getActiveProfile() {
|
|
308
|
+
const config = readConfig();
|
|
309
|
+
if (!config.activeProfile) return null;
|
|
310
|
+
return config.profiles[config.activeProfile] ?? null;
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* List all stored profile names
|
|
314
|
+
*/
|
|
315
|
+
function listProfileNames() {
|
|
316
|
+
const config = readConfig();
|
|
317
|
+
return Object.keys(config.profiles);
|
|
318
|
+
}
|
|
319
|
+
//#endregion
|
|
320
|
+
export { tryCatchAsync as C, tryCatch as S, unwrapOr as T, isNodeError as _, updateConfig as a, mapResult as b, getConfigFilePath as c, sendMfa as d, validateToken as f, isFailure as g, isError as h, readConfig as i, createDefaultConfig as l, getErrorMessage as m, getAuthToken as n, writeConfig as o, failure as p, listProfileNames as r, getConfigDir as s, getActiveProfile as t, confirmMfa as u, isSuccess as v, unwrap as w, success as x, mapError as y };
|
|
321
|
+
|
|
322
|
+
//# sourceMappingURL=token-DCpSVmEk.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"token-DCpSVmEk.mjs","names":[],"sources":["../src/utils/result.ts","../src/auth/fluid-api.ts","../src/config/types.ts","../src/config/paths.ts","../src/config/config.ts","../src/auth/token.ts"],"sourcesContent":["/**\n * Result type utilities for type-safe error handling\n *\n * The Result<T, E> pattern provides a discriminated union for fallible operations,\n * enabling exhaustive handling without try/catch blocks.\n */\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Result type - discriminated union for success/failure\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport interface Success<T> {\n readonly success: true;\n readonly value: T;\n}\n\nexport interface Failure<E> {\n readonly success: false;\n readonly error: E;\n}\n\nexport type Result<T, E = Error> = Success<T> | Failure<E>;\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Constructor functions\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport function success<T>(value: T): Success<T> {\n return { success: true, value };\n}\n\nexport function failure<E>(error: E): Failure<E> {\n return { success: false, error };\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Type guards\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport function isSuccess<T, E>(result: Result<T, E>): result is Success<T> {\n return result.success === true;\n}\n\nexport function isFailure<T, E>(result: Result<T, E>): result is Failure<E> {\n return result.success === false;\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Utility functions\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport function tryCatch<T>(fn: () => T): Result<T, Error> {\n try {\n return success(fn());\n } catch (error) {\n return failure(error instanceof Error ? error : new Error(String(error)));\n }\n}\n\nexport async function tryCatchAsync<T>(\n fn: () => Promise<T>,\n): Promise<Result<T, Error>> {\n try {\n return success(await fn());\n } catch (error) {\n return failure(error instanceof Error ? error : new Error(String(error)));\n }\n}\n\nexport function unwrap<T, E>(result: Result<T, E>): T {\n if (isSuccess(result)) return result.value;\n // Always throw an Error instance so V8 attaches a stack trace and\n // `instanceof Error` checks in catch blocks work as expected.\n if (result.error instanceof Error) throw result.error;\n throw new Error(\n typeof result.error === \"object\" && result.error !== null\n ? JSON.stringify(result.error)\n : String(result.error),\n );\n}\n\nexport function unwrapOr<T, E>(result: Result<T, E>, defaultValue: T): T {\n if (isSuccess(result)) return result.value;\n return defaultValue;\n}\n\nexport function mapResult<T, U, E>(\n result: Result<T, E>,\n fn: (value: T) => U,\n): Result<U, E> {\n if (isSuccess(result)) return success(fn(result.value));\n return result;\n}\n\nexport function mapError<T, E, F>(\n result: Result<T, E>,\n fn: (error: E) => F,\n): Result<T, F> {\n if (isFailure(result)) return failure(fn(result.error));\n return result;\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Error narrowing utilities\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport function isError(value: unknown): value is Error {\n return value instanceof Error;\n}\n\nexport function isNodeError(value: unknown): value is NodeJS.ErrnoException {\n return value instanceof Error && \"code\" in value;\n}\n\nexport function getErrorMessage(error: unknown): string {\n if (error instanceof Error) return error.message;\n if (typeof error === \"string\") return error;\n return String(error);\n}\n","/**\n * Fluid API client for authentication operations\n */\n\nimport type { CliError } from \"../utils/errors.js\";\nimport { type Result, success, failure } from \"../utils/result.js\";\n\nconst API_TIMEOUT_MS = 10_000;\n\nfunction getFluidApiBase(): string {\n return process.env[\"FLUID_API_BASE\"] ?? \"https://api.fluid.app\";\n}\n\nfunction makeSignal(): { signal: AbortSignal; cleanup: () => void } {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), API_TIMEOUT_MS);\n return { signal: controller.signal, cleanup: () => clearTimeout(timer) };\n}\n\nexport interface FluidApiError extends CliError {\n readonly code: string;\n readonly message: string;\n readonly details?: string;\n}\n\nexport const FLUID_API_ERROR = {\n INVALID_TOKEN: {\n code: \"INVALID_TOKEN\",\n message: \"Token is invalid or expired\",\n },\n API_UNREACHABLE: {\n code: \"API_UNREACHABLE\",\n message: \"Could not reach the Fluid API\",\n },\n UUID_NOT_FOUND: {\n code: \"UUID_NOT_FOUND\",\n message: \"Verification session not found\",\n },\n CODE_EXPIRED: {\n code: \"CODE_EXPIRED\",\n message: \"Verification code has expired — please request a new one\",\n },\n INVALID_CODE: {\n code: \"INVALID_CODE\",\n message: \"Invalid verification code\",\n },\n} as const;\n\nfunction createApiError(\n template: { readonly code: string; readonly message: string },\n details?: string,\n): FluidApiError {\n return { code: template.code, message: template.message, details };\n}\n\nexport interface CompanyInfo {\n readonly name: string;\n}\n\nexport interface MfaResponse {\n readonly uuid: string;\n readonly expiresAt: string;\n}\n\nexport interface CompanyChoice {\n readonly id: number;\n readonly name: string;\n readonly shopName: string;\n readonly jwt: string;\n}\n\nexport interface ConfirmMfaResponse {\n readonly authType: string;\n readonly companies: CompanyChoice[];\n}\n\n/**\n * Validate a token against the Fluid API and return company info\n */\nexport async function validateToken(\n token: string,\n): Promise<Result<CompanyInfo, FluidApiError>> {\n const { signal, cleanup } = makeSignal();\n try {\n const response = await fetch(\n `${getFluidApiBase()}/api/company/v1/companies/me`,\n {\n method: \"GET\",\n headers: {\n Authorization: `Bearer ${token}`,\n },\n signal,\n },\n );\n\n if (response.ok) {\n const data = (await response.json()) as {\n data?: { company?: { name?: string } };\n };\n const name = data?.data?.company?.name;\n if (!name) {\n return failure(\n createApiError(\n FLUID_API_ERROR.API_UNREACHABLE,\n \"Unexpected response shape from /companies/me\",\n ),\n );\n }\n return success({ name });\n }\n\n if (response.status === 401 || response.status === 403) {\n return failure(\n createApiError(\n FLUID_API_ERROR.INVALID_TOKEN,\n `HTTP ${response.status}: Check that your token is valid and non-expired.`,\n ),\n );\n }\n\n const body = await response.text();\n return failure(\n createApiError(\n FLUID_API_ERROR.API_UNREACHABLE,\n `HTTP ${response.status}: ${body}`,\n ),\n );\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n return failure(createApiError(FLUID_API_ERROR.API_UNREACHABLE, message));\n } finally {\n cleanup();\n }\n}\n\n/**\n * Send a multi-factor authentication code to the given email address.\n * Always returns 201 from the API (anti-enumeration).\n */\nexport async function sendMfa(\n email: string,\n): Promise<Result<MfaResponse, FluidApiError>> {\n const { signal, cleanup } = makeSignal();\n try {\n const response = await fetch(\n `${getFluidApiBase()}/api/authentication/send_mfa`,\n {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ email }),\n signal,\n },\n );\n\n if (response.status === 201) {\n const data = (await response.json()) as {\n multi_factor_authentication?: {\n uuid?: string;\n verification_code_expires_at?: string;\n };\n };\n const uuid = data.multi_factor_authentication?.uuid;\n const expiresAt =\n data.multi_factor_authentication?.verification_code_expires_at;\n if (!uuid || !expiresAt) {\n return failure(\n createApiError(\n FLUID_API_ERROR.API_UNREACHABLE,\n \"Unexpected response shape from /send_mfa\",\n ),\n );\n }\n return success({ uuid, expiresAt });\n }\n\n const body = await response.text();\n return failure(\n createApiError(\n FLUID_API_ERROR.API_UNREACHABLE,\n `HTTP ${response.status}: ${body}`,\n ),\n );\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n return failure(createApiError(FLUID_API_ERROR.API_UNREACHABLE, message));\n } finally {\n cleanup();\n }\n}\n\n/**\n * Confirm a multi-factor authentication code and retrieve company JWTs.\n */\nexport async function confirmMfa(\n uuid: string,\n code: string,\n): Promise<Result<ConfirmMfaResponse, FluidApiError>> {\n const { signal, cleanup } = makeSignal();\n try {\n const response = await fetch(\n `${getFluidApiBase()}/api/authentication/confirm_mfa`,\n {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n uuid,\n multi_factor_authentication: { verification_code: code },\n }),\n signal,\n },\n );\n\n if (response.ok) {\n const data = (await response.json()) as {\n authenticated?: boolean;\n auth_type?: string;\n companies?:\n | {\n id: number;\n name: string;\n shop_name: string;\n jwt: string;\n }[]\n | null;\n };\n if (!data.authenticated) {\n return failure(\n createApiError(\n FLUID_API_ERROR.INVALID_CODE,\n \"Authentication was not confirmed by the server\",\n ),\n );\n }\n const companies = Array.isArray(data.companies) ? data.companies : [];\n return success({\n authType: data.auth_type ?? \"\",\n companies: companies.map((c) => ({\n id: c.id,\n name: c.name,\n shopName: c.shop_name,\n jwt: c.jwt,\n })),\n });\n }\n\n if (response.status === 404) {\n return failure(createApiError(FLUID_API_ERROR.UUID_NOT_FOUND));\n }\n if (response.status === 410) {\n return failure(createApiError(FLUID_API_ERROR.CODE_EXPIRED));\n }\n if (response.status === 422) {\n return failure(createApiError(FLUID_API_ERROR.INVALID_CODE));\n }\n\n const body = await response.text();\n return failure(\n createApiError(\n FLUID_API_ERROR.API_UNREACHABLE,\n `HTTP ${response.status}: ${body}`,\n ),\n );\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n return failure(createApiError(FLUID_API_ERROR.API_UNREACHABLE, message));\n } finally {\n cleanup();\n }\n}\n","/**\n * Configuration types for the Fluid CLI\n */\n\nexport interface FluidProfile {\n readonly name: string;\n readonly token: string;\n readonly companyName: string;\n readonly storedAt: string; // ISO-8601\n}\n\nexport interface FluidConfig {\n activeProfile: string | null;\n profiles: Record<string, FluidProfile>;\n plugins: Record<string, unknown>;\n /**\n * Allow-list of plugin package names.\n *\n * - `null` (default) — auto-discover and load all `@fluid-app/fluid-cli-*`\n * and `*-cli-commands` packages found in `node_modules` or the pnpm\n * workspace. Only `@fluid-app`-scoped packages matching the naming\n * convention are eligible; no third-party code is loaded.\n * - `string[]` — only load plugins whose names appear in the array.\n * Set to `[]` to disable all plugins.\n */\n enabledPlugins: string[] | null;\n}\n\nexport function createDefaultConfig(): FluidConfig {\n return {\n activeProfile: null,\n profiles: {},\n plugins: {},\n enabledPlugins: null, // auto-discover all plugins\n };\n}\n","/**\n * XDG-compliant config directory resolution\n *\n * Priority:\n * 1. FLUID_CONFIG_DIR env var (explicit override)\n * 2. ~/.fluid/ on macOS (convention for CLI tools)\n * 3. %APPDATA%/fluid/ on Windows\n * 4. $XDG_CONFIG_HOME/fluid/ on Linux (XDG spec)\n * 5. ~/.config/fluid/ fallback on Linux\n */\n\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\n\nexport function getConfigDir(): string {\n const envOverride = process.env[\"FLUID_CONFIG_DIR\"];\n if (envOverride) return envOverride;\n\n if (process.platform === \"darwin\") {\n return join(homedir(), \".fluid\");\n }\n\n if (process.platform === \"win32\") {\n const appData =\n process.env[\"APPDATA\"] ?? join(homedir(), \"AppData\", \"Roaming\");\n return join(appData, \"fluid\");\n }\n\n const xdgConfig = process.env[\"XDG_CONFIG_HOME\"];\n if (xdgConfig) return join(xdgConfig, \"fluid\");\n\n return join(homedir(), \".config\", \"fluid\");\n}\n\nexport function getConfigFilePath(): string {\n return join(getConfigDir(), \"config.json\");\n}\n","/**\n * Read/write config.json with atomic writes and safe defaults\n */\n\nimport {\n existsSync,\n mkdirSync,\n readFileSync,\n renameSync,\n unlinkSync,\n writeFileSync,\n} from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { randomBytes } from \"node:crypto\";\nimport { type FluidConfig, createDefaultConfig } from \"./types.js\";\nimport { getConfigFilePath } from \"./paths.js\";\n\nexport function readConfig(): FluidConfig {\n const configPath = getConfigFilePath();\n\n if (!existsSync(configPath)) {\n return createDefaultConfig();\n }\n\n const raw = readFileSync(configPath, \"utf-8\");\n\n let parsed: Partial<FluidConfig>;\n try {\n parsed = JSON.parse(raw) as Partial<FluidConfig>;\n } catch {\n // Corrupted config — fall back to defaults rather than crashing\n return createDefaultConfig();\n }\n\n // Merge with defaults to handle schema evolution\n return {\n ...createDefaultConfig(),\n ...parsed,\n };\n}\n\nexport function writeConfig(config: FluidConfig): void {\n const configPath = getConfigFilePath();\n const dir = dirname(configPath);\n\n // 0o700: owner-only access so other users cannot enumerate the config dir\n mkdirSync(dir, { recursive: true, mode: 0o700 });\n\n // Atomic write: write to a temp file in the same directory, then rename.\n // rename() is a POSIX atomic operation — a crash mid-write will never leave\n // a partially-written config.json. The temp file must be on the same\n // filesystem as the destination for rename() to succeed.\n const suffix = randomBytes(6).toString(\"hex\");\n const tmpPath = join(dir, `.fluid-config-${suffix}.tmp`);\n\n try {\n writeFileSync(tmpPath, JSON.stringify(config, null, 2) + \"\\n\", {\n encoding: \"utf-8\",\n // NOTE: mode: 0o600 restricts to owner read/write on POSIX (macOS, Linux).\n // On Windows, Node.js ignores this option — NTFS permissions are not\n // set via the mode parameter. Windows file protection would require\n // icacls or Windows security descriptor APIs.\n mode: 0o600,\n });\n renameSync(tmpPath, configPath);\n } catch (err) {\n // Clean up the temp file if something went wrong before the rename\n try {\n unlinkSync(tmpPath);\n } catch {\n // ignore cleanup errors — the temp file may not exist yet\n }\n throw err;\n }\n}\n\nexport function updateConfig(\n updater: (config: FluidConfig) => FluidConfig,\n): FluidConfig {\n const config = readConfig();\n const updated = updater(config);\n writeConfig(updated);\n return updated;\n}\n","/**\n * Token storage and retrieval from config\n */\n\nimport { readConfig } from \"../config/config.js\";\nimport type { FluidProfile } from \"../config/types.js\";\n\n/**\n * Get the auth token for a named profile, or the active profile when omitted.\n */\nexport function getAuthToken(profileName?: string): string | null {\n const config = readConfig();\n const resolvedProfile = profileName ?? config.activeProfile;\n if (!resolvedProfile) return null;\n\n const profile = config.profiles[resolvedProfile];\n if (!profile) return null;\n\n return profile.token;\n}\n\n/**\n * Get the active profile, or null if not logged in\n */\nexport function getActiveProfile(): FluidProfile | null {\n const config = readConfig();\n if (!config.activeProfile) return null;\n\n return config.profiles[config.activeProfile] ?? null;\n}\n\n/**\n * List all stored profile names\n */\nexport function listProfileNames(): string[] {\n const config = readConfig();\n return Object.keys(config.profiles);\n}\n"],"mappings":";;;;;AA2BA,SAAgB,QAAW,OAAsB;AAC/C,QAAO;EAAE,SAAS;EAAM;EAAO;;AAGjC,SAAgB,QAAW,OAAsB;AAC/C,QAAO;EAAE,SAAS;EAAO;EAAO;;AAOlC,SAAgB,UAAgB,QAA4C;AAC1E,QAAO,OAAO,YAAY;;AAG5B,SAAgB,UAAgB,QAA4C;AAC1E,QAAO,OAAO,YAAY;;AAO5B,SAAgB,SAAY,IAA+B;AACzD,KAAI;AACF,SAAO,QAAQ,IAAI,CAAC;UACb,OAAO;AACd,SAAO,QAAQ,iBAAiB,QAAQ,QAAQ,IAAI,MAAM,OAAO,MAAM,CAAC,CAAC;;;AAI7E,eAAsB,cACpB,IAC2B;AAC3B,KAAI;AACF,SAAO,QAAQ,MAAM,IAAI,CAAC;UACnB,OAAO;AACd,SAAO,QAAQ,iBAAiB,QAAQ,QAAQ,IAAI,MAAM,OAAO,MAAM,CAAC,CAAC;;;AAI7E,SAAgB,OAAa,QAAyB;AACpD,KAAI,UAAU,OAAO,CAAE,QAAO,OAAO;AAGrC,KAAI,OAAO,iBAAiB,MAAO,OAAM,OAAO;AAChD,OAAM,IAAI,MACR,OAAO,OAAO,UAAU,YAAY,OAAO,UAAU,OACjD,KAAK,UAAU,OAAO,MAAM,GAC5B,OAAO,OAAO,MAAM,CACzB;;AAGH,SAAgB,SAAe,QAAsB,cAAoB;AACvE,KAAI,UAAU,OAAO,CAAE,QAAO,OAAO;AACrC,QAAO;;AAGT,SAAgB,UACd,QACA,IACc;AACd,KAAI,UAAU,OAAO,CAAE,QAAO,QAAQ,GAAG,OAAO,MAAM,CAAC;AACvD,QAAO;;AAGT,SAAgB,SACd,QACA,IACc;AACd,KAAI,UAAU,OAAO,CAAE,QAAO,QAAQ,GAAG,OAAO,MAAM,CAAC;AACvD,QAAO;;AAOT,SAAgB,QAAQ,OAAgC;AACtD,QAAO,iBAAiB;;AAG1B,SAAgB,YAAY,OAAgD;AAC1E,QAAO,iBAAiB,SAAS,UAAU;;AAG7C,SAAgB,gBAAgB,OAAwB;AACtD,KAAI,iBAAiB,MAAO,QAAO,MAAM;AACzC,KAAI,OAAO,UAAU,SAAU,QAAO;AACtC,QAAO,OAAO,MAAM;;;;AC9GtB,MAAM,iBAAiB;AAEvB,SAAS,kBAA0B;AACjC,QAAO,QAAQ,IAAI,qBAAqB;;AAG1C,SAAS,aAA2D;CAClE,MAAM,aAAa,IAAI,iBAAiB;CACxC,MAAM,QAAQ,iBAAiB,WAAW,OAAO,EAAE,eAAe;AAClE,QAAO;EAAE,QAAQ,WAAW;EAAQ,eAAe,aAAa,MAAM;EAAE;;AAS1E,MAAa,kBAAkB;CAC7B,eAAe;EACb,MAAM;EACN,SAAS;EACV;CACD,iBAAiB;EACf,MAAM;EACN,SAAS;EACV;CACD,gBAAgB;EACd,MAAM;EACN,SAAS;EACV;CACD,cAAc;EACZ,MAAM;EACN,SAAS;EACV;CACD,cAAc;EACZ,MAAM;EACN,SAAS;EACV;CACF;AAED,SAAS,eACP,UACA,SACe;AACf,QAAO;EAAE,MAAM,SAAS;EAAM,SAAS,SAAS;EAAS;EAAS;;;;;AA2BpE,eAAsB,cACpB,OAC6C;CAC7C,MAAM,EAAE,QAAQ,YAAY,YAAY;AACxC,KAAI;EACF,MAAM,WAAW,MAAM,MACrB,GAAG,iBAAiB,CAAC,+BACrB;GACE,QAAQ;GACR,SAAS,EACP,eAAe,UAAU,SAC1B;GACD;GACD,CACF;AAED,MAAI,SAAS,IAAI;GAIf,MAAM,QAHQ,MAAM,SAAS,MAAM,GAGhB,MAAM,SAAS;AAClC,OAAI,CAAC,KACH,QAAO,QACL,eACE,gBAAgB,iBAChB,+CACD,CACF;AAEH,UAAO,QAAQ,EAAE,MAAM,CAAC;;AAG1B,MAAI,SAAS,WAAW,OAAO,SAAS,WAAW,IACjD,QAAO,QACL,eACE,gBAAgB,eAChB,QAAQ,SAAS,OAAO,mDACzB,CACF;EAGH,MAAM,OAAO,MAAM,SAAS,MAAM;AAClC,SAAO,QACL,eACE,gBAAgB,iBAChB,QAAQ,SAAS,OAAO,IAAI,OAC7B,CACF;UACM,KAAK;EACZ,MAAM,UAAU,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI;AAChE,SAAO,QAAQ,eAAe,gBAAgB,iBAAiB,QAAQ,CAAC;WAChE;AACR,WAAS;;;;;;;AAQb,eAAsB,QACpB,OAC6C;CAC7C,MAAM,EAAE,QAAQ,YAAY,YAAY;AACxC,KAAI;EACF,MAAM,WAAW,MAAM,MACrB,GAAG,iBAAiB,CAAC,+BACrB;GACE,QAAQ;GACR,SAAS,EAAE,gBAAgB,oBAAoB;GAC/C,MAAM,KAAK,UAAU,EAAE,OAAO,CAAC;GAC/B;GACD,CACF;AAED,MAAI,SAAS,WAAW,KAAK;GAC3B,MAAM,OAAQ,MAAM,SAAS,MAAM;GAMnC,MAAM,OAAO,KAAK,6BAA6B;GAC/C,MAAM,YACJ,KAAK,6BAA6B;AACpC,OAAI,CAAC,QAAQ,CAAC,UACZ,QAAO,QACL,eACE,gBAAgB,iBAChB,2CACD,CACF;AAEH,UAAO,QAAQ;IAAE;IAAM;IAAW,CAAC;;EAGrC,MAAM,OAAO,MAAM,SAAS,MAAM;AAClC,SAAO,QACL,eACE,gBAAgB,iBAChB,QAAQ,SAAS,OAAO,IAAI,OAC7B,CACF;UACM,KAAK;EACZ,MAAM,UAAU,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI;AAChE,SAAO,QAAQ,eAAe,gBAAgB,iBAAiB,QAAQ,CAAC;WAChE;AACR,WAAS;;;;;;AAOb,eAAsB,WACpB,MACA,MACoD;CACpD,MAAM,EAAE,QAAQ,YAAY,YAAY;AACxC,KAAI;EACF,MAAM,WAAW,MAAM,MACrB,GAAG,iBAAiB,CAAC,kCACrB;GACE,QAAQ;GACR,SAAS,EAAE,gBAAgB,oBAAoB;GAC/C,MAAM,KAAK,UAAU;IACnB;IACA,6BAA6B,EAAE,mBAAmB,MAAM;IACzD,CAAC;GACF;GACD,CACF;AAED,MAAI,SAAS,IAAI;GACf,MAAM,OAAQ,MAAM,SAAS,MAAM;AAYnC,OAAI,CAAC,KAAK,cACR,QAAO,QACL,eACE,gBAAgB,cAChB,iDACD,CACF;GAEH,MAAM,YAAY,MAAM,QAAQ,KAAK,UAAU,GAAG,KAAK,YAAY,EAAE;AACrE,UAAO,QAAQ;IACb,UAAU,KAAK,aAAa;IAC5B,WAAW,UAAU,KAAK,OAAO;KAC/B,IAAI,EAAE;KACN,MAAM,EAAE;KACR,UAAU,EAAE;KACZ,KAAK,EAAE;KACR,EAAE;IACJ,CAAC;;AAGJ,MAAI,SAAS,WAAW,IACtB,QAAO,QAAQ,eAAe,gBAAgB,eAAe,CAAC;AAEhE,MAAI,SAAS,WAAW,IACtB,QAAO,QAAQ,eAAe,gBAAgB,aAAa,CAAC;AAE9D,MAAI,SAAS,WAAW,IACtB,QAAO,QAAQ,eAAe,gBAAgB,aAAa,CAAC;EAG9D,MAAM,OAAO,MAAM,SAAS,MAAM;AAClC,SAAO,QACL,eACE,gBAAgB,iBAChB,QAAQ,SAAS,OAAO,IAAI,OAC7B,CACF;UACM,KAAK;EACZ,MAAM,UAAU,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI;AAChE,SAAO,QAAQ,eAAe,gBAAgB,iBAAiB,QAAQ,CAAC;WAChE;AACR,WAAS;;;;;AC9Ob,SAAgB,sBAAmC;AACjD,QAAO;EACL,eAAe;EACf,UAAU,EAAE;EACZ,SAAS,EAAE;EACX,gBAAgB;EACjB;;;;;;;;;;;;;;ACpBH,SAAgB,eAAuB;CACrC,MAAM,cAAc,QAAQ,IAAI;AAChC,KAAI,YAAa,QAAO;AAExB,KAAI,QAAQ,aAAa,SACvB,QAAO,KAAK,SAAS,EAAE,SAAS;AAGlC,KAAI,QAAQ,aAAa,QAGvB,QAAO,KADL,QAAQ,IAAI,cAAc,KAAK,SAAS,EAAE,WAAW,UAAU,EAC5C,QAAQ;CAG/B,MAAM,YAAY,QAAQ,IAAI;AAC9B,KAAI,UAAW,QAAO,KAAK,WAAW,QAAQ;AAE9C,QAAO,KAAK,SAAS,EAAE,WAAW,QAAQ;;AAG5C,SAAgB,oBAA4B;AAC1C,QAAO,KAAK,cAAc,EAAE,cAAc;;;;;;;AClB5C,SAAgB,aAA0B;CACxC,MAAM,aAAa,mBAAmB;AAEtC,KAAI,CAAC,WAAW,WAAW,CACzB,QAAO,qBAAqB;CAG9B,MAAM,MAAM,aAAa,YAAY,QAAQ;CAE7C,IAAI;AACJ,KAAI;AACF,WAAS,KAAK,MAAM,IAAI;SAClB;AAEN,SAAO,qBAAqB;;AAI9B,QAAO;EACL,GAAG,qBAAqB;EACxB,GAAG;EACJ;;AAGH,SAAgB,YAAY,QAA2B;CACrD,MAAM,aAAa,mBAAmB;CACtC,MAAM,MAAM,QAAQ,WAAW;AAG/B,WAAU,KAAK;EAAE,WAAW;EAAM,MAAM;EAAO,CAAC;CAOhD,MAAM,UAAU,KAAK,KAAK,iBADX,YAAY,EAAE,CAAC,SAAS,MAAM,CACK,MAAM;AAExD,KAAI;AACF,gBAAc,SAAS,KAAK,UAAU,QAAQ,MAAM,EAAE,GAAG,MAAM;GAC7D,UAAU;GAKV,MAAM;GACP,CAAC;AACF,aAAW,SAAS,WAAW;UACxB,KAAK;AAEZ,MAAI;AACF,cAAW,QAAQ;UACb;AAGR,QAAM;;;AAIV,SAAgB,aACd,SACa;CAEb,MAAM,UAAU,QADD,YAAY,CACI;AAC/B,aAAY,QAAQ;AACpB,QAAO;;;;;;;;;;ACxET,SAAgB,aAAa,aAAqC;CAChE,MAAM,SAAS,YAAY;CAC3B,MAAM,kBAAkB,eAAe,OAAO;AAC9C,KAAI,CAAC,gBAAiB,QAAO;CAE7B,MAAM,UAAU,OAAO,SAAS;AAChC,KAAI,CAAC,QAAS,QAAO;AAErB,QAAO,QAAQ;;;;;AAMjB,SAAgB,mBAAwC;CACtD,MAAM,SAAS,YAAY;AAC3B,KAAI,CAAC,OAAO,cAAe,QAAO;AAElC,QAAO,OAAO,SAAS,OAAO,kBAAkB;;;;;AAMlD,SAAgB,mBAA6B;CAC3C,MAAM,SAAS,YAAY;AAC3B,QAAO,OAAO,KAAK,OAAO,SAAS"}
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@fluid-app/fluid-cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Core CLI for Fluid Commerce — auth, config, and plugin system",
|
|
5
|
+
"bin": {
|
|
6
|
+
"fluid": "./dist/bin/fluid.mjs"
|
|
7
|
+
},
|
|
8
|
+
"files": [
|
|
9
|
+
"dist"
|
|
10
|
+
],
|
|
11
|
+
"type": "module",
|
|
12
|
+
"main": "./dist/index.mjs",
|
|
13
|
+
"types": "./dist/index.d.mts",
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"import": "./dist/index.mjs",
|
|
17
|
+
"types": "./dist/index.d.mts"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"publishConfig": {
|
|
21
|
+
"access": "public"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"chalk": "^5.3.0",
|
|
25
|
+
"commander": "^12.0.0",
|
|
26
|
+
"ora": "^8.0.0",
|
|
27
|
+
"prompts": "^2.4.2"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@types/node": "^24",
|
|
31
|
+
"@types/prompts": "^2.4.9",
|
|
32
|
+
"tsdown": "^0.21.0",
|
|
33
|
+
"typescript": "^5",
|
|
34
|
+
"vitest": "^4.0.18",
|
|
35
|
+
"@fluid-app/api-client-core": "0.1.0",
|
|
36
|
+
"@fluid-app/typescript-config": "0.0.0"
|
|
37
|
+
},
|
|
38
|
+
"engines": {
|
|
39
|
+
"node": ">=18.0.0"
|
|
40
|
+
},
|
|
41
|
+
"scripts": {
|
|
42
|
+
"build": "tsdown",
|
|
43
|
+
"dev": "tsdown --watch",
|
|
44
|
+
"lint": "oxlint",
|
|
45
|
+
"lint:fix": "oxlint --fix",
|
|
46
|
+
"typecheck": "tsgo --noEmit",
|
|
47
|
+
"test": "vitest run",
|
|
48
|
+
"test:watch": "vitest"
|
|
49
|
+
}
|
|
50
|
+
}
|