atlass 1.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/README.md +167 -0
- package/dist/cli.d.mts +1 -0
- package/dist/cli.mjs +680 -0
- package/package.json +42 -0
package/README.md
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# atlass
|
|
2
|
+
|
|
3
|
+
CLI to copy Jira issues and Confluence pages to Markdown.
|
|
4
|
+
|
|
5
|
+
Fetches an issue or page from Atlassian Cloud, converts its rich content to
|
|
6
|
+
Markdown, writes a `.md` file with YAML frontmatter, and downloads any
|
|
7
|
+
attachments alongside it.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pnpm install
|
|
13
|
+
pnpm build
|
|
14
|
+
pnpm link --global # exposes the `atlass` binary
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Or run from source without linking:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
node dist/cli.mjs <command>
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Authentication
|
|
24
|
+
|
|
25
|
+
Atlassian Cloud only. Auth uses your account email plus an API token
|
|
26
|
+
(Basic auth). Create a token at
|
|
27
|
+
`https://id.atlassian.com/manage-profile/security/api-tokens`.
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
atlass auth login
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
You are prompted for:
|
|
34
|
+
|
|
35
|
+
- site, e.g. `acme.atlassian.net`
|
|
36
|
+
- account email
|
|
37
|
+
- API token
|
|
38
|
+
|
|
39
|
+
The login is verified against `/rest/api/3/myself` before anything is saved.
|
|
40
|
+
The site and email are stored in `~/.config/atlass/config.json`
|
|
41
|
+
(or `$XDG_CONFIG_HOME/atlass/config.json`). The API token is stored in the OS
|
|
42
|
+
keyring (service `atlass`), never on disk.
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
atlass auth status # show current site/email and whether a token is stored
|
|
46
|
+
atlass auth logout # remove config and delete the token from the keyring
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Only one account is supported at a time.
|
|
50
|
+
|
|
51
|
+
## Usage
|
|
52
|
+
|
|
53
|
+
### Copy a Jira issue
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
atlass jira copy PROJ-123
|
|
57
|
+
atlass jira copy https://acme.atlassian.net/browse/PROJ-123
|
|
58
|
+
atlass jira copy # prompts for the key or URL
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Accepts an issue key or any URL containing one. Writes `PROJ-123.md` to the
|
|
62
|
+
current directory.
|
|
63
|
+
|
|
64
|
+
### Copy a Confluence page
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
atlass confluence copy 123456
|
|
68
|
+
atlass confluence copy https://acme.atlassian.net/wiki/spaces/DEV/pages/123456/Title
|
|
69
|
+
atlass confluence copy # prompts for the id or URL
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Accepts a numeric page id or a page URL. Writes `123456-title-slug.md` to the
|
|
73
|
+
current directory.
|
|
74
|
+
|
|
75
|
+
### Output location
|
|
76
|
+
|
|
77
|
+
By default files are written to the current directory, named after the issue
|
|
78
|
+
key (Jira) or `<id>-<title-slug>` (Confluence). Use `--out` to override:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
atlass jira copy PROJ-123 --out ./tickets/ # directory: ./tickets/PROJ-123.md
|
|
82
|
+
atlass jira copy PROJ-123 --out ./bug.md # explicit file path
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Existing files are overwritten.
|
|
86
|
+
|
|
87
|
+
## Output format
|
|
88
|
+
|
|
89
|
+
Each file is:
|
|
90
|
+
|
|
91
|
+
- YAML frontmatter with metadata
|
|
92
|
+
- an H1 heading (issue summary / page title)
|
|
93
|
+
- the body, converted from ADF to Markdown
|
|
94
|
+
- a `## Comments` section (Jira comments / Confluence footer comments)
|
|
95
|
+
- an `## Attachments` section linking every downloaded file
|
|
96
|
+
|
|
97
|
+
Jira frontmatter: `key`, `type`, `status`, `assignee`, `reporter`, `priority`,
|
|
98
|
+
`labels`, `created`, `updated`, `url`.
|
|
99
|
+
|
|
100
|
+
Confluence frontmatter: `title`, `id`, `space`, `version`, `author`, `created`,
|
|
101
|
+
`updated`, `url`.
|
|
102
|
+
|
|
103
|
+
Example (Jira):
|
|
104
|
+
|
|
105
|
+
```markdown
|
|
106
|
+
---
|
|
107
|
+
key: "PROJ-123"
|
|
108
|
+
type: "Bug"
|
|
109
|
+
status: "In Progress"
|
|
110
|
+
assignee: "Dana Scully"
|
|
111
|
+
reporter: "Fox Mulder"
|
|
112
|
+
priority: "High"
|
|
113
|
+
labels:
|
|
114
|
+
- "regression"
|
|
115
|
+
url: "https://acme.atlassian.net/browse/PROJ-123"
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
# Login button does nothing on Safari
|
|
119
|
+
|
|
120
|
+
Steps to reproduce...
|
|
121
|
+
|
|
122
|
+
## Comments
|
|
123
|
+
|
|
124
|
+
### Fox Mulder - 2025-07-01 10:30
|
|
125
|
+
|
|
126
|
+
Reproduced on 17.5.
|
|
127
|
+
|
|
128
|
+
## Attachments
|
|
129
|
+
|
|
130
|
+
- [screenshot.png](PROJ-123.assets/screenshot.png)
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Attachments
|
|
134
|
+
|
|
135
|
+
All attachments are downloaded into a sibling `<name>.assets/` folder and
|
|
136
|
+
listed under `## Attachments`. Inline images are linked to the local copy where
|
|
137
|
+
they can be matched:
|
|
138
|
+
|
|
139
|
+
- Confluence media nodes carry the attachment file id, so inline images resolve
|
|
140
|
+
reliably.
|
|
141
|
+
- Jira media nodes usually expose only a filename, so inline images resolve when
|
|
142
|
+
that filename matches an attachment; otherwise they render as
|
|
143
|
+
`[embedded media: ...]` and the file is still captured in the attachments
|
|
144
|
+
folder and section.
|
|
145
|
+
|
|
146
|
+
## Development
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
pnpm test # run unit tests
|
|
150
|
+
pnpm check # format, lint, typecheck (add --fix to auto-fix)
|
|
151
|
+
pnpm build # build dist/cli.mjs
|
|
152
|
+
pnpm dev # build in watch mode
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Layout
|
|
156
|
+
|
|
157
|
+
- `src/cli.ts` command wiring (commander)
|
|
158
|
+
- `src/commands/` `auth`, `jira`, `confluence` command handlers
|
|
159
|
+
- `src/api/` fetch client, Jira and Confluence endpoints, attachment downloader
|
|
160
|
+
- `src/adf/` ADF to Markdown converter (unit tested)
|
|
161
|
+
- `src/markdown/` frontmatter, comments, attachments, media resolver
|
|
162
|
+
- `src/config.ts`, `src/credentials.ts` config file and keyring
|
|
163
|
+
- `src/util/` key/id parsing and output path resolution
|
|
164
|
+
|
|
165
|
+
The ADF to Markdown converter in `src/adf/to-markdown.ts` is a single hand
|
|
166
|
+
rolled walker shared by both commands. Confluence page bodies are requested as
|
|
167
|
+
`atlas_doc_format` so they flow through the same converter as Jira.
|
package/dist/cli.d.mts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
package/dist/cli.mjs
ADDED
|
@@ -0,0 +1,680 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import { input, password } from "@inquirer/prompts";
|
|
4
|
+
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
5
|
+
import { homedir } from "node:os";
|
|
6
|
+
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
7
|
+
import { Entry } from "@napi-rs/keyring";
|
|
8
|
+
//#region package.json
|
|
9
|
+
var version = "1.0.0";
|
|
10
|
+
//#endregion
|
|
11
|
+
//#region src/api/client.ts
|
|
12
|
+
var AtlassianClient = class {
|
|
13
|
+
site;
|
|
14
|
+
authHeader;
|
|
15
|
+
constructor(auth) {
|
|
16
|
+
this.site = auth.site;
|
|
17
|
+
const basic = Buffer.from(`${auth.email}:${auth.token}`).toString("base64");
|
|
18
|
+
this.authHeader = `Basic ${basic}`;
|
|
19
|
+
}
|
|
20
|
+
async request(path, accept) {
|
|
21
|
+
const res = await fetch(`${this.site}${path}`, { headers: {
|
|
22
|
+
Authorization: this.authHeader,
|
|
23
|
+
Accept: accept
|
|
24
|
+
} });
|
|
25
|
+
if (!res.ok) throw httpError(res.status, path);
|
|
26
|
+
return res;
|
|
27
|
+
}
|
|
28
|
+
async getJson(path) {
|
|
29
|
+
return (await this.request(path, "application/json")).json();
|
|
30
|
+
}
|
|
31
|
+
async getBinary(url) {
|
|
32
|
+
const path = url.startsWith("http") ? new URL(url).pathname + new URL(url).search : url;
|
|
33
|
+
const res = await this.request(path, "*/*");
|
|
34
|
+
return new Uint8Array(await res.arrayBuffer());
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
function httpError(status, path) {
|
|
38
|
+
if (status === 401 || status === 403) return /* @__PURE__ */ new Error("Authentication failed (401/403). Run `atlass auth login` to update your token.");
|
|
39
|
+
if (status === 404) return /* @__PURE__ */ new Error(`Not found (404): ${path}`);
|
|
40
|
+
return /* @__PURE__ */ new Error(`Request failed (${status}): ${path}`);
|
|
41
|
+
}
|
|
42
|
+
//#endregion
|
|
43
|
+
//#region src/config.ts
|
|
44
|
+
function configDir() {
|
|
45
|
+
return join(process.env.XDG_CONFIG_HOME || join(homedir(), ".config"), "atlass");
|
|
46
|
+
}
|
|
47
|
+
function configPath() {
|
|
48
|
+
return join(configDir(), "config.json");
|
|
49
|
+
}
|
|
50
|
+
async function readConfig() {
|
|
51
|
+
try {
|
|
52
|
+
const raw = await readFile(configPath(), "utf8");
|
|
53
|
+
const parsed = JSON.parse(raw);
|
|
54
|
+
if (!parsed.site || !parsed.email) return null;
|
|
55
|
+
return {
|
|
56
|
+
site: parsed.site,
|
|
57
|
+
email: parsed.email
|
|
58
|
+
};
|
|
59
|
+
} catch {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
async function writeConfig(config) {
|
|
64
|
+
await mkdir(configDir(), { recursive: true });
|
|
65
|
+
await writeFile(configPath(), `${JSON.stringify(config, null, 2)}\n`, "utf8");
|
|
66
|
+
}
|
|
67
|
+
async function clearConfig() {
|
|
68
|
+
await rm(configPath(), { force: true });
|
|
69
|
+
}
|
|
70
|
+
function normalizeSite(input) {
|
|
71
|
+
let value = input.trim();
|
|
72
|
+
if (!/^https?:\/\//i.test(value)) value = `https://${value}`;
|
|
73
|
+
return new URL(value).origin;
|
|
74
|
+
}
|
|
75
|
+
//#endregion
|
|
76
|
+
//#region src/credentials.ts
|
|
77
|
+
const SERVICE = "atlass";
|
|
78
|
+
function entry(email) {
|
|
79
|
+
return new Entry(SERVICE, email);
|
|
80
|
+
}
|
|
81
|
+
function saveToken(email, token) {
|
|
82
|
+
entry(email).setPassword(token);
|
|
83
|
+
}
|
|
84
|
+
function readToken(email) {
|
|
85
|
+
return entry(email).getPassword();
|
|
86
|
+
}
|
|
87
|
+
function deleteToken(email) {
|
|
88
|
+
try {
|
|
89
|
+
entry(email).deleteCredential();
|
|
90
|
+
} catch {}
|
|
91
|
+
}
|
|
92
|
+
async function requireAuth() {
|
|
93
|
+
const config = await readConfig();
|
|
94
|
+
if (!config) throw new Error("Not logged in. Run `atlass auth login` first.");
|
|
95
|
+
const token = readToken(config.email);
|
|
96
|
+
if (!token) throw new Error("No API token found in keyring. Run `atlass auth login` again.");
|
|
97
|
+
return {
|
|
98
|
+
...config,
|
|
99
|
+
token
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
//#endregion
|
|
103
|
+
//#region src/commands/auth.ts
|
|
104
|
+
async function login() {
|
|
105
|
+
const site = normalizeSite(await input({
|
|
106
|
+
message: "Atlassian site (e.g. acme.atlassian.net):",
|
|
107
|
+
required: true
|
|
108
|
+
}));
|
|
109
|
+
const email = await input({
|
|
110
|
+
message: "Account email:",
|
|
111
|
+
required: true
|
|
112
|
+
});
|
|
113
|
+
const token = await password({
|
|
114
|
+
message: "API token (from id.atlassian.com/manage-profile/security/api-tokens):",
|
|
115
|
+
mask: true
|
|
116
|
+
});
|
|
117
|
+
const me = await new AtlassianClient({
|
|
118
|
+
site,
|
|
119
|
+
email,
|
|
120
|
+
token
|
|
121
|
+
}).getJson("/rest/api/3/myself");
|
|
122
|
+
await writeConfig({
|
|
123
|
+
site,
|
|
124
|
+
email
|
|
125
|
+
});
|
|
126
|
+
saveToken(email, token);
|
|
127
|
+
console.log(`Logged in as ${me.displayName} on ${site}.`);
|
|
128
|
+
}
|
|
129
|
+
async function logout() {
|
|
130
|
+
const config = await readConfig();
|
|
131
|
+
if (config) deleteToken(config.email);
|
|
132
|
+
await clearConfig();
|
|
133
|
+
console.log("Logged out. Credentials removed.");
|
|
134
|
+
}
|
|
135
|
+
async function status() {
|
|
136
|
+
const config = await readConfig();
|
|
137
|
+
if (!config) {
|
|
138
|
+
console.log("Not logged in. Run `atlass auth login`.");
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
const hasToken = readToken(config.email) !== null;
|
|
142
|
+
console.log(`Site: ${config.site}`);
|
|
143
|
+
console.log(`Email: ${config.email}`);
|
|
144
|
+
console.log(`Token: ${hasToken ? "stored in keyring" : "MISSING (run `atlass auth login`)"}`);
|
|
145
|
+
}
|
|
146
|
+
//#endregion
|
|
147
|
+
//#region src/adf/to-markdown.ts
|
|
148
|
+
function adfToMarkdown(doc, options = {}) {
|
|
149
|
+
if (!doc) return "";
|
|
150
|
+
const ctx = { resolveMedia: options.resolveMedia };
|
|
151
|
+
return (doc.type === "doc" ? renderBlocks(doc.content ?? [], ctx, "") : renderBlock(doc, ctx, "")).trim();
|
|
152
|
+
}
|
|
153
|
+
function renderBlocks(nodes, ctx, indent) {
|
|
154
|
+
return nodes.map((n) => renderBlock(n, ctx, indent)).filter((s) => s.length > 0).join("\n\n");
|
|
155
|
+
}
|
|
156
|
+
function renderBlock(node, ctx, indent) {
|
|
157
|
+
switch (node.type) {
|
|
158
|
+
case "paragraph": return indent + renderInline(node.content ?? [], ctx);
|
|
159
|
+
case "heading": {
|
|
160
|
+
const level = clampLevel(node.attrs?.["level"]);
|
|
161
|
+
return `${"#".repeat(level)} ${renderInline(node.content ?? [], ctx)}`;
|
|
162
|
+
}
|
|
163
|
+
case "bulletList": return renderList(node, ctx, indent, "bullet");
|
|
164
|
+
case "orderedList": return renderList(node, ctx, indent, "ordered");
|
|
165
|
+
case "taskList": return renderTaskList(node, ctx, indent);
|
|
166
|
+
case "decisionList": return renderDecisionList(node, ctx, indent);
|
|
167
|
+
case "codeBlock": return renderCodeBlock(node, indent);
|
|
168
|
+
case "blockquote": return prefixLines(renderBlocks(node.content ?? [], ctx, ""), `${indent}> `);
|
|
169
|
+
case "panel": return renderPanel(node, ctx, indent);
|
|
170
|
+
case "rule": return `${indent}---`;
|
|
171
|
+
case "table": return renderTable(node, ctx);
|
|
172
|
+
case "mediaSingle":
|
|
173
|
+
case "mediaGroup": return renderBlocks(node.content ?? [], ctx, indent);
|
|
174
|
+
case "media": return indent + renderMedia(node, ctx);
|
|
175
|
+
case "expand":
|
|
176
|
+
case "nestedExpand": return renderExpand(node, ctx, indent);
|
|
177
|
+
default:
|
|
178
|
+
if (node.content?.length) return renderBlocks(node.content, ctx, indent);
|
|
179
|
+
return node.text ? indent + node.text : "";
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
function renderList(node, ctx, indent, kind) {
|
|
183
|
+
const start = kind === "ordered" ? toNumber(node.attrs?.["order"]) ?? 1 : 0;
|
|
184
|
+
return (node.content ?? []).filter((n) => n.type === "listItem").map((item, i) => {
|
|
185
|
+
return renderListItem(item, ctx, indent, kind === "ordered" ? `${start + i}. ` : "- ");
|
|
186
|
+
}).join("\n");
|
|
187
|
+
}
|
|
188
|
+
function renderListItem(item, ctx, indent, marker) {
|
|
189
|
+
const childIndent = `${indent}${" ".repeat(marker.length)}`;
|
|
190
|
+
const rendered = (item.content ?? []).map((b, i) => renderBlock(b, ctx, i === 0 ? "" : childIndent));
|
|
191
|
+
const parts = [];
|
|
192
|
+
rendered.forEach((text, i) => {
|
|
193
|
+
if (i === 0) parts.push(`${indent}${marker}${text}`);
|
|
194
|
+
else parts.push(text);
|
|
195
|
+
});
|
|
196
|
+
return parts.join("\n");
|
|
197
|
+
}
|
|
198
|
+
function renderTaskList(node, ctx, indent) {
|
|
199
|
+
return (node.content ?? []).filter((n) => n.type === "taskItem").map((item) => {
|
|
200
|
+
return `${indent}- [${item.attrs?.["state"] === "DONE" ? "x" : " "}] ${renderInline(item.content ?? [], ctx)}`;
|
|
201
|
+
}).join("\n");
|
|
202
|
+
}
|
|
203
|
+
function renderDecisionList(node, ctx, indent) {
|
|
204
|
+
return (node.content ?? []).filter((n) => n.type === "decisionItem").map((item) => `${indent}- (decision) ${renderInline(item.content ?? [], ctx)}`).join("\n");
|
|
205
|
+
}
|
|
206
|
+
function renderCodeBlock(node, indent) {
|
|
207
|
+
const lang = typeof node.attrs?.["language"] === "string" ? node.attrs["language"] : "";
|
|
208
|
+
const code = (node.content ?? []).map((n) => n.text ?? "").join("");
|
|
209
|
+
const fence = "```";
|
|
210
|
+
return prefixLines(`${fence}${lang}\n${code}\n${fence}`, indent);
|
|
211
|
+
}
|
|
212
|
+
const PANEL_LABELS = {
|
|
213
|
+
info: "Info",
|
|
214
|
+
note: "Note",
|
|
215
|
+
warning: "Warning",
|
|
216
|
+
success: "Success",
|
|
217
|
+
error: "Error"
|
|
218
|
+
};
|
|
219
|
+
function renderPanel(node, ctx, indent) {
|
|
220
|
+
return prefixLines(`**${PANEL_LABELS[typeof node.attrs?.["panelType"] === "string" ? node.attrs["panelType"] : "info"] ?? "Note"}**\n\n${renderBlocks(node.content ?? [], ctx, "")}`, `${indent}> `);
|
|
221
|
+
}
|
|
222
|
+
function renderExpand(node, ctx, indent) {
|
|
223
|
+
return `${indent}<details><summary>${typeof node.attrs?.["title"] === "string" ? node.attrs["title"] : "Details"}</summary>\n\n${renderBlocks(node.content ?? [], ctx, "")}\n\n${indent}</details>`;
|
|
224
|
+
}
|
|
225
|
+
function renderTable(node, ctx) {
|
|
226
|
+
const rows = (node.content ?? []).filter((n) => n.type === "tableRow");
|
|
227
|
+
if (rows.length === 0) return "";
|
|
228
|
+
const grid = rows.map((row) => (row.content ?? []).map((cell) => renderCell(cell, ctx)));
|
|
229
|
+
const cols = Math.max(...grid.map((r) => r.length));
|
|
230
|
+
const pad = (r) => {
|
|
231
|
+
const copy = [...r];
|
|
232
|
+
while (copy.length < cols) copy.push("");
|
|
233
|
+
return copy;
|
|
234
|
+
};
|
|
235
|
+
const header = pad(grid[0] ?? []);
|
|
236
|
+
const lines = [`| ${header.join(" | ")} |`, `| ${header.map(() => "---").join(" | ")} |`];
|
|
237
|
+
for (const row of grid.slice(1)) lines.push(`| ${pad(row).join(" | ")} |`);
|
|
238
|
+
return lines.join("\n");
|
|
239
|
+
}
|
|
240
|
+
function renderCell(cell, ctx) {
|
|
241
|
+
return renderBlocks(cell.content ?? [], ctx, "").replace(/\n+/g, " ").replace(/\|/g, "\\|").trim();
|
|
242
|
+
}
|
|
243
|
+
function renderMedia(node, ctx) {
|
|
244
|
+
const attrs = node.attrs ?? {};
|
|
245
|
+
const alt = attrs.alt ?? "";
|
|
246
|
+
const resolved = ctx.resolveMedia?.(attrs);
|
|
247
|
+
if (resolved) return ``;
|
|
248
|
+
return `[embedded media: ${alt || attrs.id || "media"}]`;
|
|
249
|
+
}
|
|
250
|
+
function renderInline(nodes, ctx) {
|
|
251
|
+
return nodes.map((n) => renderInlineNode(n, ctx)).join("");
|
|
252
|
+
}
|
|
253
|
+
function renderInlineNode(node, ctx) {
|
|
254
|
+
switch (node.type) {
|
|
255
|
+
case "text": return applyMarks(node.text ?? "", node.marks ?? []);
|
|
256
|
+
case "hardBreak": return " \n";
|
|
257
|
+
case "mention": return (typeof node.attrs?.["text"] === "string" ? node.attrs["text"] : "") || "@unknown";
|
|
258
|
+
case "emoji": {
|
|
259
|
+
const text = node.attrs?.["text"];
|
|
260
|
+
if (typeof text === "string" && text.length > 0) return text;
|
|
261
|
+
const short = node.attrs?.["shortName"];
|
|
262
|
+
return typeof short === "string" ? short : "";
|
|
263
|
+
}
|
|
264
|
+
case "date": return formatDate$1(node.attrs?.["timestamp"]);
|
|
265
|
+
case "status": return `\`[${typeof node.attrs?.["text"] === "string" ? node.attrs["text"] : ""}]\``;
|
|
266
|
+
case "inlineCard": {
|
|
267
|
+
const url = node.attrs?.["url"];
|
|
268
|
+
if (typeof url === "string") return `[${url}](${url})`;
|
|
269
|
+
return "";
|
|
270
|
+
}
|
|
271
|
+
case "media": return renderMedia(node, ctx);
|
|
272
|
+
default: return node.text ?? "";
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
function applyMarks(text, marks) {
|
|
276
|
+
if (text.length === 0) return text;
|
|
277
|
+
let out = text;
|
|
278
|
+
let href;
|
|
279
|
+
for (const mark of marks) switch (mark.type) {
|
|
280
|
+
case "code":
|
|
281
|
+
out = `\`${out}\``;
|
|
282
|
+
break;
|
|
283
|
+
case "strong":
|
|
284
|
+
out = `**${out}**`;
|
|
285
|
+
break;
|
|
286
|
+
case "em":
|
|
287
|
+
out = `*${out}*`;
|
|
288
|
+
break;
|
|
289
|
+
case "strike":
|
|
290
|
+
out = `~~${out}~~`;
|
|
291
|
+
break;
|
|
292
|
+
case "link": {
|
|
293
|
+
const value = mark.attrs?.["href"];
|
|
294
|
+
if (typeof value === "string") href = value;
|
|
295
|
+
break;
|
|
296
|
+
}
|
|
297
|
+
default: break;
|
|
298
|
+
}
|
|
299
|
+
if (href) out = `[${out}](${href})`;
|
|
300
|
+
return out;
|
|
301
|
+
}
|
|
302
|
+
function prefixLines(text, prefix) {
|
|
303
|
+
return text.split("\n").map((line) => line.length > 0 ? prefix + line : prefix.trimEnd()).join("\n");
|
|
304
|
+
}
|
|
305
|
+
function clampLevel(value) {
|
|
306
|
+
const n = toNumber(value) ?? 1;
|
|
307
|
+
return Math.min(6, Math.max(1, n));
|
|
308
|
+
}
|
|
309
|
+
function toNumber(value) {
|
|
310
|
+
return typeof value === "number" ? value : void 0;
|
|
311
|
+
}
|
|
312
|
+
function formatDate$1(timestamp) {
|
|
313
|
+
const ms = typeof timestamp === "string" ? Number(timestamp) : typeof timestamp === "number" ? timestamp : NaN;
|
|
314
|
+
if (Number.isNaN(ms)) return "";
|
|
315
|
+
return new Date(ms).toISOString().slice(0, 10);
|
|
316
|
+
}
|
|
317
|
+
//#endregion
|
|
318
|
+
//#region src/api/attachments.ts
|
|
319
|
+
async function downloadAttachments(client, attachments, assetsDir, assetsDirName) {
|
|
320
|
+
if (attachments.length === 0) return [];
|
|
321
|
+
await mkdir(assetsDir, { recursive: true });
|
|
322
|
+
const used = /* @__PURE__ */ new Set();
|
|
323
|
+
const results = [];
|
|
324
|
+
for (const att of attachments) {
|
|
325
|
+
const savedAs = uniqueName(safeName(att.filename), used);
|
|
326
|
+
try {
|
|
327
|
+
const bytes = await client.getBinary(att.url);
|
|
328
|
+
await writeFile(join(assetsDir, savedAs), bytes);
|
|
329
|
+
results.push({
|
|
330
|
+
...att,
|
|
331
|
+
relativePath: `${assetsDirName}/${savedAs}`
|
|
332
|
+
});
|
|
333
|
+
} catch (err) {
|
|
334
|
+
console.warn(` ! could not download ${att.filename}: ${err.message}`);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
return results;
|
|
338
|
+
}
|
|
339
|
+
function safeName(name) {
|
|
340
|
+
return basename(name).replace(/[/\\]/g, "_") || "attachment";
|
|
341
|
+
}
|
|
342
|
+
function uniqueName(name, used) {
|
|
343
|
+
if (!used.has(name)) {
|
|
344
|
+
used.add(name);
|
|
345
|
+
return name;
|
|
346
|
+
}
|
|
347
|
+
const dot = name.lastIndexOf(".");
|
|
348
|
+
const stem = dot > 0 ? name.slice(0, dot) : name;
|
|
349
|
+
const ext = dot > 0 ? name.slice(dot) : "";
|
|
350
|
+
let i = 1;
|
|
351
|
+
let candidate = `${stem}-${i}${ext}`;
|
|
352
|
+
while (used.has(candidate)) {
|
|
353
|
+
i += 1;
|
|
354
|
+
candidate = `${stem}-${i}${ext}`;
|
|
355
|
+
}
|
|
356
|
+
used.add(candidate);
|
|
357
|
+
return candidate;
|
|
358
|
+
}
|
|
359
|
+
//#endregion
|
|
360
|
+
//#region src/api/confluence.ts
|
|
361
|
+
async function fetchPage(client, site, id) {
|
|
362
|
+
const page = await client.getJson(`/wiki/api/v2/pages/${encodeURIComponent(id)}?body-format=atlas_doc_format`);
|
|
363
|
+
const names = new UserNames(client);
|
|
364
|
+
const [spaceKey, attachments, comments, author] = await Promise.all([
|
|
365
|
+
fetchSpaceKey(client, page.spaceId),
|
|
366
|
+
fetchAttachments(client, id),
|
|
367
|
+
fetchComments$1(client, id, names),
|
|
368
|
+
names.resolve(page.version?.authorId ?? page.authorId)
|
|
369
|
+
]);
|
|
370
|
+
const webui = page._links?.webui ?? "";
|
|
371
|
+
return {
|
|
372
|
+
id: page.id,
|
|
373
|
+
title: page.title,
|
|
374
|
+
spaceKey,
|
|
375
|
+
version: page.version?.number ?? 0,
|
|
376
|
+
author,
|
|
377
|
+
createdAt: page.createdAt ?? "",
|
|
378
|
+
updatedAt: page.version?.createdAt ?? "",
|
|
379
|
+
url: webui ? `${site}/wiki${webui}` : "",
|
|
380
|
+
body: parseAdf(page.body?.atlas_doc_format?.value),
|
|
381
|
+
attachments,
|
|
382
|
+
comments
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
async function fetchSpaceKey(client, spaceId) {
|
|
386
|
+
if (!spaceId) return "";
|
|
387
|
+
try {
|
|
388
|
+
return (await client.getJson(`/wiki/api/v2/spaces/${encodeURIComponent(spaceId)}`)).key ?? "";
|
|
389
|
+
} catch {
|
|
390
|
+
return "";
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
async function fetchAttachments(client, id) {
|
|
394
|
+
return (await client.getJson(`/wiki/api/v2/pages/${encodeURIComponent(id)}/attachments?limit=250`)).results.filter((a) => a.downloadLink).map((a) => ({
|
|
395
|
+
id: a.fileId ?? a.id,
|
|
396
|
+
filename: a.title ?? a.id,
|
|
397
|
+
url: normalizeDownloadLink(a.downloadLink ?? "")
|
|
398
|
+
}));
|
|
399
|
+
}
|
|
400
|
+
async function fetchComments$1(client, id, names) {
|
|
401
|
+
const res = await client.getJson(`/wiki/api/v2/pages/${encodeURIComponent(id)}/footer-comments?body-format=atlas_doc_format&limit=250`);
|
|
402
|
+
return Promise.all(res.results.map(async (c) => ({
|
|
403
|
+
author: await names.resolve(c.version?.authorId),
|
|
404
|
+
created: c.version?.createdAt ?? "",
|
|
405
|
+
body: parseAdf(c.body?.atlas_doc_format?.value)
|
|
406
|
+
})));
|
|
407
|
+
}
|
|
408
|
+
function normalizeDownloadLink(link) {
|
|
409
|
+
if (link.startsWith("http") || link.startsWith("/wiki")) return link;
|
|
410
|
+
return `/wiki${link}`;
|
|
411
|
+
}
|
|
412
|
+
function parseAdf(value) {
|
|
413
|
+
if (!value) return null;
|
|
414
|
+
try {
|
|
415
|
+
return JSON.parse(value);
|
|
416
|
+
} catch {
|
|
417
|
+
return null;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
var UserNames = class {
|
|
421
|
+
client;
|
|
422
|
+
cache = /* @__PURE__ */ new Map();
|
|
423
|
+
constructor(client) {
|
|
424
|
+
this.client = client;
|
|
425
|
+
}
|
|
426
|
+
async resolve(accountId) {
|
|
427
|
+
if (!accountId) return "";
|
|
428
|
+
const cached = this.cache.get(accountId);
|
|
429
|
+
if (cached !== void 0) return cached;
|
|
430
|
+
let name = accountId;
|
|
431
|
+
try {
|
|
432
|
+
const user = await this.client.getJson(`/wiki/rest/api/user?accountId=${encodeURIComponent(accountId)}`);
|
|
433
|
+
if (user.displayName) name = user.displayName;
|
|
434
|
+
} catch {}
|
|
435
|
+
this.cache.set(accountId, name);
|
|
436
|
+
return name;
|
|
437
|
+
}
|
|
438
|
+
};
|
|
439
|
+
//#endregion
|
|
440
|
+
//#region src/markdown/document.ts
|
|
441
|
+
function mediaResolver(downloaded) {
|
|
442
|
+
const byId = new Map(downloaded.map((d) => [d.id, d.relativePath]));
|
|
443
|
+
const byName = new Map(downloaded.map((d) => [d.filename, d.relativePath]));
|
|
444
|
+
return (media) => {
|
|
445
|
+
if (media.id && byId.has(media.id)) return byId.get(media.id);
|
|
446
|
+
if (media.alt && byName.has(media.alt)) return byName.get(media.alt);
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
function frontmatter(fields) {
|
|
450
|
+
const lines = ["---"];
|
|
451
|
+
for (const [key, value] of Object.entries(fields)) if (Array.isArray(value)) if (value.length === 0) lines.push(`${key}: []`);
|
|
452
|
+
else lines.push(`${key}:`, ...value.map((v) => ` - ${quote(v)}`));
|
|
453
|
+
else if (typeof value === "number") lines.push(`${key}: ${value}`);
|
|
454
|
+
else lines.push(`${key}: ${quote(value)}`);
|
|
455
|
+
lines.push("---");
|
|
456
|
+
return lines.join("\n");
|
|
457
|
+
}
|
|
458
|
+
function quote(value) {
|
|
459
|
+
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`;
|
|
460
|
+
}
|
|
461
|
+
function attachmentsSection(downloaded) {
|
|
462
|
+
if (downloaded.length === 0) return "";
|
|
463
|
+
return [
|
|
464
|
+
"## Attachments",
|
|
465
|
+
"",
|
|
466
|
+
...downloaded.map((d) => `- [${d.filename}](${d.relativePath})`)
|
|
467
|
+
].join("\n");
|
|
468
|
+
}
|
|
469
|
+
function commentsSection(comments, resolveMedia) {
|
|
470
|
+
if (comments.length === 0) return "";
|
|
471
|
+
return [
|
|
472
|
+
"## Comments",
|
|
473
|
+
"",
|
|
474
|
+
comments.map((c) => {
|
|
475
|
+
const heading = `### ${c.author || "Unknown"}${c.created ? ` - ${formatDate(c.created)}` : ""}`;
|
|
476
|
+
const body = adfToMarkdown(c.body, { resolveMedia });
|
|
477
|
+
return body ? `${heading}\n\n${body}` : heading;
|
|
478
|
+
}).join("\n\n")
|
|
479
|
+
].join("\n");
|
|
480
|
+
}
|
|
481
|
+
function formatDate(iso) {
|
|
482
|
+
const date = new Date(iso);
|
|
483
|
+
if (Number.isNaN(date.getTime())) return iso;
|
|
484
|
+
return date.toISOString().replace("T", " ").slice(0, 16);
|
|
485
|
+
}
|
|
486
|
+
function joinSections(sections) {
|
|
487
|
+
return `${sections.filter((s) => s.trim().length > 0).join("\n\n")}\n`;
|
|
488
|
+
}
|
|
489
|
+
//#endregion
|
|
490
|
+
//#region src/util/output-path.ts
|
|
491
|
+
function resolveOutput(defaultBase, out) {
|
|
492
|
+
let filePath;
|
|
493
|
+
if (!out) filePath = resolve(`${defaultBase}.md`);
|
|
494
|
+
else if (out.endsWith(".md")) filePath = isAbsolute(out) ? out : resolve(out);
|
|
495
|
+
else filePath = resolve(out, `${defaultBase}.md`);
|
|
496
|
+
const dir = dirname(filePath);
|
|
497
|
+
const assetsDirName = `${filePath.slice(dir.length + 1).replace(/\.md$/, "")}.assets`;
|
|
498
|
+
return {
|
|
499
|
+
filePath,
|
|
500
|
+
assetsDir: join(dir, assetsDirName),
|
|
501
|
+
assetsDirName
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
function slugify(title) {
|
|
505
|
+
return title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60).replace(/-+$/g, "") || "page";
|
|
506
|
+
}
|
|
507
|
+
//#endregion
|
|
508
|
+
//#region src/util/parse.ts
|
|
509
|
+
function parseIssueKey(input) {
|
|
510
|
+
const match = input.toUpperCase().match(/[A-Z][A-Z0-9]+-\d+/);
|
|
511
|
+
return match ? match[0] : null;
|
|
512
|
+
}
|
|
513
|
+
function parsePageId(input) {
|
|
514
|
+
const trimmed = input.trim();
|
|
515
|
+
if (/^\d+$/.test(trimmed)) return trimmed;
|
|
516
|
+
const fromPath = trimmed.match(/\/pages\/(\d+)/);
|
|
517
|
+
if (fromPath) return fromPath[1];
|
|
518
|
+
const fromQuery = trimmed.match(/[?&]pageId=(\d+)/);
|
|
519
|
+
if (fromQuery) return fromQuery[1];
|
|
520
|
+
return null;
|
|
521
|
+
}
|
|
522
|
+
//#endregion
|
|
523
|
+
//#region src/commands/confluence.ts
|
|
524
|
+
async function confluenceCopy(arg, options) {
|
|
525
|
+
const auth = await requireAuth();
|
|
526
|
+
const id = await resolveId(arg);
|
|
527
|
+
const client = new AtlassianClient(auth);
|
|
528
|
+
console.log(`Fetching page ${id} ...`);
|
|
529
|
+
const page = await fetchPage(client, auth.site, id);
|
|
530
|
+
const target = resolveOutput(`${page.id}-${slugify(page.title)}`, options.out);
|
|
531
|
+
const downloaded = await downloadAttachments(client, page.attachments, target.assetsDir, target.assetsDirName);
|
|
532
|
+
const resolveMedia = mediaResolver(downloaded);
|
|
533
|
+
const document = joinSections([
|
|
534
|
+
frontmatter({
|
|
535
|
+
title: page.title,
|
|
536
|
+
id: page.id,
|
|
537
|
+
space: page.spaceKey,
|
|
538
|
+
version: page.version,
|
|
539
|
+
author: page.author,
|
|
540
|
+
created: page.createdAt,
|
|
541
|
+
updated: page.updatedAt,
|
|
542
|
+
url: page.url
|
|
543
|
+
}),
|
|
544
|
+
`# ${page.title}`,
|
|
545
|
+
adfToMarkdown(page.body, { resolveMedia }),
|
|
546
|
+
commentsSection(page.comments, resolveMedia),
|
|
547
|
+
attachmentsSection(downloaded)
|
|
548
|
+
]);
|
|
549
|
+
await writeFile(target.filePath, document, "utf8");
|
|
550
|
+
const suffix = downloaded.length > 0 ? ` (+${downloaded.length} attachment${downloaded.length === 1 ? "" : "s"})` : "";
|
|
551
|
+
console.log(`Wrote ${target.filePath}${suffix}`);
|
|
552
|
+
}
|
|
553
|
+
async function resolveId(arg) {
|
|
554
|
+
const raw = arg ?? await input({
|
|
555
|
+
message: "Confluence page id or URL:",
|
|
556
|
+
required: true
|
|
557
|
+
});
|
|
558
|
+
const id = parsePageId(raw);
|
|
559
|
+
if (!id) throw new Error(`Could not find a page id in "${raw}".`);
|
|
560
|
+
return id;
|
|
561
|
+
}
|
|
562
|
+
//#endregion
|
|
563
|
+
//#region src/api/jira.ts
|
|
564
|
+
const FIELDS = [
|
|
565
|
+
"summary",
|
|
566
|
+
"description",
|
|
567
|
+
"issuetype",
|
|
568
|
+
"status",
|
|
569
|
+
"assignee",
|
|
570
|
+
"reporter",
|
|
571
|
+
"priority",
|
|
572
|
+
"labels",
|
|
573
|
+
"created",
|
|
574
|
+
"updated",
|
|
575
|
+
"attachment"
|
|
576
|
+
].join(",");
|
|
577
|
+
async function fetchIssue(client, site, key) {
|
|
578
|
+
const issue = await client.getJson(`/rest/api/3/issue/${encodeURIComponent(key)}?fields=${FIELDS}`);
|
|
579
|
+
const comments = await fetchComments(client, key);
|
|
580
|
+
const f = issue.fields;
|
|
581
|
+
return {
|
|
582
|
+
key: issue.key,
|
|
583
|
+
url: `${site}/browse/${issue.key}`,
|
|
584
|
+
summary: f.summary ?? "",
|
|
585
|
+
type: f.issuetype?.name ?? "",
|
|
586
|
+
status: f.status?.name ?? "",
|
|
587
|
+
assignee: f.assignee?.displayName ?? "Unassigned",
|
|
588
|
+
reporter: f.reporter?.displayName ?? "",
|
|
589
|
+
priority: f.priority?.name ?? "",
|
|
590
|
+
labels: f.labels ?? [],
|
|
591
|
+
created: f.created ?? "",
|
|
592
|
+
updated: f.updated ?? "",
|
|
593
|
+
description: f.description ?? null,
|
|
594
|
+
comments,
|
|
595
|
+
attachments: (f.attachment ?? []).map((a) => ({
|
|
596
|
+
id: a.id,
|
|
597
|
+
filename: a.filename,
|
|
598
|
+
url: a.content
|
|
599
|
+
}))
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
async function fetchComments(client, key) {
|
|
603
|
+
return (await client.getJson(`/rest/api/3/issue/${encodeURIComponent(key)}/comment?maxResults=100&orderBy=created`)).comments.map((c) => ({
|
|
604
|
+
author: c.author?.displayName ?? "",
|
|
605
|
+
created: c.created ?? "",
|
|
606
|
+
body: c.body ?? null
|
|
607
|
+
}));
|
|
608
|
+
}
|
|
609
|
+
//#endregion
|
|
610
|
+
//#region src/commands/jira.ts
|
|
611
|
+
async function jiraCopy(arg, options) {
|
|
612
|
+
const auth = await requireAuth();
|
|
613
|
+
const key = await resolveKey(arg);
|
|
614
|
+
const client = new AtlassianClient(auth);
|
|
615
|
+
console.log(`Fetching ${key} ...`);
|
|
616
|
+
const issue = await fetchIssue(client, auth.site, key);
|
|
617
|
+
const target = resolveOutput(issue.key, options.out);
|
|
618
|
+
const downloaded = await downloadAttachments(client, issue.attachments, target.assetsDir, target.assetsDirName);
|
|
619
|
+
const resolveMedia = mediaResolver(downloaded);
|
|
620
|
+
const document = joinSections([
|
|
621
|
+
frontmatter({
|
|
622
|
+
key: issue.key,
|
|
623
|
+
type: issue.type,
|
|
624
|
+
status: issue.status,
|
|
625
|
+
assignee: issue.assignee,
|
|
626
|
+
reporter: issue.reporter,
|
|
627
|
+
priority: issue.priority,
|
|
628
|
+
labels: issue.labels,
|
|
629
|
+
created: issue.created,
|
|
630
|
+
updated: issue.updated,
|
|
631
|
+
url: issue.url
|
|
632
|
+
}),
|
|
633
|
+
`# ${issue.summary}`,
|
|
634
|
+
adfToMarkdown(issue.description, { resolveMedia }),
|
|
635
|
+
commentsSection(issue.comments, resolveMedia),
|
|
636
|
+
attachmentsSection(downloaded)
|
|
637
|
+
]);
|
|
638
|
+
await writeFile(target.filePath, document, "utf8");
|
|
639
|
+
report(target.filePath, downloaded.length);
|
|
640
|
+
}
|
|
641
|
+
async function resolveKey(arg) {
|
|
642
|
+
const raw = arg ?? await input({
|
|
643
|
+
message: "Jira issue key or URL:",
|
|
644
|
+
required: true
|
|
645
|
+
});
|
|
646
|
+
const key = parseIssueKey(raw);
|
|
647
|
+
if (!key) throw new Error(`Could not find an issue key in "${raw}" (expected e.g. PROJ-123).`);
|
|
648
|
+
return key;
|
|
649
|
+
}
|
|
650
|
+
function report(filePath, assetCount) {
|
|
651
|
+
const suffix = assetCount > 0 ? ` (+${assetCount} attachment${assetCount === 1 ? "" : "s"})` : "";
|
|
652
|
+
console.log(`Wrote ${filePath}${suffix}`);
|
|
653
|
+
}
|
|
654
|
+
//#endregion
|
|
655
|
+
//#region src/cli.ts
|
|
656
|
+
const program = new Command();
|
|
657
|
+
program.name("atlass").description("Copy Jira issues and Confluence pages to Markdown.").version(version);
|
|
658
|
+
const auth = program.command("auth").description("Manage Atlassian credentials");
|
|
659
|
+
auth.command("login").description("Store site, email, and API token").action(run(login));
|
|
660
|
+
auth.command("logout").description("Remove stored credentials").action(run(logout));
|
|
661
|
+
auth.command("status").description("Show the current login").action(run(status));
|
|
662
|
+
program.command("jira").description("Jira commands").command("copy [issue]").description("Copy a Jira issue (key or URL) to a Markdown file").option("-o, --out <path>", "output file or directory").action(run(jiraCopy));
|
|
663
|
+
program.command("confluence").description("Confluence commands").command("copy [page]").description("Copy a Confluence page (id or URL) to a Markdown file").option("-o, --out <path>", "output file or directory").action(run(confluenceCopy));
|
|
664
|
+
program.parseAsync().catch(fail);
|
|
665
|
+
function run(fn) {
|
|
666
|
+
return async (...args) => {
|
|
667
|
+
try {
|
|
668
|
+
await fn(...args);
|
|
669
|
+
} catch (err) {
|
|
670
|
+
fail(err);
|
|
671
|
+
}
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
function fail(err) {
|
|
675
|
+
if (err instanceof Error && err.name === "ExitPromptError") process.exit(130);
|
|
676
|
+
console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
677
|
+
process.exit(1);
|
|
678
|
+
}
|
|
679
|
+
//#endregion
|
|
680
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "atlass",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "CLI to copy Jira issues and Confluence pages to Markdown.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"bin": {
|
|
7
|
+
"atlass": "./dist/cli.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist"
|
|
11
|
+
],
|
|
12
|
+
"type": "module",
|
|
13
|
+
"exports": {
|
|
14
|
+
".": "./dist/cli.mjs",
|
|
15
|
+
"./package.json": "./package.json"
|
|
16
|
+
},
|
|
17
|
+
"publishConfig": {
|
|
18
|
+
"access": "public"
|
|
19
|
+
},
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "vp pack",
|
|
22
|
+
"dev": "vp pack --watch",
|
|
23
|
+
"test": "vp test",
|
|
24
|
+
"check": "vp check",
|
|
25
|
+
"prepublishOnly": "vp run build",
|
|
26
|
+
"prepare": "vp config",
|
|
27
|
+
"release": "bumpp"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@inquirer/prompts": "^8.5.2",
|
|
31
|
+
"@napi-rs/keyring": "^1.3.0",
|
|
32
|
+
"commander": "^15.0.0"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@types/node": "^25.6.2",
|
|
36
|
+
"@typescript/native-preview": "7.0.0-dev.20260509.2",
|
|
37
|
+
"bumpp": "^11.1.0",
|
|
38
|
+
"typescript": "^6.0.3",
|
|
39
|
+
"vite-plus": "^0.1.20"
|
|
40
|
+
},
|
|
41
|
+
"packageManager": "pnpm@11.9.0"
|
|
42
|
+
}
|