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.
Files changed (52) hide show
  1. package/config/products.json +17 -0
  2. package/dist/commands/activities.d.ts +2 -0
  3. package/dist/commands/activities.js +43 -0
  4. package/dist/commands/analytics.d.ts +2 -0
  5. package/dist/commands/analytics.js +30 -0
  6. package/dist/commands/auth.d.ts +2 -0
  7. package/dist/commands/auth.js +145 -0
  8. package/dist/commands/community.d.ts +2 -0
  9. package/dist/commands/community.js +30 -0
  10. package/dist/commands/critical.d.ts +2 -0
  11. package/dist/commands/critical.js +205 -0
  12. package/dist/commands/knowledge.d.ts +2 -0
  13. package/dist/commands/knowledge.js +24 -0
  14. package/dist/commands/metrics.d.ts +2 -0
  15. package/dist/commands/metrics.js +27 -0
  16. package/dist/commands/product-activity.d.ts +2 -0
  17. package/dist/commands/product-activity.js +26 -0
  18. package/dist/commands/products.d.ts +2 -0
  19. package/dist/commands/products.js +100 -0
  20. package/dist/commands/qa-reports.d.ts +2 -0
  21. package/dist/commands/qa-reports.js +71 -0
  22. package/dist/commands/tickets.d.ts +2 -0
  23. package/dist/commands/tickets.js +431 -0
  24. package/dist/commands/todos.d.ts +2 -0
  25. package/dist/commands/todos.js +24 -0
  26. package/dist/commands/user-activity.d.ts +2 -0
  27. package/dist/commands/user-activity.js +31 -0
  28. package/dist/commands/users.d.ts +2 -0
  29. package/dist/commands/users.js +80 -0
  30. package/dist/commands/versions.d.ts +2 -0
  31. package/dist/commands/versions.js +65 -0
  32. package/dist/index.d.ts +2 -0
  33. package/dist/index.js +82 -0
  34. package/dist/lib/auth-state.d.ts +11 -0
  35. package/dist/lib/auth-state.js +27 -0
  36. package/dist/lib/cache.d.ts +20 -0
  37. package/dist/lib/cache.js +135 -0
  38. package/dist/lib/config.d.ts +8 -0
  39. package/dist/lib/config.js +25 -0
  40. package/dist/lib/credentials.d.ts +15 -0
  41. package/dist/lib/credentials.js +50 -0
  42. package/dist/lib/http.d.ts +14 -0
  43. package/dist/lib/http.js +174 -0
  44. package/dist/lib/oauth.d.ts +20 -0
  45. package/dist/lib/oauth.js +155 -0
  46. package/dist/lib/output.d.ts +3 -0
  47. package/dist/lib/output.js +9 -0
  48. package/dist/lib/products.d.ts +24 -0
  49. package/dist/lib/products.js +87 -0
  50. package/dist/lib/user-config.d.ts +10 -0
  51. package/dist/lib/user-config.js +47 -0
  52. package/package.json +49 -0
@@ -0,0 +1,174 @@
1
+ import { getBaseUrl, getApiKey } from "./config.js";
2
+ import { readCredentials, writeCredentials, isAccessTokenExpired, } from "./credentials.js";
3
+ export class ApiError extends Error {
4
+ status;
5
+ body;
6
+ constructor(message, status, body) {
7
+ super(message);
8
+ this.status = status;
9
+ this.body = body;
10
+ this.name = "ApiError";
11
+ }
12
+ }
13
+ function normalizeBaseUrl(base) {
14
+ let url = base.trim();
15
+ if (!url.startsWith("http://") && !url.startsWith("https://")) {
16
+ url = `https://${url}`;
17
+ }
18
+ return url.replace(/\/$/, "");
19
+ }
20
+ function normalizePath(path) {
21
+ const trimmed = path.trim();
22
+ if (trimmed.startsWith("http://") || trimmed.startsWith("https://")) {
23
+ throw new Error("path must be relative, not a full URL");
24
+ }
25
+ return trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
26
+ }
27
+ async function refreshAccessToken(refreshToken) {
28
+ const baseUrl = normalizeBaseUrl(getBaseUrl());
29
+ const url = `${baseUrl}/api/v1/cli/oauth/refresh`;
30
+ const res = await fetch(url, {
31
+ method: "POST",
32
+ headers: { "Content-Type": "application/json" },
33
+ body: JSON.stringify({ refresh_token: refreshToken }),
34
+ });
35
+ const data = await res.json().catch(() => ({}));
36
+ if (!res.ok) {
37
+ throw new ApiError("Token expired, run: cawplan auth login", res.status, data);
38
+ }
39
+ // BE response: { code, data: { access_token, expire } }
40
+ // refresh_token is slid server-side; the same token remains valid.
41
+ const payload = (data.data ?? data);
42
+ if (!payload.access_token || typeof payload.expire !== "number") {
43
+ throw new ApiError("Token refresh failed: unexpected response", res.status, data);
44
+ }
45
+ return {
46
+ accessToken: payload.access_token,
47
+ refreshToken, // unchanged — server slides TTL in place
48
+ expire: payload.expire,
49
+ };
50
+ }
51
+ async function refreshStoredAccessToken(credentials) {
52
+ if (!credentials.refreshToken) {
53
+ throw new ApiError("Session expired. Run: cawplan auth login", 401);
54
+ }
55
+ const refreshed = await refreshAccessToken(credentials.refreshToken);
56
+ const nextCredentials = {
57
+ ...credentials,
58
+ accessToken: refreshed.accessToken,
59
+ refreshToken: refreshed.refreshToken,
60
+ expire: refreshed.expire,
61
+ };
62
+ await writeCredentials(nextCredentials);
63
+ return {
64
+ accessToken: refreshed.accessToken,
65
+ credentials: nextCredentials,
66
+ };
67
+ }
68
+ async function resolveAuthContext() {
69
+ const creds = await readCredentials();
70
+ // Priority 1: valid access token
71
+ if (creds?.accessToken && !isAccessTokenExpired(creds)) {
72
+ return {
73
+ header: `Bearer ${creds.accessToken}`,
74
+ kind: "oauth",
75
+ credentials: creds,
76
+ };
77
+ }
78
+ // Priority 2: expired access token with refresh token -> auto-refresh
79
+ if (creds?.accessToken && creds.refreshToken && isAccessTokenExpired(creds)) {
80
+ try {
81
+ const refreshed = await refreshStoredAccessToken(creds);
82
+ return {
83
+ header: `Bearer ${refreshed.accessToken}`,
84
+ kind: "oauth",
85
+ credentials: refreshed.credentials,
86
+ };
87
+ }
88
+ catch {
89
+ // Fall through to API key
90
+ }
91
+ }
92
+ // Priority 3: API key from credentials file or env var
93
+ const apiKey = creds?.apiKey ?? getApiKey();
94
+ if (apiKey) {
95
+ return {
96
+ header: apiKey,
97
+ kind: "apiKey",
98
+ credentials: creds,
99
+ };
100
+ }
101
+ return null;
102
+ }
103
+ async function readResponsePayload(res) {
104
+ const contentType = res.headers.get("content-type") || "";
105
+ return contentType.includes("application/json")
106
+ ? await res.json().catch(() => ({}))
107
+ : await res.text();
108
+ }
109
+ function responseMessage(payload) {
110
+ if (typeof payload === "object" && payload !== null) {
111
+ const body = payload;
112
+ const message = body.msg ?? body.message ?? body.code;
113
+ if (typeof message === "string" && message.trim())
114
+ return message;
115
+ }
116
+ return String(payload || "unknown");
117
+ }
118
+ export async function cawplanRequest(options) {
119
+ const baseUrl = normalizeBaseUrl(getBaseUrl());
120
+ const url = new URL(`${baseUrl}${normalizePath(options.path)}`);
121
+ if (options.query) {
122
+ for (const [key, value] of Object.entries(options.query)) {
123
+ url.searchParams.set(key, value);
124
+ }
125
+ }
126
+ let auth = await resolveAuthContext();
127
+ if (!auth) {
128
+ console.error("Not authenticated. Run: cawplan auth login (or: cawplan auth configure)");
129
+ process.exit(1);
130
+ }
131
+ const method = options.method ?? "GET";
132
+ const body = options.body !== undefined ? JSON.stringify(options.body) : undefined;
133
+ const fetchWithAuth = async (authHeader) => {
134
+ const headers = {
135
+ Authorization: authHeader,
136
+ accept: "application/json",
137
+ };
138
+ if (options.body !== undefined) {
139
+ headers["content-type"] = "application/json";
140
+ }
141
+ return fetch(url.toString(), { method, headers, body });
142
+ };
143
+ let res = await fetchWithAuth(auth.header);
144
+ let payload = await readResponsePayload(res);
145
+ if (res.status === 401) {
146
+ if (auth.kind === "oauth") {
147
+ try {
148
+ const refreshed = await refreshStoredAccessToken(auth.credentials ?? {});
149
+ auth = {
150
+ header: `Bearer ${refreshed.accessToken}`,
151
+ kind: "oauth",
152
+ credentials: refreshed.credentials,
153
+ };
154
+ res = await fetchWithAuth(auth.header);
155
+ payload = await readResponsePayload(res);
156
+ if (res.status === 401) {
157
+ throw new ApiError("Session expired. Run: cawplan auth login", 401, payload);
158
+ }
159
+ }
160
+ catch {
161
+ throw new ApiError("Session expired. Run: cawplan auth login", 401, payload);
162
+ }
163
+ }
164
+ else {
165
+ throw new ApiError("API Key invalid. Run: cawplan auth configure", 401, payload);
166
+ }
167
+ }
168
+ if (!res.ok) {
169
+ const msg = responseMessage(payload);
170
+ const err = new ApiError(`API error ${res.status}: ${msg}`, res.status, payload);
171
+ throw err;
172
+ }
173
+ return payload;
174
+ }
@@ -0,0 +1,20 @@
1
+ import type { Credentials } from "./credentials.js";
2
+ export declare const OAUTH_TIMEOUT_MS: number;
3
+ export declare const OAUTH_TIMEOUT_ERROR = "oauth_timeout";
4
+ export declare const OAUTH_DENIED_ERROR = "access_denied";
5
+ export interface CallbackResult {
6
+ stateToken?: string;
7
+ error?: string;
8
+ }
9
+ export interface OAuthLoginOptions {
10
+ openBrowser?: (url: string) => Promise<void>;
11
+ callbackTimeoutMs?: number;
12
+ }
13
+ export declare function openBrowser(url: string): Promise<void>;
14
+ export declare function waitForOAuthCallback(host?: string, timeoutMs?: number): Promise<{
15
+ port: number;
16
+ result: Promise<CallbackResult>;
17
+ }>;
18
+ export declare function buildConsentUrl(redirectUri: string, state: string): string;
19
+ export declare function exchangeStateToken(stateToken: string): Promise<Credentials>;
20
+ export declare function runOAuthLogin(options?: OAuthLoginOptions): Promise<Credentials>;
@@ -0,0 +1,155 @@
1
+ import { execFile } from "node:child_process";
2
+ import { createServer } from "node:http";
3
+ import { promisify } from "node:util";
4
+ import { randomBytes } from "node:crypto";
5
+ import { getBaseUrl } from "./config.js";
6
+ import { getPortalBase } from "./products.js";
7
+ const execFileAsync = promisify(execFile);
8
+ export const OAUTH_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
9
+ export const OAUTH_TIMEOUT_ERROR = "oauth_timeout";
10
+ export const OAUTH_DENIED_ERROR = "access_denied";
11
+ // ─── Browser opener ──────────────────────────────────────────────────────────
12
+ export async function openBrowser(url) {
13
+ const platform = process.platform;
14
+ if (platform === "darwin") {
15
+ await execFileAsync("open", [url]);
16
+ return;
17
+ }
18
+ if (platform === "win32") {
19
+ await execFileAsync("cmd", ["/c", "start", "", url]);
20
+ return;
21
+ }
22
+ await execFileAsync("xdg-open", [url]);
23
+ }
24
+ // ─── Local callback server ───────────────────────────────────────────────────
25
+ export function waitForOAuthCallback(host = "127.0.0.1", timeoutMs = OAUTH_TIMEOUT_MS) {
26
+ let resolveCallback;
27
+ const result = new Promise((resolve) => {
28
+ resolveCallback = resolve;
29
+ });
30
+ return new Promise((resolve, reject) => {
31
+ let timer;
32
+ const cleanup = (server) => {
33
+ if (timer)
34
+ clearTimeout(timer);
35
+ server.close();
36
+ };
37
+ const server = createServer((req, res) => {
38
+ if (!req.url?.startsWith("/callback")) {
39
+ res.statusCode = 404;
40
+ res.end("Not found");
41
+ return;
42
+ }
43
+ const requestUrl = new URL(req.url, `http://${host}`);
44
+ const error = requestUrl.searchParams.get("error") ?? undefined;
45
+ const stateToken = requestUrl.searchParams.get("state_token") ?? undefined;
46
+ const message = error
47
+ ? `Authentication failed: ${error}.`
48
+ : !stateToken
49
+ ? "Authentication failed: missing state token."
50
+ : "Authentication successful.";
51
+ res.statusCode = 200;
52
+ res.setHeader("Content-Type", "text/plain; charset=utf-8");
53
+ res.setHeader("Connection", "close");
54
+ res.end(`${message} You can close this tab and return to the terminal.`);
55
+ cleanup(server);
56
+ if (error) {
57
+ resolveCallback({ error });
58
+ return;
59
+ }
60
+ if (!stateToken) {
61
+ resolveCallback({ error: "missing_state_token" });
62
+ return;
63
+ }
64
+ resolveCallback({ stateToken });
65
+ });
66
+ server.once("error", reject);
67
+ server.listen(0, host, () => {
68
+ const address = server.address();
69
+ if (!address || typeof address === "string") {
70
+ reject(new Error("failed to bind local OAuth callback server"));
71
+ return;
72
+ }
73
+ timer = setTimeout(() => {
74
+ cleanup(server);
75
+ resolveCallback({ error: OAUTH_TIMEOUT_ERROR });
76
+ }, timeoutMs);
77
+ resolve({ port: address.port, result });
78
+ });
79
+ });
80
+ }
81
+ // ─── Consent URL builder ─────────────────────────────────────────────────────
82
+ //
83
+ // Points to the CawPlan web portal's /cli/auth page — NOT the BE API directly.
84
+ // The portal page is already authenticated in the user's browser; it calls
85
+ // POST /api/v1/cli/oauth/consent with the user's JWT, then redirects the browser
86
+ // to the CLI localhost callback with the returned state_token.
87
+ export function buildConsentUrl(redirectUri, state) {
88
+ const portalBase = getPortalBase().replace(/\/$/, "");
89
+ const params = new URLSearchParams({
90
+ client: "cawplan-cli",
91
+ redirect_uri: redirectUri,
92
+ state,
93
+ });
94
+ return `${portalBase}/cli/auth?${params.toString()}`;
95
+ }
96
+ // ─── Token exchange ───────────────────────────────────────────────────────────
97
+ function responseMessage(body) {
98
+ const message = body.msg ?? body.message ?? body.code;
99
+ return typeof message === "string" && message.trim() ? message : "unknown error";
100
+ }
101
+ export async function exchangeStateToken(stateToken) {
102
+ const base = getBaseUrl().replace(/\/$/, "");
103
+ const url = `${base}/api/v1/cli/oauth/exchange`;
104
+ const res = await fetch(url, {
105
+ method: "POST",
106
+ headers: { "Content-Type": "application/json" },
107
+ body: JSON.stringify({ state_token: stateToken }),
108
+ });
109
+ const body = await res.json().catch(() => ({}));
110
+ if (!res.ok) {
111
+ throw new Error(`Exchange failed (${res.status}): ${responseMessage(body)}`);
112
+ }
113
+ const data = (body.data ?? {});
114
+ const accessToken = data.access_token;
115
+ const refreshToken = data.refresh_token;
116
+ const expire = data.expire;
117
+ const user = data.user;
118
+ if (!accessToken || !refreshToken || typeof expire !== "number") {
119
+ throw new Error("Exchange response missing required token fields");
120
+ }
121
+ return {
122
+ accessToken,
123
+ refreshToken,
124
+ expire,
125
+ email: user?.email,
126
+ };
127
+ }
128
+ // ─── Full login flow ──────────────────────────────────────────────────────────
129
+ export async function runOAuthLogin(options) {
130
+ const host = "127.0.0.1";
131
+ const { port, result } = await waitForOAuthCallback(host, options?.callbackTimeoutMs);
132
+ const redirectUri = `http://${host}:${port}/callback`;
133
+ const state = randomBytes(16).toString("hex");
134
+ const consentUrl = buildConsentUrl(redirectUri, state);
135
+ const open = options?.openBrowser ?? openBrowser;
136
+ console.error(`Opening browser for authentication...`);
137
+ console.error(`If the browser does not open, visit:\n ${consentUrl}`);
138
+ try {
139
+ await open(consentUrl);
140
+ }
141
+ catch {
142
+ // Browser open failed — user can still visit the URL manually
143
+ }
144
+ const callback = await result;
145
+ if (callback.error === OAUTH_DENIED_ERROR) {
146
+ throw new Error("Authentication was denied.");
147
+ }
148
+ if (callback.error === OAUTH_TIMEOUT_ERROR) {
149
+ throw new Error("Authentication timed out (5 minutes). Run: cawplan auth login");
150
+ }
151
+ if (callback.error || !callback.stateToken) {
152
+ throw new Error(`Authentication failed: ${callback.error ?? "missing state token"}`);
153
+ }
154
+ return exchangeStateToken(callback.stateToken);
155
+ }
@@ -0,0 +1,3 @@
1
+ export declare function success(msg: string): void;
2
+ export declare function info(msg: string): void;
3
+ export declare function error(msg: string): void;
@@ -0,0 +1,9 @@
1
+ export function success(msg) {
2
+ console.log(msg);
3
+ }
4
+ export function info(msg) {
5
+ console.error(msg);
6
+ }
7
+ export function error(msg) {
8
+ console.error(`Error: ${msg}`);
9
+ }
@@ -0,0 +1,24 @@
1
+ export interface EnvConfig {
2
+ portalBase: string;
3
+ apiBase: string;
4
+ }
5
+ export declare function getEnvNames(): string[];
6
+ export declare function getDefaultEnvName(): string;
7
+ export declare function getProductEnvConfig(envName: string): EnvConfig;
8
+ /**
9
+ * API base URL for the cawplan backend.
10
+ * Priority: CAWPLAN_BASE_URL env var > CAWPLAN_ENV profile > ~/.cawplan/config.json > products.json.
11
+ */
12
+ export declare function getApiBase(): string;
13
+ /**
14
+ * Portal (web app) base URL.
15
+ * Priority: CAWPLAN_PORTAL_URL env var > CAWPLAN_ENV profile > ~/.cawplan/config.json > products.json.
16
+ */
17
+ export declare function getPortalBase(): string;
18
+ /**
19
+ * Normalize a core-product API path for the configured base URL.
20
+ * Paths are always /api/v1/... relative to the service root (direct BE or gateway .../core-product).
21
+ */
22
+ export declare function resolveApiPath(path: string): string;
23
+ /** True when apiBase already includes the gateway /core-product suffix. */
24
+ export declare function apiBaseUsesGatewayPrefix(): boolean;
@@ -0,0 +1,87 @@
1
+ import { createRequire } from "node:module";
2
+ import { fileURLToPath } from "node:url";
3
+ import { dirname, resolve } from "node:path";
4
+ import { readUserConfigSync } from "./user-config.js";
5
+ const _require = createRequire(import.meta.url);
6
+ // ─── Loader ───────────────────────────────────────────────────────────────────
7
+ function loadProductConfig() {
8
+ const __dirname = dirname(fileURLToPath(import.meta.url));
9
+ const data = _require(resolve(__dirname, "../../config/products.json"));
10
+ const product = data.products["cawplan"];
11
+ if (!product)
12
+ throw new Error("products.json: missing 'cawplan' entry");
13
+ return product;
14
+ }
15
+ export function getEnvNames() {
16
+ return Object.keys(loadProductConfig().env);
17
+ }
18
+ export function getDefaultEnvName() {
19
+ return loadProductConfig().defaultEnv;
20
+ }
21
+ export function getProductEnvConfig(envName) {
22
+ const product = loadProductConfig();
23
+ const envConfig = product.env[envName];
24
+ if (!envConfig)
25
+ throw new Error(`products.json: unknown env '${envName}'`);
26
+ return envConfig;
27
+ }
28
+ function getSelectedEnvName() {
29
+ const product = loadProductConfig();
30
+ const userConfig = readUserConfigSync();
31
+ return process.env.CAWPLAN_ENV ?? userConfig?.env ?? product.defaultEnv;
32
+ }
33
+ function loadEnvConfig() {
34
+ const product = loadProductConfig();
35
+ const envName = getSelectedEnvName();
36
+ const envConfig = product.env[envName] ?? product.env[product.defaultEnv];
37
+ if (!envConfig)
38
+ throw new Error(`products.json: unknown env '${envName}'`);
39
+ return envConfig;
40
+ }
41
+ // ─── Public API ───────────────────────────────────────────────────────────────
42
+ /**
43
+ * API base URL for the cawplan backend.
44
+ * Priority: CAWPLAN_BASE_URL env var > CAWPLAN_ENV profile > ~/.cawplan/config.json > products.json.
45
+ */
46
+ export function getApiBase() {
47
+ const userConfig = readUserConfigSync();
48
+ if (process.env.CAWPLAN_BASE_URL)
49
+ return process.env.CAWPLAN_BASE_URL;
50
+ if (process.env.CAWPLAN_ENV)
51
+ return loadEnvConfig().apiBase;
52
+ return userConfig?.baseUrl ?? loadEnvConfig().apiBase;
53
+ }
54
+ /**
55
+ * Portal (web app) base URL.
56
+ * Priority: CAWPLAN_PORTAL_URL env var > CAWPLAN_ENV profile > ~/.cawplan/config.json > products.json.
57
+ */
58
+ export function getPortalBase() {
59
+ const userConfig = readUserConfigSync();
60
+ if (process.env.CAWPLAN_PORTAL_URL)
61
+ return process.env.CAWPLAN_PORTAL_URL;
62
+ if (process.env.CAWPLAN_ENV)
63
+ return loadEnvConfig().portalBase;
64
+ return userConfig?.portalUrl ?? loadEnvConfig().portalBase;
65
+ }
66
+ /**
67
+ * Normalize a core-product API path for the configured base URL.
68
+ * Paths are always /api/v1/... relative to the service root (direct BE or gateway .../core-product).
69
+ */
70
+ export function resolveApiPath(path) {
71
+ const trimmed = path.trim();
72
+ if (!trimmed) {
73
+ throw new Error("API path is required");
74
+ }
75
+ let normalized = trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
76
+ if (normalized.startsWith("/core-product/")) {
77
+ normalized = normalized.slice("/core-product".length);
78
+ }
79
+ if (!normalized.startsWith("/api/v1/")) {
80
+ throw new Error(`API path must start with /api/v1/: ${path}`);
81
+ }
82
+ return normalized;
83
+ }
84
+ /** True when apiBase already includes the gateway /core-product suffix. */
85
+ export function apiBaseUsesGatewayPrefix() {
86
+ return getApiBase().includes("/core-product");
87
+ }
@@ -0,0 +1,10 @@
1
+ export interface UserConfig {
2
+ env?: string;
3
+ baseUrl?: string;
4
+ portalUrl?: string;
5
+ }
6
+ export declare const CONFIG_PATH: string;
7
+ export declare function getConfigPath(): string;
8
+ export declare function readUserConfigSync(): UserConfig | null;
9
+ export declare function readUserConfig(): Promise<UserConfig | null>;
10
+ export declare function writeUserConfig(config: UserConfig): Promise<void>;
@@ -0,0 +1,47 @@
1
+ import { chmod, mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import { readFileSync } from "node:fs";
3
+ import { dirname, join } from "node:path";
4
+ import { homedir } from "node:os";
5
+ const CONFIG_MODE = 0o600;
6
+ export const CONFIG_PATH = join(homedir(), ".cawplan", "config.json");
7
+ export function getConfigPath() {
8
+ return process.env.CAWPLAN_CONFIG_PATH ?? CONFIG_PATH;
9
+ }
10
+ function normalizeConfig(parsed) {
11
+ return {
12
+ env: parsed.env,
13
+ baseUrl: parsed.baseUrl,
14
+ portalUrl: parsed.portalUrl,
15
+ };
16
+ }
17
+ export function readUserConfigSync() {
18
+ try {
19
+ const raw = readFileSync(getConfigPath(), "utf8");
20
+ return normalizeConfig(JSON.parse(raw));
21
+ }
22
+ catch (err) {
23
+ if (err.code === "ENOENT") {
24
+ return null;
25
+ }
26
+ throw err;
27
+ }
28
+ }
29
+ export async function readUserConfig() {
30
+ try {
31
+ const raw = await readFile(getConfigPath(), "utf8");
32
+ return normalizeConfig(JSON.parse(raw));
33
+ }
34
+ catch (err) {
35
+ if (err.code === "ENOENT") {
36
+ return null;
37
+ }
38
+ throw err;
39
+ }
40
+ }
41
+ export async function writeUserConfig(config) {
42
+ const configPath = getConfigPath();
43
+ await mkdir(dirname(configPath), { recursive: true });
44
+ const payload = `${JSON.stringify(config, null, 2)}\n`;
45
+ await writeFile(configPath, payload, { encoding: "utf8", mode: CONFIG_MODE });
46
+ await chmod(configPath, CONFIG_MODE);
47
+ }
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "cawplan",
3
+ "version": "0.0.0",
4
+ "description": "CawPlan CLI",
5
+ "author": "Ubiquiti UID",
6
+ "license": "MIT",
7
+ "type": "module",
8
+ "main": "dist/index.js",
9
+ "bin": {
10
+ "cawplan": "dist/index.js"
11
+ },
12
+ "files": [
13
+ "dist",
14
+ "config"
15
+ ],
16
+ "keywords": [
17
+ "cawplan",
18
+ "prm",
19
+ "release-management",
20
+ "ubiquiti"
21
+ ],
22
+ "engines": {
23
+ "node": ">=20"
24
+ },
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "git+https://github.com/Ubiquiti-UID/flow-cawplan-skill.git",
28
+ "directory": "cli"
29
+ },
30
+ "scripts": {
31
+ "build": "tsc",
32
+ "postbuild": "chmod +x dist/index.js",
33
+ "prepublishOnly": "npm run build",
34
+ "prepack": "node scripts/pack-public-config.mjs prepack",
35
+ "postpack": "node scripts/pack-public-config.mjs postpack",
36
+ "cli": "tsx src/index.ts",
37
+ "test": "vitest run"
38
+ },
39
+ "dependencies": {
40
+ "commander": "^12.0.0",
41
+ "zod": "^3.24.1"
42
+ },
43
+ "devDependencies": {
44
+ "@types/node": "^22.0.0",
45
+ "tsx": "^4.0.0",
46
+ "typescript": "^5.0.0",
47
+ "vitest": "^4.1.8"
48
+ }
49
+ }