@stravica-ai/digital-operator-pa 0.1.39 → 0.1.41

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 (92) hide show
  1. package/README.md +12 -2
  2. package/bin/confluence +9 -0
  3. package/bin/confluence-fetch +9 -0
  4. package/bin/jira +9 -0
  5. package/dist/engineering-pa-core/knowledge/content-voice/banned-ai-tells-taxonomy.md +1 -1
  6. package/dist/engineering-pa-core/knowledge/knowledge-memory/staging-and-observations.md +3 -3
  7. package/dist/engineering-pa-core/knowledge/platform-tooling/workspace-extensions.md +62 -0
  8. package/dist/engineering-pa-core-extensions-atlassian/README.md +81 -16
  9. package/dist/engineering-pa-core-extensions-atlassian/bin/confluence +2 -0
  10. package/dist/engineering-pa-core-extensions-atlassian/bin/confluence-fetch +2 -0
  11. package/dist/engineering-pa-core-extensions-atlassian/bin/jira +2 -0
  12. package/dist/engineering-pa-core-extensions-atlassian/config/jira-workflow-chains.default.json +4 -0
  13. package/dist/engineering-pa-core-extensions-atlassian/index.mjs +17 -6
  14. package/dist/engineering-pa-core-extensions-atlassian/package.json +11 -4
  15. package/dist/engineering-pa-core-extensions-atlassian/src/common/invalidate-client.mjs +137 -0
  16. package/dist/engineering-pa-core-extensions-atlassian/src/common/pa-name.mjs +58 -0
  17. package/dist/engineering-pa-core-extensions-atlassian/src/common/secrets.mjs +158 -0
  18. package/dist/engineering-pa-core-extensions-atlassian/src/confluence/cli.mjs +165 -0
  19. package/dist/engineering-pa-core-extensions-atlassian/src/confluence/client.mjs +294 -0
  20. package/dist/engineering-pa-core-extensions-atlassian/src/confluence/commands/auth.mjs +59 -0
  21. package/dist/engineering-pa-core-extensions-atlassian/src/confluence/commands/comment.mjs +174 -0
  22. package/dist/engineering-pa-core-extensions-atlassian/src/confluence/commands/create.mjs +136 -0
  23. package/dist/engineering-pa-core-extensions-atlassian/src/confluence/commands/label.mjs +98 -0
  24. package/dist/engineering-pa-core-extensions-atlassian/src/confluence/commands/mine.mjs +105 -0
  25. package/dist/engineering-pa-core-extensions-atlassian/src/confluence/commands/mirror-folder.mjs +487 -0
  26. package/dist/engineering-pa-core-extensions-atlassian/src/confluence/commands/mirror-page.mjs +351 -0
  27. package/dist/engineering-pa-core-extensions-atlassian/src/confluence/commands/page.mjs +127 -0
  28. package/dist/engineering-pa-core-extensions-atlassian/src/confluence/commands/pages.mjs +107 -0
  29. package/dist/engineering-pa-core-extensions-atlassian/src/confluence/commands/recent.mjs +107 -0
  30. package/dist/engineering-pa-core-extensions-atlassian/src/confluence/commands/search.mjs +123 -0
  31. package/dist/engineering-pa-core-extensions-atlassian/src/confluence/commands/spaces.mjs +82 -0
  32. package/dist/engineering-pa-core-extensions-atlassian/src/confluence/commands/test.mjs +155 -0
  33. package/dist/engineering-pa-core-extensions-atlassian/src/confluence/commands/tree.mjs +103 -0
  34. package/dist/engineering-pa-core-extensions-atlassian/src/confluence/commands/update.mjs +134 -0
  35. package/dist/engineering-pa-core-extensions-atlassian/src/confluence/format.mjs +260 -0
  36. package/dist/engineering-pa-core-extensions-atlassian/src/confluence/mirror.mjs +353 -0
  37. package/dist/engineering-pa-core-extensions-atlassian/src/confluence/storage.mjs +880 -0
  38. package/dist/engineering-pa-core-extensions-atlassian/src/confluence-fetch/cli.mjs +313 -0
  39. package/dist/engineering-pa-core-extensions-atlassian/src/confluence-fetch/fetch.mjs +327 -0
  40. package/dist/engineering-pa-core-extensions-atlassian/src/jira/adf.mjs +388 -0
  41. package/dist/engineering-pa-core-extensions-atlassian/src/jira/bug-fields.mjs +294 -0
  42. package/dist/engineering-pa-core-extensions-atlassian/src/jira/cli.mjs +197 -0
  43. package/dist/engineering-pa-core-extensions-atlassian/src/jira/client.mjs +350 -0
  44. package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/advance.mjs +486 -0
  45. package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/attach.mjs +129 -0
  46. package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/auth.mjs +53 -0
  47. package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/boards.mjs +88 -0
  48. package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/bulk-transition.mjs +223 -0
  49. package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/cancel.mjs +135 -0
  50. package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/comment.mjs +169 -0
  51. package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/create.mjs +376 -0
  52. package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/issue.mjs +105 -0
  53. package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/link.mjs +204 -0
  54. package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/mine.mjs +97 -0
  55. package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/projects.mjs +91 -0
  56. package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/search.mjs +80 -0
  57. package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/sprint.mjs +111 -0
  58. package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/sync-statuses.mjs +313 -0
  59. package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/test.mjs +142 -0
  60. package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/transition.mjs +197 -0
  61. package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/update.mjs +284 -0
  62. package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/worklog.mjs +380 -0
  63. package/dist/engineering-pa-core-extensions-atlassian/src/jira/format.mjs +267 -0
  64. package/dist/engineering-pa-core-extensions-atlassian/src/jira/invalidate.mjs +72 -0
  65. package/dist/engineering-pa-core-extensions-atlassian/src/jira/metadata.mjs +225 -0
  66. package/dist/engineering-pa-core-extensions-atlassian/src/jira/required-fields.mjs +334 -0
  67. package/dist/engineering-pa-core-extensions-atlassian/src/jira/workflow-chains.mjs +405 -0
  68. package/dist/engineering-pa-core-extensions-atlassian/src/jira/workflow-map.mjs +107 -0
  69. package/dist/engineering-pa-scaffold/README.md +32 -1
  70. package/dist/engineering-pa-scaffold/src/cli.mjs +83 -6
  71. package/dist/engineering-pa-scaffold/src/commands/init-workspace-extension.mjs +234 -0
  72. package/dist/engineering-pa-scaffold/src/commands/init.mjs +6 -5
  73. package/dist/engineering-pa-scaffold/src/generator.mjs +240 -4
  74. package/dist/engineering-pa-scaffold/templates/CLAUDE.md.template +7 -7
  75. package/dist/engineering-pa-scaffold/templates/operator-README.md.template +4 -4
  76. package/dist/engineering-pa-scaffold/templates/root-CLAUDE.md.template +13 -0
  77. package/dist/engineering-pa-types-engineering/index.mjs +14 -6
  78. package/dist/engineering-pa-types-engineering/knowledge/rcf-lite/create-a-small-app.md +81 -0
  79. package/dist/engineering-pa-types-engineering/knowledge/rcf-lite/for-agents.md +25 -0
  80. package/dist/engineering-pa-types-engineering/knowledge/rcf-lite/index.md +34 -0
  81. package/dist/engineering-pa-types-engineering/knowledge/rcf-lite/reference.md +16 -0
  82. package/dist/engineering-pa-types-engineering/knowledge/rcf-lite/wire-your-agent.md +17 -0
  83. package/dist/engineering-pa-types-engineering/package.json +2 -2
  84. package/dist/engineering-pa-types-engineering/roles/rcf-lite-engineer/definition.md +183 -0
  85. package/dist/engineering-pa-workspace/app/home.html +7 -7
  86. package/dist/engineering-pa-workspace/app/onboarding.html +4 -4
  87. package/dist/engineering-pa-workspace/src/api.mjs +1 -1
  88. package/dist/engineering-pa-workspace/src/server.mjs +1 -1
  89. package/docs/installing.md +78 -0
  90. package/docs/onboarding-solo.md +162 -0
  91. package/docs/what-it-can-do.md +85 -0
  92. package/package.json +6 -2
@@ -0,0 +1,351 @@
1
+ // mirror-page.mjs - Mirror a Confluence page subtree (descendants) to local markdown.
2
+ //
3
+ // Usage:
4
+ // confluence mirror-page <PAGE_ID> [--target DIR] [--dry-run] [--no-reindex]
5
+ //
6
+ // Behaviour:
7
+ // Mirror semantics match mirror-folder. The only difference is the root
8
+ // node: instead of a folder, the subtree root is a Confluence page, and
9
+ // we walk via the page-children endpoint. The root page itself is NOT
10
+ // written - only its descendants are mirrored - matching mirror-folder
11
+ // which excludes the root folder. Same staging + atomic-swap + reindex
12
+ // pipeline.
13
+ //
14
+ // Why this exists alongside mirror-folder:
15
+ // Some Confluence sites keep a content set under a single top-level page
16
+ // rather than a folder. The v2 folder/direct-children endpoint can't reach page-rooted
17
+ // trees, so we need an explicit page-rooted entry point.
18
+
19
+ import { mkdir, writeFile, rm, readdir, rename, stat, readFile } from 'node:fs/promises';
20
+ import { basename, join, resolve, dirname } from 'node:path';
21
+ import { fileURLToPath } from 'node:url';
22
+ import { spawnSync } from 'node:child_process';
23
+
24
+ import { createClient, ConfluenceError } from '../client.mjs';
25
+ import { storageToMarkdown } from '../storage.mjs';
26
+ import {
27
+ composeMarkdown,
28
+ planTree,
29
+ splitForSwap,
30
+ pageUrl,
31
+ buildManifest,
32
+ serialiseManifest,
33
+ } from '../mirror.mjs';
34
+
35
+ const __dirname = dirname(fileURLToPath(import.meta.url));
36
+ const DEFAULT_REPO_ROOT = resolve(__dirname, '..', '..', '..', '..', '..', '..');
37
+
38
+ const HELP = `confluence mirror-page - Mirror a Confluence page subtree to local markdown
39
+
40
+ Usage:
41
+ confluence mirror-page <PAGE_ID> [options]
42
+
43
+ Arguments:
44
+ PAGE_ID Numeric Confluence page ID (root of the subtree).
45
+
46
+ Options:
47
+ --target DIR Target directory (default: operator/knowledge/mirror/).
48
+ --dry-run Walk the tree and print the plan; write nothing.
49
+ --no-reindex Skip the npm run reindex step after a successful mirror.
50
+ --help, -h Show this help.
51
+
52
+ Behaviour:
53
+ Walks the page's descendants (recursively), fetches each as markdown,
54
+ and mirrors the hierarchy into the target directory. The root page itself
55
+ is NOT included - only its descendants. README.md in the target is
56
+ preserved across runs; everything else is wiped and replaced.
57
+
58
+ Examples:
59
+ confluence mirror-page 3543138535
60
+ confluence mirror-page 3543138535 --target /tmp/scratch --dry-run
61
+ `;
62
+
63
+ export async function run(argv = [], deps = {}) {
64
+ const log = deps.log || console.log;
65
+ const logProgress = deps.logProgress || ((msg) => console.error(msg));
66
+ const logError = deps.logError || console.error;
67
+ const { parseArgs } = await import('../cli.mjs');
68
+ const args = parseArgs(argv);
69
+
70
+ if (args.help || args.h) {
71
+ log(HELP);
72
+ return 0;
73
+ }
74
+
75
+ const pageId = args._[0];
76
+ if (!pageId) {
77
+ logError('Usage: confluence mirror-page <PAGE_ID> [--target DIR] [--dry-run]');
78
+ return 1;
79
+ }
80
+
81
+ const repoRoot = deps.repoRoot || process.env.DAVE_ROOT || DEFAULT_REPO_ROOT;
82
+ const target = args.target
83
+ ? resolve(args.target)
84
+ : join(repoRoot, 'operator', 'knowledge', 'mirror');
85
+ const dryRun = !!args['dry-run'];
86
+ const skipReindex = !!args['no-reindex'];
87
+
88
+ const client = deps.client || createClient();
89
+
90
+ let domain;
91
+ try {
92
+ domain = client.getCredentials().domain;
93
+ } catch (err) {
94
+ if (err instanceof ConfluenceError) {
95
+ logError(`Failed to load credentials: ${err.message}`);
96
+ return err.exitCode;
97
+ }
98
+ throw err;
99
+ }
100
+
101
+ try {
102
+ logProgress(`Walking Confluence page ${pageId}…`);
103
+ const rootMeta = await fetchPageMeta(client, pageId);
104
+ logProgress(` Page: ${rootMeta.title} (space ${rootMeta.spaceId})`);
105
+
106
+ const rootChildren = await collectPageDescendants(client, pageId, logProgress);
107
+ const pageCount = countPages(rootChildren);
108
+ logProgress(` Discovered ${pageCount} descendant page(s).`);
109
+
110
+ if (pageCount === 0) {
111
+ logError('No descendant pages found under page. Refusing to wipe target.');
112
+ return 2;
113
+ }
114
+
115
+ const plan = planTree(rootChildren);
116
+ if (dryRun) {
117
+ log(`Target: ${target}`);
118
+ log(`Root page: ${rootMeta.title} (${pageId})`);
119
+ log(`Plan (${plan.length} entries):`);
120
+ for (const entry of plan) {
121
+ log(` [${entry.kind}] ${entry.relPath} - ${entry.node.title}`);
122
+ }
123
+ return 0;
124
+ }
125
+
126
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-');
127
+ const stagingDir = join(target, `.mirror-tmp-${stamp}`);
128
+ logProgress(`Staging directory: ${stagingDir}`);
129
+
130
+ await mkdir(target, { recursive: true });
131
+ await mkdir(stagingDir, { recursive: true });
132
+
133
+ try {
134
+ const mirrorRunAt = new Date().toISOString();
135
+ const mirroredPageIds = [];
136
+ let writtenFiles = 0;
137
+ for (const entry of plan) {
138
+ const absPath = join(stagingDir, ...entry.relPath.split('/'));
139
+ if (entry.kind === 'dir') {
140
+ await mkdir(absPath, { recursive: true });
141
+ continue;
142
+ }
143
+ logProgress(` Fetching page ${entry.node.id} - ${entry.node.title}`);
144
+ const pageBody = await fetchPageMarkdown(client, entry.node.id);
145
+ const md = composeMarkdown(
146
+ {
147
+ title: entry.node.title,
148
+ pageId: entry.node.id,
149
+ url: pageUrl(domain, entry.node.id, pageBody.webuiPath),
150
+ space: rootMeta.spaceId ? String(rootMeta.spaceId) : undefined,
151
+ version: pageBody.version,
152
+ updatedAt: pageBody.updatedAt,
153
+ breadcrumb: entry.breadcrumb,
154
+ },
155
+ pageBody.markdown,
156
+ );
157
+ await mkdir(dirname(absPath), { recursive: true });
158
+ await writeFile(absPath, md, 'utf-8');
159
+ mirroredPageIds.push(String(entry.node.id));
160
+ writtenFiles++;
161
+ }
162
+ logProgress(` Wrote ${writtenFiles} file(s) to staging.`);
163
+
164
+ const mirrorToolVersion = await readToolVersion();
165
+ const manifest = buildManifest({
166
+ domainId: basename(target),
167
+ sourceFolderId: pageId,
168
+ sourceSpaceKey: rootMeta.spaceId ? String(rootMeta.spaceId) : undefined,
169
+ mirrorRunAt,
170
+ pageIds: mirroredPageIds,
171
+ mirrorToolVersion,
172
+ });
173
+ const manifestPath = join(stagingDir, 'manifest.json');
174
+ await writeFile(manifestPath, serialiseManifest(manifest), 'utf-8');
175
+ logProgress(` Wrote manifest.json (${manifest.pageCount} page IDs).`);
176
+
177
+ logProgress('Swapping staged content into target…');
178
+ await swapIntoTarget(target, stagingDir);
179
+ logProgress('Swap complete.');
180
+ } finally {
181
+ await rm(stagingDir, { recursive: true, force: true }).catch(() => {});
182
+ }
183
+
184
+ if (!skipReindex) {
185
+ logProgress('Running npm run reindex…');
186
+ const rc = runReindex(repoRoot, logProgress);
187
+ if (rc !== 0) {
188
+ // Soft-fail (acceptance ruling C2, 2026-07-14): on the composed v2
189
+ // workspace there is no local `reindex` script - domains.json is
190
+ // estate-composed and refreshed by the librarian, not by this tool.
191
+ // A completed mirror is a success; a stale local index is a warning.
192
+ logError(`Reindex exited with code ${rc}. Mirror completed; domains.json refresh is estate-side on v2 (warning only).`);
193
+ }
194
+ }
195
+
196
+ logProgress(`Mirror complete: ${pageCount} page(s) written to ${target}.`);
197
+ return 0;
198
+ } catch (err) {
199
+ if (err instanceof ConfluenceError) {
200
+ logError(`Mirror failed: ${err.message}`);
201
+ return err.exitCode;
202
+ }
203
+ logError(`Mirror failed: ${err.message || err}`);
204
+ return 2;
205
+ }
206
+ }
207
+
208
+ // ── API walkers ──────────────────────────────────────────────────────
209
+
210
+ async function fetchPageMeta(client, pageId) {
211
+ const { data } = await client.get(`/wiki/api/v2/pages/${encodeURIComponent(pageId)}`);
212
+ return data;
213
+ }
214
+
215
+ async function fetchPageChildren(client, pageId) {
216
+ const results = [];
217
+ let cursor = null;
218
+ for (let i = 0; i < 200; i++) {
219
+ const query = new URLSearchParams({ limit: '100' });
220
+ if (cursor) query.set('cursor', cursor);
221
+ const path = `/wiki/api/v2/pages/${encodeURIComponent(pageId)}/children?${query.toString()}`;
222
+ const { data } = await client.get(path);
223
+ for (const item of data.results || []) results.push(item);
224
+ const nextPath = data._links?.next;
225
+ if (!nextPath) break;
226
+ const nextCursor = extractCursor(nextPath);
227
+ if (!nextCursor || nextCursor === cursor) break;
228
+ cursor = nextCursor;
229
+ }
230
+ return results;
231
+ }
232
+
233
+ function extractCursor(nextPath) {
234
+ try {
235
+ const match = nextPath.match(/[?&]cursor=([^&]+)/);
236
+ if (match) return decodeURIComponent(match[1]);
237
+ } catch {
238
+ // fall through
239
+ }
240
+ return null;
241
+ }
242
+
243
+ async function collectPageDescendants(client, pageId, logProgress) {
244
+ const children = await fetchPageChildren(client, pageId);
245
+ const nodes = [];
246
+ for (const child of children) {
247
+ if (logProgress) logProgress(` ↳ page: ${child.title}`);
248
+ const sub = await collectPageDescendants(client, child.id, null);
249
+ nodes.push({
250
+ type: 'page',
251
+ id: String(child.id),
252
+ title: child.title || 'untitled',
253
+ children: sub,
254
+ });
255
+ }
256
+ return nodes;
257
+ }
258
+
259
+ function countPages(nodes) {
260
+ let n = 0;
261
+ for (const node of nodes) {
262
+ if (node.type === 'page') n++;
263
+ if (Array.isArray(node.children)) n += countPages(node.children);
264
+ }
265
+ return n;
266
+ }
267
+
268
+ // ── Page fetching ────────────────────────────────────────────────────
269
+
270
+ async function fetchPageMarkdown(client, pageId) {
271
+ const { data } = await client.get(
272
+ `/wiki/api/v2/pages/${encodeURIComponent(pageId)}?body-format=storage`,
273
+ );
274
+ const storageBody = data.body?.storage?.value || '';
275
+ const markdown = storageToMarkdown(storageBody);
276
+ return {
277
+ markdown,
278
+ version: data.version?.number,
279
+ updatedAt: data.version?.createdAt,
280
+ webuiPath: data._links?.webui,
281
+ };
282
+ }
283
+
284
+ // ── Atomic swap ──────────────────────────────────────────────────────
285
+
286
+ async function swapIntoTarget(target, stagingDir) {
287
+ const existing = await readdir(target);
288
+ const { toRemove } = splitForSwap(existing.filter((e) => !isStagingDir(e)));
289
+ for (const name of toRemove) {
290
+ await rm(join(target, name), { recursive: true, force: true });
291
+ }
292
+
293
+ const staged = await readdir(stagingDir);
294
+ for (const name of staged) {
295
+ const from = join(stagingDir, name);
296
+ const to = join(target, name);
297
+ try {
298
+ await stat(to);
299
+ continue;
300
+ } catch {
301
+ // safe to move
302
+ }
303
+ await rename(from, to);
304
+ }
305
+ }
306
+
307
+ function isStagingDir(name) {
308
+ return name.startsWith('.mirror-tmp-');
309
+ }
310
+
311
+ // ── Tool version ─────────────────────────────────────────────────────
312
+
313
+ async function readToolVersion() {
314
+ try {
315
+ const pkgPath = resolve(__dirname, '..', '..', 'package.json');
316
+ const raw = await readFile(pkgPath, 'utf-8');
317
+ const pkg = JSON.parse(raw);
318
+ if (typeof pkg.version === 'string' && pkg.version.length > 0) {
319
+ return pkg.version;
320
+ }
321
+ return undefined;
322
+ } catch {
323
+ return undefined;
324
+ }
325
+ }
326
+
327
+ // ── Reindex ──────────────────────────────────────────────────────────
328
+
329
+ function runReindex(repoRoot, logProgress) {
330
+ const result = spawnSync('npm', ['run', 'reindex'], {
331
+ cwd: repoRoot,
332
+ stdio: ['ignore', 'pipe', 'pipe'],
333
+ encoding: 'utf-8',
334
+ });
335
+ if (result.stdout) logProgress(result.stdout.trimEnd());
336
+ if (result.stderr) logProgress(result.stderr.trimEnd());
337
+ return result.status ?? 1;
338
+ }
339
+
340
+ // ── Re-exports for testing ───────────────────────────────────────────
341
+
342
+ export const _test = {
343
+ extractCursor,
344
+ countPages,
345
+ swapIntoTarget,
346
+ isStagingDir,
347
+ readToolVersion,
348
+ fetchPageMeta,
349
+ fetchPageChildren,
350
+ collectPageDescendants,
351
+ };
@@ -0,0 +1,127 @@
1
+ // page.mjs - Get a single Confluence page.
2
+ //
3
+ // Usage:
4
+ // confluence page <ID> [--format md|storage|text] [--save FILE]
5
+ //
6
+ // Default format: md (converts storage to markdown).
7
+ // Shows metadata: space, version, author, labels, modified date.
8
+
9
+ import { createClient, ConfluenceError } from '../client.mjs';
10
+ import { storageToMarkdown, storageToText } from '../storage.mjs';
11
+ import { formatPage, bold, dim, green } from '../format.mjs';
12
+ import { writeFileSync } from 'node:fs';
13
+
14
+ const HELP = `confluence page - Get page content
15
+
16
+ Usage:
17
+ confluence page <ID> [--format md|storage|text] [--save FILE]
18
+
19
+ Arguments:
20
+ ID Page ID (numeric)
21
+
22
+ Options:
23
+ --format FMT Output format: md (default), storage, text
24
+ --save FILE Save content to file
25
+ --help, -h Show this help
26
+
27
+ Examples:
28
+ confluence page 12345
29
+ confluence page 12345 --format storage
30
+ confluence page 12345 --format md --save output.md
31
+ `;
32
+
33
+ /**
34
+ * Run the page command.
35
+ *
36
+ * @param {string[]} argv Remaining args after 'page'
37
+ * @param {object} [deps] Dependency overrides
38
+ * @returns {Promise<number>} Exit code
39
+ */
40
+ export async function run(argv = [], deps = {}) {
41
+ const log = deps.log || console.log;
42
+ const logError = deps.logError || console.error;
43
+ const { parseArgs } = await import('../cli.mjs');
44
+ const args = parseArgs(argv);
45
+
46
+ if (args.help || args.h) {
47
+ log(HELP);
48
+ return 0;
49
+ }
50
+
51
+ const pageId = args._[0];
52
+ if (!pageId) {
53
+ logError('Usage: confluence page <ID> [--format md|storage|text] [--save FILE]');
54
+ return 1;
55
+ }
56
+
57
+ const format = args.format || 'md';
58
+ if (!['md', 'storage', 'text'].includes(format)) {
59
+ logError('--format must be one of: md, storage, text');
60
+ return 1;
61
+ }
62
+
63
+ const saveFile = args.save || null;
64
+
65
+ const client = deps.client || createClient();
66
+
67
+ try {
68
+ // Fetch page with body in storage format
69
+ const { data: page } = await client.get(
70
+ `/wiki/api/v2/pages/${encodeURIComponent(pageId)}?body-format=storage`
71
+ );
72
+
73
+ // Fetch labels
74
+ let labels = [];
75
+ try {
76
+ const { data: labelData } = await client.get(
77
+ `/wiki/api/v2/pages/${encodeURIComponent(pageId)}/labels`
78
+ );
79
+ labels = (labelData.results || []).map((l) => l.name || l.label || l.prefix + ':' + l.name);
80
+ } catch {
81
+ // Labels fetch may fail - non-critical
82
+ }
83
+
84
+ // Enrich page object for formatting
85
+ page._labels = labels;
86
+
87
+ // Print metadata
88
+ log(formatPage(page));
89
+ log('');
90
+
91
+ // Extract storage body
92
+ const storageBody = page.body?.storage?.value || '';
93
+
94
+ // Convert to requested format
95
+ let content;
96
+ switch (format) {
97
+ case 'storage':
98
+ content = storageBody;
99
+ break;
100
+ case 'text':
101
+ content = storageToText(storageBody);
102
+ break;
103
+ case 'md':
104
+ default:
105
+ content = storageToMarkdown(storageBody);
106
+ break;
107
+ }
108
+
109
+ // Save or print
110
+ if (saveFile) {
111
+ const writeFn = deps.writeFileSync || writeFileSync;
112
+ writeFn(saveFile, content, 'utf-8');
113
+ log(green(`Content saved to ${saveFile}`));
114
+ } else {
115
+ log(dim('--- Content ---'));
116
+ log(content);
117
+ }
118
+
119
+ return 0;
120
+ } catch (err) {
121
+ if (err instanceof ConfluenceError) {
122
+ logError(`Failed to get page: ${err.message}`);
123
+ return err.exitCode;
124
+ }
125
+ throw err;
126
+ }
127
+ }
@@ -0,0 +1,107 @@
1
+ // pages.mjs - List pages in a Confluence space.
2
+ //
3
+ // Usage:
4
+ // confluence pages <SPACE_KEY> [--limit N]
5
+ //
6
+ // Resolves space key → space ID via API, then lists pages.
7
+
8
+ import { createClient, ConfluenceError } from '../client.mjs';
9
+ import { formatPageList, bold, dim } from '../format.mjs';
10
+
11
+ const HELP = `confluence pages - List pages in a space
12
+
13
+ Usage:
14
+ confluence pages <SPACE_KEY> [--limit N]
15
+
16
+ Arguments:
17
+ SPACE_KEY Space key (e.g. DEV, DOCS)
18
+
19
+ Options:
20
+ --limit N Maximum pages to return (default: 25)
21
+ --help, -h Show this help
22
+
23
+ Examples:
24
+ confluence pages DEV
25
+ confluence pages DOCS --limit 50
26
+ `;
27
+
28
+ /**
29
+ * Resolve a space key to a space ID via the v2 API.
30
+ *
31
+ * @param {object} client Confluence client
32
+ * @param {string} spaceKey Space key (e.g. DEV)
33
+ * @returns {Promise<string>} Space ID
34
+ */
35
+ async function resolveSpaceId(client, spaceKey) {
36
+ const { data } = await client.get(
37
+ `/wiki/api/v2/spaces?keys=${encodeURIComponent(spaceKey)}&limit=1`
38
+ );
39
+ const results = data.results || [];
40
+ if (results.length === 0) {
41
+ throw new ConfluenceError(`Space not found: ${spaceKey}`, 2);
42
+ }
43
+ return results[0].id;
44
+ }
45
+
46
+ /**
47
+ * Run the pages command.
48
+ *
49
+ * @param {string[]} argv Remaining args after 'pages'
50
+ * @param {object} [deps] Dependency overrides
51
+ * @returns {Promise<number>} Exit code
52
+ */
53
+ export async function run(argv = [], deps = {}) {
54
+ const log = deps.log || console.log;
55
+ const logError = deps.logError || console.error;
56
+ const { parseArgs } = await import('../cli.mjs');
57
+ const args = parseArgs(argv);
58
+
59
+ if (args.help || args.h) {
60
+ log(HELP);
61
+ return 0;
62
+ }
63
+
64
+ const spaceKey = args._[0];
65
+ if (!spaceKey) {
66
+ logError('Usage: confluence pages <SPACE_KEY> [--limit N]');
67
+ return 1;
68
+ }
69
+
70
+ const limit = args.limit ? parseInt(args.limit, 10) : 25;
71
+ if (isNaN(limit) || limit < 1) {
72
+ logError('--limit must be a positive integer');
73
+ return 1;
74
+ }
75
+
76
+ const client = deps.client || createClient();
77
+
78
+ try {
79
+ const spaceId = await resolveSpaceId(client, spaceKey.toUpperCase());
80
+
81
+ const pages = [];
82
+ let count = 0;
83
+ for await (const page of client.paginateCursor(`/wiki/api/v2/spaces/${spaceId}/pages`, {
84
+ sort: '-modified-date',
85
+ })) {
86
+ pages.push(page);
87
+ count++;
88
+ if (count >= limit) break;
89
+ }
90
+
91
+ if (pages.length === 0) {
92
+ log(dim(`No pages found in space ${spaceKey}.`));
93
+ return 0;
94
+ }
95
+
96
+ log(bold(`Pages in ${spaceKey.toUpperCase()}`) + dim(` (${pages.length})\n`));
97
+ log(formatPageList(pages));
98
+
99
+ return 0;
100
+ } catch (err) {
101
+ if (err instanceof ConfluenceError) {
102
+ logError(`Failed to list pages: ${err.message}`);
103
+ return err.exitCode;
104
+ }
105
+ throw err;
106
+ }
107
+ }
@@ -0,0 +1,107 @@
1
+ // recent.mjs - Recently modified Confluence pages.
2
+ //
3
+ // Usage:
4
+ // confluence recent [--space SPACE] [--days N]
5
+ //
6
+ // Uses CQL: lastModified > now("-Nd")
7
+ // Default: 7 days.
8
+
9
+ import { createClient, ConfluenceError } from '../client.mjs';
10
+ import { formatTable, bold, dim } from '../format.mjs';
11
+
12
+ const HELP = `confluence recent - Recently modified pages
13
+
14
+ Usage:
15
+ confluence recent [--space KEY] [--days N]
16
+
17
+ Options:
18
+ --space KEY Filter by space key
19
+ --days N How many days back to look (default: 7)
20
+ --help, -h Show this help
21
+
22
+ Examples:
23
+ confluence recent
24
+ confluence recent --space DEV --days 3
25
+ `;
26
+
27
+ /**
28
+ * Run the recent command.
29
+ *
30
+ * @param {string[]} argv Remaining args after 'recent'
31
+ * @param {object} [deps] Dependency overrides
32
+ * @returns {Promise<number>} Exit code
33
+ */
34
+ export async function run(argv = [], deps = {}) {
35
+ const log = deps.log || console.log;
36
+ const logError = deps.logError || console.error;
37
+ const { parseArgs } = await import('../cli.mjs');
38
+ const args = parseArgs(argv);
39
+
40
+ if (args.help || args.h) {
41
+ log(HELP);
42
+ return 0;
43
+ }
44
+
45
+ const space = args.space || null;
46
+ const days = args.days ? parseInt(args.days, 10) : 7;
47
+
48
+ if (isNaN(days) || days < 1) {
49
+ logError('--days must be a positive integer');
50
+ return 1;
51
+ }
52
+
53
+ const client = deps.client || createClient();
54
+
55
+ try {
56
+ let cql = `type = page AND lastModified > now("-${days}d") ORDER BY lastModified DESC`;
57
+ if (space) {
58
+ cql = `type = page AND space = "${space.toUpperCase()}" AND lastModified > now("-${days}d") ORDER BY lastModified DESC`;
59
+ }
60
+
61
+ const results = [];
62
+ for await (const item of client.paginateOffset('/wiki/rest/api/content/search', {
63
+ cql,
64
+ }, 25)) {
65
+ results.push(item);
66
+ if (results.length >= 25) break;
67
+ }
68
+
69
+ if (results.length === 0) {
70
+ log(dim(`No pages modified in the last ${days} day(s).`));
71
+ return 0;
72
+ }
73
+
74
+ log(bold(`Recently modified pages`) + dim(` (last ${days} days)\n`));
75
+
76
+ const rows = results.map((item) => ({
77
+ id: item.id || '',
78
+ title: item.title || '',
79
+ space: item.space?.key || '',
80
+ modified: item.version?.when ? formatDate(item.version.when) : '',
81
+ }));
82
+
83
+ log(formatTable(rows, [
84
+ { key: 'id', label: 'ID', width: 12 },
85
+ { key: 'title', label: 'Title', width: 50 },
86
+ { key: 'space', label: 'Space', width: 10 },
87
+ { key: 'modified', label: 'Modified', width: 12 },
88
+ ]));
89
+
90
+ return 0;
91
+ } catch (err) {
92
+ if (err instanceof ConfluenceError) {
93
+ logError(`Failed to fetch recent pages: ${err.message}`);
94
+ return err.exitCode;
95
+ }
96
+ throw err;
97
+ }
98
+ }
99
+
100
+ function formatDate(isoString) {
101
+ if (!isoString) return '';
102
+ try {
103
+ return new Date(isoString).toISOString().slice(0, 10);
104
+ } catch {
105
+ return isoString;
106
+ }
107
+ }