cawplan 0.0.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/config/products.json +17 -0
- package/dist/commands/activities.d.ts +2 -0
- package/dist/commands/activities.js +43 -0
- package/dist/commands/analytics.d.ts +2 -0
- package/dist/commands/analytics.js +30 -0
- package/dist/commands/auth.d.ts +2 -0
- package/dist/commands/auth.js +145 -0
- package/dist/commands/community.d.ts +2 -0
- package/dist/commands/community.js +30 -0
- package/dist/commands/critical.d.ts +2 -0
- package/dist/commands/critical.js +205 -0
- package/dist/commands/knowledge.d.ts +2 -0
- package/dist/commands/knowledge.js +24 -0
- package/dist/commands/metrics.d.ts +2 -0
- package/dist/commands/metrics.js +27 -0
- package/dist/commands/product-activity.d.ts +2 -0
- package/dist/commands/product-activity.js +26 -0
- package/dist/commands/products.d.ts +2 -0
- package/dist/commands/products.js +100 -0
- package/dist/commands/qa-reports.d.ts +2 -0
- package/dist/commands/qa-reports.js +71 -0
- package/dist/commands/tickets.d.ts +2 -0
- package/dist/commands/tickets.js +431 -0
- package/dist/commands/todos.d.ts +2 -0
- package/dist/commands/todos.js +24 -0
- package/dist/commands/user-activity.d.ts +2 -0
- package/dist/commands/user-activity.js +31 -0
- package/dist/commands/users.d.ts +2 -0
- package/dist/commands/users.js +80 -0
- package/dist/commands/versions.d.ts +2 -0
- package/dist/commands/versions.js +65 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +82 -0
- package/dist/lib/auth-state.d.ts +11 -0
- package/dist/lib/auth-state.js +27 -0
- package/dist/lib/cache.d.ts +20 -0
- package/dist/lib/cache.js +135 -0
- package/dist/lib/config.d.ts +8 -0
- package/dist/lib/config.js +25 -0
- package/dist/lib/credentials.d.ts +15 -0
- package/dist/lib/credentials.js +50 -0
- package/dist/lib/http.d.ts +14 -0
- package/dist/lib/http.js +174 -0
- package/dist/lib/oauth.d.ts +20 -0
- package/dist/lib/oauth.js +155 -0
- package/dist/lib/output.d.ts +3 -0
- package/dist/lib/output.js +9 -0
- package/dist/lib/products.d.ts +24 -0
- package/dist/lib/products.js +87 -0
- package/dist/lib/user-config.d.ts +10 -0
- package/dist/lib/user-config.js +47 -0
- package/package.json +49 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { cawplanRequest } from "../lib/http.js";
|
|
2
|
+
import { getCache, setCache, buildScopedCacheKey, buildQueryFromFlags } from "../lib/cache.js";
|
|
3
|
+
export function registerUsersCommand(program) {
|
|
4
|
+
const users = program.command("users").description("Manage users");
|
|
5
|
+
users
|
|
6
|
+
.command("list")
|
|
7
|
+
.description("List users")
|
|
8
|
+
.option("--search <q>", "Search query")
|
|
9
|
+
.option("--page_size <n>", "Page size")
|
|
10
|
+
.option("--page_num <n>", "Page number")
|
|
11
|
+
.option("--refresh", "Bypass cache")
|
|
12
|
+
.action(async (opts) => {
|
|
13
|
+
const flags = {};
|
|
14
|
+
if (opts.search)
|
|
15
|
+
flags.search = opts.search;
|
|
16
|
+
if (opts.page_size)
|
|
17
|
+
flags.page_size = opts.page_size;
|
|
18
|
+
if (opts.page_num)
|
|
19
|
+
flags.page_num = opts.page_num;
|
|
20
|
+
const refresh = Boolean(opts.refresh);
|
|
21
|
+
const query = buildQueryFromFlags(flags, ["search", "page_size", "page_num"]);
|
|
22
|
+
const key = await buildScopedCacheKey("users:list", query);
|
|
23
|
+
const cached = getCache(key, refresh);
|
|
24
|
+
if (cached) {
|
|
25
|
+
console.log(JSON.stringify(cached, null, 2));
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
const result = await cawplanRequest({
|
|
29
|
+
method: "GET",
|
|
30
|
+
path: "/api/v1/public/openapi/users",
|
|
31
|
+
query,
|
|
32
|
+
});
|
|
33
|
+
setCache(key, result);
|
|
34
|
+
console.log(JSON.stringify(result, null, 2));
|
|
35
|
+
});
|
|
36
|
+
users
|
|
37
|
+
.command("query")
|
|
38
|
+
.description("Query users by email or keyword")
|
|
39
|
+
.option("--email <email>", "User email")
|
|
40
|
+
.option("--keyword <q>", "Search keyword")
|
|
41
|
+
.option("--page_size <n>", "Page size")
|
|
42
|
+
.option("--page_num <n>", "Page number")
|
|
43
|
+
.option("--refresh", "Bypass cache")
|
|
44
|
+
.action(async (opts) => {
|
|
45
|
+
if (!opts.email && !opts.keyword) {
|
|
46
|
+
console.error("Error: users query requires --email or --keyword");
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
const refresh = Boolean(opts.refresh);
|
|
50
|
+
const key = await buildScopedCacheKey("users:query", {
|
|
51
|
+
email: opts.email || "",
|
|
52
|
+
keyword: opts.keyword || "",
|
|
53
|
+
page_num: opts.page_num || "",
|
|
54
|
+
page_size: opts.page_size || "",
|
|
55
|
+
});
|
|
56
|
+
const cached = getCache(key, refresh);
|
|
57
|
+
if (cached) {
|
|
58
|
+
console.log(JSON.stringify(cached, null, 2));
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const body = {};
|
|
62
|
+
if (opts.email) {
|
|
63
|
+
body.email = opts.email;
|
|
64
|
+
}
|
|
65
|
+
else if (opts.keyword) {
|
|
66
|
+
body.keyword = opts.keyword;
|
|
67
|
+
}
|
|
68
|
+
if (opts.page_num)
|
|
69
|
+
body.page_num = opts.page_num;
|
|
70
|
+
if (opts.page_size)
|
|
71
|
+
body.page_size = opts.page_size;
|
|
72
|
+
const result = await cawplanRequest({
|
|
73
|
+
method: "POST",
|
|
74
|
+
path: "/api/v1/public/openapi/users/query",
|
|
75
|
+
body,
|
|
76
|
+
});
|
|
77
|
+
setCache(key, result);
|
|
78
|
+
console.log(JSON.stringify(result, null, 2));
|
|
79
|
+
});
|
|
80
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { cawplanRequest } from "../lib/http.js";
|
|
2
|
+
import { buildQueryFromFlags } from "../lib/cache.js";
|
|
3
|
+
export function registerVersionsCommand(program) {
|
|
4
|
+
const versions = program.command("versions").description("Manage versions");
|
|
5
|
+
versions
|
|
6
|
+
.command("list <product_id>")
|
|
7
|
+
.description("List versions for a product")
|
|
8
|
+
.option("--page_size <n>", "Page size")
|
|
9
|
+
.option("--page_num <n>", "Page number")
|
|
10
|
+
.action(async (productId, opts) => {
|
|
11
|
+
const flags = {};
|
|
12
|
+
if (opts.page_size)
|
|
13
|
+
flags.page_size = opts.page_size;
|
|
14
|
+
if (opts.page_num)
|
|
15
|
+
flags.page_num = opts.page_num;
|
|
16
|
+
const query = buildQueryFromFlags(flags, ["page_size", "page_num"]);
|
|
17
|
+
const result = await cawplanRequest({
|
|
18
|
+
method: "GET",
|
|
19
|
+
path: `/api/v1/public/openapi/product/${productId}/versions`,
|
|
20
|
+
query,
|
|
21
|
+
});
|
|
22
|
+
console.log(JSON.stringify(result, null, 2));
|
|
23
|
+
});
|
|
24
|
+
versions
|
|
25
|
+
.command("get <product_id> <version_id>")
|
|
26
|
+
.description("Get version details")
|
|
27
|
+
.action(async (productId, versionId) => {
|
|
28
|
+
const result = await cawplanRequest({
|
|
29
|
+
method: "GET",
|
|
30
|
+
path: `/api/v1/public/openapi/product/${productId}/versions/${versionId}`,
|
|
31
|
+
});
|
|
32
|
+
console.log(JSON.stringify(result, null, 2));
|
|
33
|
+
});
|
|
34
|
+
versions
|
|
35
|
+
.command("create <product_id>")
|
|
36
|
+
.description("Create a new version")
|
|
37
|
+
.requiredOption("--name <version>", "Version name (e.g. X.Y.Z)")
|
|
38
|
+
.option("--major_id <id>", "Major version unique_id")
|
|
39
|
+
.option("--description <text>", "Description")
|
|
40
|
+
.action(async (productId, opts) => {
|
|
41
|
+
const body = { name: opts.name };
|
|
42
|
+
if (opts.major_id)
|
|
43
|
+
body.major_id = opts.major_id;
|
|
44
|
+
if (opts.description !== undefined)
|
|
45
|
+
body.description = opts.description;
|
|
46
|
+
const result = await cawplanRequest({
|
|
47
|
+
method: "POST",
|
|
48
|
+
path: `/api/v1/public/openapi/product/${productId}/versions`,
|
|
49
|
+
body,
|
|
50
|
+
});
|
|
51
|
+
console.log(JSON.stringify(result, null, 2));
|
|
52
|
+
});
|
|
53
|
+
// Releases subcommand lives naturally alongside versions
|
|
54
|
+
const releases = program.command("releases").description("Manage releases");
|
|
55
|
+
releases
|
|
56
|
+
.command("list <product_id> <version_id>")
|
|
57
|
+
.description("List releases for a version")
|
|
58
|
+
.action(async (productId, versionId) => {
|
|
59
|
+
const result = await cawplanRequest({
|
|
60
|
+
method: "GET",
|
|
61
|
+
path: `/api/v1/public/openapi/product/${productId}/versions/${versionId}/release`,
|
|
62
|
+
});
|
|
63
|
+
console.log(JSON.stringify(result, null, 2));
|
|
64
|
+
});
|
|
65
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
import { registerAuthCommand } from "./commands/auth.js";
|
|
5
|
+
import { registerProductsCommand } from "./commands/products.js";
|
|
6
|
+
import { registerVersionsCommand } from "./commands/versions.js";
|
|
7
|
+
import { registerTicketsCommand } from "./commands/tickets.js";
|
|
8
|
+
import { registerCriticalCommand } from "./commands/critical.js";
|
|
9
|
+
import { registerMetricsCommand } from "./commands/metrics.js";
|
|
10
|
+
import { registerTodosCommand } from "./commands/todos.js";
|
|
11
|
+
import { registerUsersCommand } from "./commands/users.js";
|
|
12
|
+
import { registerActivitiesCommand } from "./commands/activities.js";
|
|
13
|
+
import { registerUserActivityCommand } from "./commands/user-activity.js";
|
|
14
|
+
import { registerProductActivityCommand } from "./commands/product-activity.js";
|
|
15
|
+
import { registerKnowledgeCommand } from "./commands/knowledge.js";
|
|
16
|
+
import { registerAnalyticsCommand } from "./commands/analytics.js";
|
|
17
|
+
import { registerQAReportsCommand } from "./commands/qa-reports.js";
|
|
18
|
+
import { registerCommunityCommand } from "./commands/community.js";
|
|
19
|
+
const require = createRequire(import.meta.url);
|
|
20
|
+
const { version } = require("../package.json");
|
|
21
|
+
const program = new Command();
|
|
22
|
+
program
|
|
23
|
+
.name("cawplan")
|
|
24
|
+
.description("CawPlan CLI")
|
|
25
|
+
.version(version);
|
|
26
|
+
// Register all commands
|
|
27
|
+
registerAuthCommand(program);
|
|
28
|
+
registerProductsCommand(program);
|
|
29
|
+
registerVersionsCommand(program);
|
|
30
|
+
registerTicketsCommand(program);
|
|
31
|
+
registerCriticalCommand(program);
|
|
32
|
+
registerMetricsCommand(program);
|
|
33
|
+
registerTodosCommand(program);
|
|
34
|
+
registerUsersCommand(program);
|
|
35
|
+
registerActivitiesCommand(program);
|
|
36
|
+
registerUserActivityCommand(program);
|
|
37
|
+
registerProductActivityCommand(program);
|
|
38
|
+
registerKnowledgeCommand(program);
|
|
39
|
+
registerAnalyticsCommand(program);
|
|
40
|
+
registerQAReportsCommand(program);
|
|
41
|
+
registerCommunityCommand(program);
|
|
42
|
+
// Raw API passthrough
|
|
43
|
+
program
|
|
44
|
+
.command("api <method> <path>")
|
|
45
|
+
.description("Raw API request passthrough")
|
|
46
|
+
.option("--query <params>", "Query params as key=val&key2=val2")
|
|
47
|
+
.option("--body <json>", "Request body as JSON")
|
|
48
|
+
.action(async (method, path, opts) => {
|
|
49
|
+
const { cawplanRequest } = await import("./lib/http.js");
|
|
50
|
+
let body;
|
|
51
|
+
if (opts.body) {
|
|
52
|
+
try {
|
|
53
|
+
body = JSON.parse(opts.body);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
console.error("Error: --body must be valid JSON");
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const query = opts.query
|
|
61
|
+
? Object.fromEntries(new URLSearchParams(opts.query).entries())
|
|
62
|
+
: undefined;
|
|
63
|
+
const result = await cawplanRequest({
|
|
64
|
+
method: method.toUpperCase(),
|
|
65
|
+
path,
|
|
66
|
+
query,
|
|
67
|
+
body,
|
|
68
|
+
});
|
|
69
|
+
console.log(JSON.stringify(result, null, 2));
|
|
70
|
+
});
|
|
71
|
+
// Cache management
|
|
72
|
+
program
|
|
73
|
+
.command("cache")
|
|
74
|
+
.description("Manage local cache")
|
|
75
|
+
.command("clear")
|
|
76
|
+
.description("Clear the local cache")
|
|
77
|
+
.action(async () => {
|
|
78
|
+
const { clearCache } = await import("./lib/cache.js");
|
|
79
|
+
clearCache();
|
|
80
|
+
console.log(JSON.stringify({ code: "SUCCESS", msg: "cache cleared" }, null, 2));
|
|
81
|
+
});
|
|
82
|
+
await program.parseAsync(process.argv);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type Credentials } from "./credentials.js";
|
|
2
|
+
export type ActiveAuthKind = "oauth" | "apiKey" | "none";
|
|
3
|
+
export interface AuthState {
|
|
4
|
+
credentials: Credentials | null;
|
|
5
|
+
envApiKey?: string;
|
|
6
|
+
hasOAuth: boolean;
|
|
7
|
+
hasApiKey: boolean;
|
|
8
|
+
active: ActiveAuthKind;
|
|
9
|
+
}
|
|
10
|
+
export declare function getAuthState(): Promise<AuthState>;
|
|
11
|
+
export declare function isAuthenticated(): Promise<boolean>;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { getApiKey } from "./config.js";
|
|
2
|
+
import { isAccessTokenExpired, readCredentials } from "./credentials.js";
|
|
3
|
+
export async function getAuthState() {
|
|
4
|
+
const credentials = await readCredentials();
|
|
5
|
+
const envApiKey = getApiKey();
|
|
6
|
+
const hasOAuth = Boolean(credentials?.accessToken &&
|
|
7
|
+
(!isAccessTokenExpired(credentials) || credentials.refreshToken));
|
|
8
|
+
const hasApiKey = Boolean(credentials?.apiKey || envApiKey);
|
|
9
|
+
let active = "none";
|
|
10
|
+
if (hasOAuth) {
|
|
11
|
+
active = "oauth";
|
|
12
|
+
}
|
|
13
|
+
else if (hasApiKey) {
|
|
14
|
+
active = "apiKey";
|
|
15
|
+
}
|
|
16
|
+
return {
|
|
17
|
+
credentials,
|
|
18
|
+
envApiKey,
|
|
19
|
+
hasOAuth,
|
|
20
|
+
hasApiKey,
|
|
21
|
+
active,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
export async function isAuthenticated() {
|
|
25
|
+
const state = await getAuthState();
|
|
26
|
+
return state.active !== "none";
|
|
27
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
type CacheEntry = {
|
|
2
|
+
fetched_at: number;
|
|
3
|
+
data: unknown;
|
|
4
|
+
};
|
|
5
|
+
type CacheStore = {
|
|
6
|
+
version: 1;
|
|
7
|
+
entries: Record<string, CacheEntry>;
|
|
8
|
+
};
|
|
9
|
+
export declare function loadCache(): CacheStore;
|
|
10
|
+
export declare function saveCache(store: CacheStore): void;
|
|
11
|
+
export declare function getCache(key: string, refresh: boolean): unknown | undefined;
|
|
12
|
+
export declare function setCache(key: string, data: unknown): void;
|
|
13
|
+
export declare function clearCache(): void;
|
|
14
|
+
export declare function buildCacheKey(prefix: string, query: Record<string, string> | undefined): string;
|
|
15
|
+
export declare function getCacheScope(): Promise<string>;
|
|
16
|
+
export declare function buildScopedCacheKey(prefix: string, query: Record<string, string> | undefined): Promise<string>;
|
|
17
|
+
export declare function stableStringify(value: unknown): string;
|
|
18
|
+
export declare function buildQueryFromFlags(flags: Record<string, string>, allow: string[]): Record<string, string> | undefined;
|
|
19
|
+
export declare function csvToArray(value: string | undefined): string[] | undefined;
|
|
20
|
+
export {};
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import { getBaseUrl, getCachePath, getCacheTtlMs } from "./config.js";
|
|
5
|
+
import { readCredentials } from "./credentials.js";
|
|
6
|
+
export function loadCache() {
|
|
7
|
+
const cachePath = getCachePath();
|
|
8
|
+
if (!existsSync(cachePath))
|
|
9
|
+
return { version: 1, entries: {} };
|
|
10
|
+
try {
|
|
11
|
+
const raw = readFileSync(cachePath, "utf-8");
|
|
12
|
+
const parsed = JSON.parse(raw);
|
|
13
|
+
if (parsed && parsed.version === 1 && typeof parsed.entries === "object") {
|
|
14
|
+
return parsed;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
// Ignore cache read/parse errors
|
|
19
|
+
}
|
|
20
|
+
return { version: 1, entries: {} };
|
|
21
|
+
}
|
|
22
|
+
export function saveCache(store) {
|
|
23
|
+
const cachePath = getCachePath();
|
|
24
|
+
try {
|
|
25
|
+
mkdirSync(resolve(cachePath, ".."), { recursive: true });
|
|
26
|
+
writeFileSync(cachePath, JSON.stringify(store, null, 2));
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
// Ignore cache write errors
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export function getCache(key, refresh) {
|
|
33
|
+
if (refresh)
|
|
34
|
+
return undefined;
|
|
35
|
+
const store = loadCache();
|
|
36
|
+
const entry = store.entries[key];
|
|
37
|
+
if (!entry)
|
|
38
|
+
return undefined;
|
|
39
|
+
const ttlMs = getCacheTtlMs();
|
|
40
|
+
if (Date.now() - entry.fetched_at > ttlMs)
|
|
41
|
+
return undefined;
|
|
42
|
+
return entry.data;
|
|
43
|
+
}
|
|
44
|
+
export function setCache(key, data) {
|
|
45
|
+
const store = loadCache();
|
|
46
|
+
store.entries[key] = { fetched_at: Date.now(), data };
|
|
47
|
+
saveCache(store);
|
|
48
|
+
}
|
|
49
|
+
export function clearCache() {
|
|
50
|
+
const cachePath = getCachePath();
|
|
51
|
+
try {
|
|
52
|
+
if (existsSync(cachePath)) {
|
|
53
|
+
unlinkSync(cachePath);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
// Ignore cache delete errors
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
export function buildCacheKey(prefix, query) {
|
|
61
|
+
if (!query)
|
|
62
|
+
return prefix;
|
|
63
|
+
const normalized = Object.keys(query)
|
|
64
|
+
.sort()
|
|
65
|
+
.map((k) => `${k}=${query[k]}`)
|
|
66
|
+
.join("&");
|
|
67
|
+
return `${prefix}?${normalized}`;
|
|
68
|
+
}
|
|
69
|
+
function shortHash(value) {
|
|
70
|
+
return createHash("sha256").update(value).digest("hex").slice(0, 16);
|
|
71
|
+
}
|
|
72
|
+
function decodeJwtPayload(token) {
|
|
73
|
+
const payload = token.split(".")[1];
|
|
74
|
+
if (!payload)
|
|
75
|
+
return null;
|
|
76
|
+
try {
|
|
77
|
+
return JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
export async function getCacheScope() {
|
|
84
|
+
const base = `base:${shortHash(getBaseUrl())}`;
|
|
85
|
+
const credentials = await readCredentials();
|
|
86
|
+
if (credentials?.accessToken) {
|
|
87
|
+
const payload = decodeJwtPayload(credentials.accessToken);
|
|
88
|
+
const workspaceId = payload?.workspace_id;
|
|
89
|
+
if (typeof workspaceId === "string" && workspaceId.trim()) {
|
|
90
|
+
return `${base}:workspace:${workspaceId}`;
|
|
91
|
+
}
|
|
92
|
+
const userId = payload?.uid_id ?? payload?.user_id ?? payload?.sub ?? credentials.email;
|
|
93
|
+
if (typeof userId === "string" && userId.trim()) {
|
|
94
|
+
return `${base}:oauth:${shortHash(userId)}`;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (credentials?.apiKey) {
|
|
98
|
+
return `${base}:api-key:${shortHash(credentials.apiKey)}`;
|
|
99
|
+
}
|
|
100
|
+
return `${base}:anonymous`;
|
|
101
|
+
}
|
|
102
|
+
export async function buildScopedCacheKey(prefix, query) {
|
|
103
|
+
const scope = await getCacheScope();
|
|
104
|
+
return `${scope}:${buildCacheKey(prefix, query)}`;
|
|
105
|
+
}
|
|
106
|
+
export function stableStringify(value) {
|
|
107
|
+
if (value === null || typeof value !== "object") {
|
|
108
|
+
return JSON.stringify(value);
|
|
109
|
+
}
|
|
110
|
+
if (Array.isArray(value)) {
|
|
111
|
+
return `[${value.map((item) => stableStringify(item)).join(",")}]`;
|
|
112
|
+
}
|
|
113
|
+
const obj = value;
|
|
114
|
+
const keys = Object.keys(obj).sort();
|
|
115
|
+
const entries = keys.map((key) => `${JSON.stringify(key)}:${stableStringify(obj[key])}`);
|
|
116
|
+
return `{${entries.join(",")}}`;
|
|
117
|
+
}
|
|
118
|
+
export function buildQueryFromFlags(flags, allow) {
|
|
119
|
+
const query = {};
|
|
120
|
+
for (const key of allow) {
|
|
121
|
+
if (flags[key] !== undefined) {
|
|
122
|
+
query[key] = flags[key];
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return Object.keys(query).length ? query : undefined;
|
|
126
|
+
}
|
|
127
|
+
export function csvToArray(value) {
|
|
128
|
+
if (!value)
|
|
129
|
+
return undefined;
|
|
130
|
+
const items = value
|
|
131
|
+
.split(",")
|
|
132
|
+
.map((item) => item.trim())
|
|
133
|
+
.filter(Boolean);
|
|
134
|
+
return items.length ? items : undefined;
|
|
135
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API base URL. Reads from products.json (env selected by CAWPLAN_ENV),
|
|
3
|
+
* overridable via CAWPLAN_BASE_URL for advanced use / debugging.
|
|
4
|
+
*/
|
|
5
|
+
export declare function getBaseUrl(): string;
|
|
6
|
+
export declare function getApiKey(): string | undefined;
|
|
7
|
+
export declare function getCachePath(): string;
|
|
8
|
+
export declare function getCacheTtlMs(): number;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { getApiBase } from "./products.js";
|
|
4
|
+
/**
|
|
5
|
+
* API base URL. Reads from products.json (env selected by CAWPLAN_ENV),
|
|
6
|
+
* overridable via CAWPLAN_BASE_URL for advanced use / debugging.
|
|
7
|
+
*/
|
|
8
|
+
export function getBaseUrl() {
|
|
9
|
+
return getApiBase();
|
|
10
|
+
}
|
|
11
|
+
export function getApiKey() {
|
|
12
|
+
return process.env.CAWPLAN_API_KEY;
|
|
13
|
+
}
|
|
14
|
+
export function getCachePath() {
|
|
15
|
+
return process.env.CAWPLAN_CACHE_PATH ?? resolve(homedir(), ".cawplan", "cache.json");
|
|
16
|
+
}
|
|
17
|
+
export function getCacheTtlMs() {
|
|
18
|
+
const hoursRaw = process.env.CAWPLAN_CACHE_TTL_HOURS;
|
|
19
|
+
if (hoursRaw !== undefined) {
|
|
20
|
+
const hours = Number(hoursRaw);
|
|
21
|
+
if (Number.isFinite(hours) && hours > 0)
|
|
22
|
+
return hours * 60 * 60 * 1000;
|
|
23
|
+
}
|
|
24
|
+
return 12 * 60 * 60 * 1000;
|
|
25
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface Credentials {
|
|
2
|
+
apiKey?: string;
|
|
3
|
+
accessToken?: string;
|
|
4
|
+
refreshToken?: string;
|
|
5
|
+
/** Unix timestamp (seconds) when access token expires */
|
|
6
|
+
expire?: number;
|
|
7
|
+
/** Email of the authenticated user (from OAuth) */
|
|
8
|
+
email?: string;
|
|
9
|
+
}
|
|
10
|
+
export declare const CREDENTIALS_PATH: string;
|
|
11
|
+
export declare function getCredentialsPath(): string;
|
|
12
|
+
export declare function isAccessTokenExpired(credentials: Credentials, nowSeconds?: number): boolean;
|
|
13
|
+
export declare function readCredentials(): Promise<Credentials | null>;
|
|
14
|
+
export declare function writeCredentials(creds: Credentials): Promise<void>;
|
|
15
|
+
export declare function deleteCredentials(): Promise<void>;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { chmod, mkdir, readFile, unlink, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
const CREDENTIALS_MODE = 0o600;
|
|
5
|
+
export const CREDENTIALS_PATH = join(homedir(), ".cawplan", "credentials.json");
|
|
6
|
+
export function getCredentialsPath() {
|
|
7
|
+
return process.env.CAWPLAN_CREDENTIALS_PATH ?? CREDENTIALS_PATH;
|
|
8
|
+
}
|
|
9
|
+
export function isAccessTokenExpired(credentials, nowSeconds = Math.floor(Date.now() / 1000)) {
|
|
10
|
+
if (credentials.expire == null)
|
|
11
|
+
return true;
|
|
12
|
+
return credentials.expire <= nowSeconds;
|
|
13
|
+
}
|
|
14
|
+
export async function readCredentials() {
|
|
15
|
+
try {
|
|
16
|
+
const raw = await readFile(getCredentialsPath(), "utf8");
|
|
17
|
+
const parsed = JSON.parse(raw);
|
|
18
|
+
// Return whatever fields are present; no required fields check
|
|
19
|
+
return {
|
|
20
|
+
apiKey: parsed.apiKey,
|
|
21
|
+
accessToken: parsed.accessToken,
|
|
22
|
+
refreshToken: parsed.refreshToken,
|
|
23
|
+
expire: parsed.expire,
|
|
24
|
+
email: parsed.email,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
catch (err) {
|
|
28
|
+
if (err.code === "ENOENT") {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
throw err;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
export async function writeCredentials(creds) {
|
|
35
|
+
const credentialsPath = getCredentialsPath();
|
|
36
|
+
await mkdir(dirname(credentialsPath), { recursive: true });
|
|
37
|
+
const payload = `${JSON.stringify(creds, null, 2)}\n`;
|
|
38
|
+
await writeFile(credentialsPath, payload, { encoding: "utf8", mode: CREDENTIALS_MODE });
|
|
39
|
+
await chmod(credentialsPath, CREDENTIALS_MODE);
|
|
40
|
+
}
|
|
41
|
+
export async function deleteCredentials() {
|
|
42
|
+
try {
|
|
43
|
+
await unlink(getCredentialsPath());
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
if (err.code !== "ENOENT") {
|
|
47
|
+
throw err;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export declare class ApiError extends Error {
|
|
2
|
+
readonly status: number;
|
|
3
|
+
readonly body?: unknown | undefined;
|
|
4
|
+
constructor(message: string, status: number, body?: unknown | undefined);
|
|
5
|
+
}
|
|
6
|
+
type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
|
|
7
|
+
export interface RequestOptions {
|
|
8
|
+
method?: HttpMethod;
|
|
9
|
+
path: string;
|
|
10
|
+
query?: Record<string, string>;
|
|
11
|
+
body?: unknown;
|
|
12
|
+
}
|
|
13
|
+
export declare function cawplanRequest(options: RequestOptions): Promise<unknown>;
|
|
14
|
+
export {};
|