@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,123 @@
1
+ // search.mjs - Search Confluence with CQL.
2
+ //
3
+ // Usage:
4
+ // confluence search <CQL> [--max N] [--expand]
5
+ //
6
+ // Uses v1 endpoint: GET /wiki/rest/api/content/search?cql=...
7
+ // --expand includes body.storage in results.
8
+
9
+ import { createClient, ConfluenceError } from '../client.mjs';
10
+ import { formatTable, bold, dim } from '../format.mjs';
11
+
12
+ const HELP = `confluence search - CQL search
13
+
14
+ Usage:
15
+ confluence search <CQL> [--max N] [--expand]
16
+
17
+ Arguments:
18
+ CQL CQL query string (quote if it contains spaces)
19
+
20
+ Options:
21
+ --max N Maximum results to return (default: 25)
22
+ --expand Include body.storage in results
23
+ --help, -h Show this help
24
+
25
+ CQL examples:
26
+ confluence search "text ~ 'api documentation'"
27
+ confluence search "space = DEV AND type = page"
28
+ confluence search "label = 'important' ORDER BY lastModified DESC"
29
+ confluence search "creator = currentUser() AND lastModified > now('-7d')"
30
+ `;
31
+
32
+ /**
33
+ * Run the search command.
34
+ *
35
+ * @param {string[]} argv Remaining args after 'search'
36
+ * @param {object} [deps] Dependency overrides
37
+ * @returns {Promise<number>} Exit code
38
+ */
39
+ export async function run(argv = [], deps = {}) {
40
+ const log = deps.log || console.log;
41
+ const logError = deps.logError || console.error;
42
+ const { parseArgs } = await import('../cli.mjs');
43
+ const args = parseArgs(argv);
44
+
45
+ if (args.help || args.h) {
46
+ log(HELP);
47
+ return 0;
48
+ }
49
+
50
+ const cql = args._.join(' ');
51
+ if (!cql) {
52
+ logError('Usage: confluence search <CQL> [--max N] [--expand]');
53
+ return 1;
54
+ }
55
+
56
+ const maxResults = args.max ? parseInt(args.max, 10) : 25;
57
+ if (isNaN(maxResults) || maxResults < 1) {
58
+ logError('--max must be a positive integer');
59
+ return 1;
60
+ }
61
+
62
+ const expand = args.expand === true;
63
+
64
+ const client = deps.client || createClient();
65
+
66
+ try {
67
+ const params = {
68
+ cql,
69
+ limit: String(maxResults),
70
+ };
71
+ if (expand) {
72
+ params.expand = 'body.storage';
73
+ }
74
+
75
+ const results = [];
76
+ let count = 0;
77
+ for await (const item of client.paginateOffset('/wiki/rest/api/content/search', params, maxResults)) {
78
+ results.push(item);
79
+ count++;
80
+ if (count >= maxResults) break;
81
+ }
82
+
83
+ if (results.length === 0) {
84
+ log(dim('No results found.'));
85
+ return 0;
86
+ }
87
+
88
+ log(bold('Search results') + dim(` (${results.length})\n`));
89
+
90
+ const rows = results.map((item) => ({
91
+ id: item.id || '',
92
+ title: item.title || '',
93
+ type: item.type || '',
94
+ space: item._expandable?.space ? '' : (item.space?.key || ''),
95
+ modified: item.version?.when ? formatDate(item.version.when) : '',
96
+ }));
97
+
98
+ log(formatTable(rows, [
99
+ { key: 'id', label: 'ID', width: 12 },
100
+ { key: 'title', label: 'Title', width: 50 },
101
+ { key: 'type', label: 'Type', width: 10 },
102
+ { key: 'space', label: 'Space', width: 10 },
103
+ { key: 'modified', label: 'Modified', width: 12 },
104
+ ]));
105
+
106
+ return 0;
107
+ } catch (err) {
108
+ if (err instanceof ConfluenceError) {
109
+ logError(`Search failed: ${err.message}`);
110
+ return err.exitCode;
111
+ }
112
+ throw err;
113
+ }
114
+ }
115
+
116
+ function formatDate(isoString) {
117
+ if (!isoString) return '';
118
+ try {
119
+ return new Date(isoString).toISOString().slice(0, 10);
120
+ } catch {
121
+ return isoString;
122
+ }
123
+ }
@@ -0,0 +1,82 @@
1
+ // spaces.mjs - List Confluence spaces.
2
+ //
3
+ // Usage:
4
+ // confluence spaces [--filter QUERY] [--status current|archived|all]
5
+ //
6
+ // Fetches spaces via v2 cursor pagination and renders as a table.
7
+ // Default status: current.
8
+
9
+ import { createClient, ConfluenceError } from '../client.mjs';
10
+ import { formatSpaceList, bold, dim } from '../format.mjs';
11
+
12
+ const HELP = `confluence spaces - List Confluence spaces
13
+
14
+ Usage:
15
+ confluence spaces [--filter QUERY] [--status current|archived|all]
16
+
17
+ Options:
18
+ --filter QUERY Case-insensitive filter on space name or key
19
+ --status STATUS Filter by status: current (default), archived, all
20
+ --help, -h Show this help
21
+ `;
22
+
23
+ /**
24
+ * Run the spaces command.
25
+ *
26
+ * @param {string[]} argv Remaining args after 'spaces'
27
+ * @param {object} [deps] Dependency overrides
28
+ * @returns {Promise<number>} Exit code
29
+ */
30
+ export async function run(argv = [], deps = {}) {
31
+ const log = deps.log || console.log;
32
+ const logError = deps.logError || console.error;
33
+ const { parseArgs } = await import('../cli.mjs');
34
+ const args = parseArgs(argv);
35
+
36
+ if (args.help || args.h) {
37
+ log(HELP);
38
+ return 0;
39
+ }
40
+
41
+ const filter = args.filter ? args.filter.toLowerCase() : null;
42
+ const status = args.status || 'current';
43
+
44
+ const client = deps.client || createClient();
45
+
46
+ try {
47
+ const params = {};
48
+ if (status !== 'all') {
49
+ params.status = status;
50
+ }
51
+
52
+ const spaces = [];
53
+ for await (const space of client.paginateCursor('/wiki/api/v2/spaces', params)) {
54
+ spaces.push(space);
55
+ }
56
+
57
+ let filtered = spaces;
58
+ if (filter) {
59
+ filtered = spaces.filter((s) => {
60
+ const name = (s.name || '').toLowerCase();
61
+ const key = (s.key || '').toLowerCase();
62
+ return name.includes(filter) || key.includes(filter);
63
+ });
64
+ }
65
+
66
+ if (filtered.length === 0) {
67
+ log(dim(filter ? `No spaces matching "${filter}".` : 'No spaces found.'));
68
+ return 0;
69
+ }
70
+
71
+ log(bold('Spaces') + dim(` (${filtered.length}${filter ? ' matching' : ''})\n`));
72
+ log(formatSpaceList(filtered));
73
+
74
+ return 0;
75
+ } catch (err) {
76
+ if (err instanceof ConfluenceError) {
77
+ logError(`Failed to list spaces: ${err.message}`);
78
+ return err.exitCode;
79
+ }
80
+ throw err;
81
+ }
82
+ }
@@ -0,0 +1,155 @@
1
+ // test.mjs - Live API contract smoke test.
2
+ //
3
+ // Hits key Confluence endpoints and validates response shapes. NO MUTATIONS.
4
+ // Used to verify the API still behaves as expected after upgrades,
5
+ // config changes, or permission adjustments.
6
+ //
7
+ // Exit 0 if all pass, 2 if any fail.
8
+
9
+ import { createClient, ConfluenceError } from '../client.mjs';
10
+ import { bold, dim, green, red, yellow } from '../format.mjs';
11
+
12
+ const HELP = `confluence test - Live API contract smoke test
13
+
14
+ Usage:
15
+ confluence test
16
+
17
+ Runs read-only checks against the Confluence API to validate response shapes
18
+ and connectivity. No data is created, modified, or deleted.
19
+
20
+ Options:
21
+ --help, -h Show this help
22
+ `;
23
+
24
+ /**
25
+ * Run the test command.
26
+ *
27
+ * @param {string[]} argv Remaining args after 'test'
28
+ * @param {object} [deps] Dependency overrides
29
+ * @returns {Promise<number>} Exit code
30
+ */
31
+ export async function run(argv = [], deps = {}) {
32
+ const log = deps.log || console.log;
33
+ const logError = deps.logError || console.error;
34
+
35
+ if (argv.includes('--help') || argv.includes('-h')) {
36
+ log(HELP);
37
+ return 0;
38
+ }
39
+
40
+ const client = deps.client || createClient();
41
+
42
+ log(bold('Confluence API Contract Tests\n'));
43
+
44
+ const results = [];
45
+
46
+ // ── Test 1: Auth check ───────────────────────────────────────────
47
+ results.push(await runTest(log, 'Auth (GET /wiki/rest/api/user/current)', async () => {
48
+ const { data } = await client.get('/wiki/rest/api/user/current');
49
+ assert(data.displayName, 'missing displayName');
50
+ assert(data.accountId || data.username, 'missing accountId or username');
51
+ return `user: ${data.displayName}`;
52
+ }));
53
+
54
+ // ── Test 2: Spaces (v2) ──────────────────────────────────────────
55
+ let firstSpaceKey = null;
56
+ results.push(await runTest(log, 'Spaces (GET /wiki/api/v2/spaces)', async () => {
57
+ const { data } = await client.get('/wiki/api/v2/spaces?limit=5');
58
+ assert(Array.isArray(data.results), 'results is not an array');
59
+ if (data.results.length > 0) {
60
+ firstSpaceKey = data.results[0].key;
61
+ }
62
+ return `${data.results.length} space(s)${firstSpaceKey ? `, first: ${firstSpaceKey}` : ''}`;
63
+ }));
64
+
65
+ // ── Test 3: Pages in space (v2) ──────────────────────────────────
66
+ let firstPageId = null;
67
+ results.push(await runTest(log, 'Pages in space (GET /wiki/api/v2/spaces/{id}/pages)', async () => {
68
+ if (!firstSpaceKey) {
69
+ return 'SKIP: no space found';
70
+ }
71
+ // Resolve space key to ID
72
+ const { data: spaceData } = await client.get(
73
+ `/wiki/api/v2/spaces?keys=${encodeURIComponent(firstSpaceKey)}&limit=1`
74
+ );
75
+ const spaceId = spaceData.results[0]?.id;
76
+ if (!spaceId) return 'SKIP: could not resolve space ID';
77
+
78
+ const { data } = await client.get(`/wiki/api/v2/spaces/${spaceId}/pages?limit=5`);
79
+ assert(Array.isArray(data.results), 'results is not an array');
80
+ if (data.results.length > 0) {
81
+ firstPageId = data.results[0].id;
82
+ }
83
+ return `${data.results.length} page(s)${firstPageId ? `, first ID: ${firstPageId}` : ''}`;
84
+ }));
85
+
86
+ // ── Test 4: Page fetch (v2) ──────────────────────────────────────
87
+ results.push(await runTest(log, 'Page fetch (GET /wiki/api/v2/pages/{id})', async () => {
88
+ if (!firstPageId) {
89
+ return 'SKIP: no page from listing to fetch';
90
+ }
91
+ const { data } = await client.get(
92
+ `/wiki/api/v2/pages/${firstPageId}?body-format=storage`
93
+ );
94
+ assert(data.title, 'missing title');
95
+ assert(data.id, 'missing id');
96
+ return `${data.id}: ${data.title}`;
97
+ }));
98
+
99
+ // ── Test 5: CQL search (v1) ─────────────────────────────────────
100
+ results.push(await runTest(log, 'CQL search (GET /wiki/rest/api/content/search)', async () => {
101
+ const { data } = await client.get(
102
+ `/wiki/rest/api/content/search?cql=${encodeURIComponent('type = page')}&limit=3`
103
+ );
104
+ assert(Array.isArray(data.results), 'results is not an array');
105
+ return `${data.results.length} result(s)`;
106
+ }));
107
+
108
+ // ── Summary ──────────────────────────────────────────────────────
109
+ const passed = results.filter((r) => r.passed).length;
110
+ const total = results.length;
111
+ const skipped = results.filter((r) => r.skipped).length;
112
+
113
+ log('');
114
+ const color = passed === total ? green : (passed >= total - 1 ? yellow : red);
115
+ log(color(bold(`${passed}/${total} passed`)) + (skipped ? dim(` (${skipped} skipped)`) : ''));
116
+
117
+ return passed === total ? 0 : 2;
118
+ }
119
+
120
+ /**
121
+ * Run a single test and print result.
122
+ *
123
+ * @param {Function} log Output function
124
+ * @param {string} name Test name
125
+ * @param {Function} testFn Async function to execute
126
+ * @returns {Promise<{ passed: boolean, skipped: boolean }>}
127
+ */
128
+ async function runTest(log, name, testFn) {
129
+ const start = Date.now();
130
+ try {
131
+ const detail = await testFn();
132
+ const ms = Date.now() - start;
133
+ const skipped = typeof detail === 'string' && detail.startsWith('SKIP:');
134
+ if (skipped) {
135
+ log(` ${yellow('SKIP')} ${name} ${dim(`${ms}ms`)} ${dim(detail.slice(6))}`);
136
+ return { passed: true, skipped: true };
137
+ }
138
+ log(` ${green('PASS')} ${name} ${dim(`${ms}ms`)} ${dim(detail || '')}`);
139
+ return { passed: true, skipped: false };
140
+ } catch (err) {
141
+ const ms = Date.now() - start;
142
+ const msg = err instanceof ConfluenceError ? err.message : (err.message || String(err));
143
+ log(` ${red('FAIL')} ${name} ${dim(`${ms}ms`)} ${red(msg)}`);
144
+ return { passed: false, skipped: false };
145
+ }
146
+ }
147
+
148
+ /**
149
+ * Simple assertion helper - throws on falsy value.
150
+ */
151
+ function assert(value, message) {
152
+ if (!value) {
153
+ throw new Error(`Assertion failed: ${message}`);
154
+ }
155
+ }
@@ -0,0 +1,103 @@
1
+ // tree.mjs - Show page tree: ancestors (breadcrumb) + children.
2
+ //
3
+ // Usage:
4
+ // confluence tree <ID>
5
+ //
6
+ // Uses v1 endpoints for ancestor/child data.
7
+
8
+ import { createClient, ConfluenceError } from '../client.mjs';
9
+ import { bold, dim, cyan } from '../format.mjs';
10
+
11
+ const HELP = `confluence tree - Show page tree
12
+
13
+ Usage:
14
+ confluence tree <ID>
15
+
16
+ Arguments:
17
+ ID Page ID (numeric)
18
+
19
+ Options:
20
+ --help, -h Show this help
21
+
22
+ Shows the page's ancestors (breadcrumb path from root) and immediate children.
23
+
24
+ Examples:
25
+ confluence tree 12345
26
+ `;
27
+
28
+ /**
29
+ * Run the tree command.
30
+ *
31
+ * @param {string[]} argv Remaining args after 'tree'
32
+ * @param {object} [deps] Dependency overrides
33
+ * @returns {Promise<number>} Exit code
34
+ */
35
+ export async function run(argv = [], deps = {}) {
36
+ const log = deps.log || console.log;
37
+ const logError = deps.logError || console.error;
38
+ const { parseArgs } = await import('../cli.mjs');
39
+ const args = parseArgs(argv);
40
+
41
+ if (args.help || args.h) {
42
+ log(HELP);
43
+ return 0;
44
+ }
45
+
46
+ const pageId = args._[0];
47
+ if (!pageId) {
48
+ logError('Usage: confluence tree <ID>');
49
+ return 1;
50
+ }
51
+
52
+ const client = deps.client || createClient();
53
+
54
+ try {
55
+ // Fetch the page with ancestors via v1
56
+ const { data: page } = await client.get(
57
+ `/wiki/rest/api/content/${encodeURIComponent(pageId)}?expand=ancestors,children.page`
58
+ );
59
+
60
+ const title = page.title || '(untitled)';
61
+
62
+ // Print ancestors (breadcrumb)
63
+ const ancestors = page.ancestors || [];
64
+ log(bold('Ancestors (breadcrumb):'));
65
+ if (ancestors.length === 0) {
66
+ log(dim(' (root page - no ancestors)'));
67
+ } else {
68
+ for (let i = 0; i < ancestors.length; i++) {
69
+ const indent = ' '.repeat(i + 1);
70
+ log(`${indent}${dim(ancestors[i].id)} ${ancestors[i].title}`);
71
+ }
72
+ }
73
+
74
+ // Current page
75
+ const indent = ' '.repeat(ancestors.length + 1);
76
+ log(`${indent}${cyan(bold(`${page.id} ${title}`))} ${dim('(current)')}`);
77
+
78
+ // Children
79
+ const children = page.children?.page?.results || [];
80
+ log('');
81
+ log(bold('Children:'));
82
+ if (children.length === 0) {
83
+ log(dim(' (no children)'));
84
+ } else {
85
+ const childIndent = ' '.repeat(ancestors.length + 2);
86
+ for (const child of children) {
87
+ log(`${childIndent}${dim(child.id)} ${child.title}`);
88
+ }
89
+ const size = page.children?.page?.size;
90
+ if (size && size > children.length) {
91
+ log(dim(` ... and ${size - children.length} more`));
92
+ }
93
+ }
94
+
95
+ return 0;
96
+ } catch (err) {
97
+ if (err instanceof ConfluenceError) {
98
+ logError(`Failed to get page tree: ${err.message}`);
99
+ return err.exitCode;
100
+ }
101
+ throw err;
102
+ }
103
+ }
@@ -0,0 +1,134 @@
1
+ // update.mjs - Update a Confluence page.
2
+ //
3
+ // Usage:
4
+ // confluence update <ID> [--from-file FILE.md] [-t TITLE] [-b BODY] [-m MESSAGE]
5
+ //
6
+ // Auto-fetches current version and increments.
7
+ // If --from-file, reads markdown and converts to storage format.
8
+ // -m sets version message.
9
+
10
+ import { createClient, ConfluenceError } from '../client.mjs';
11
+ import { markdownToStorage, textToStorage } from '../storage.mjs';
12
+ import { bold, green } from '../format.mjs';
13
+ import { readFileSync } from 'node:fs';
14
+
15
+ const HELP = `confluence update - Update a page
16
+
17
+ Usage:
18
+ confluence update <ID> [--from-file FILE.md] [-t TITLE] [-b BODY] [-m MESSAGE]
19
+
20
+ Arguments:
21
+ ID Page ID (numeric)
22
+
23
+ Options:
24
+ --from-file FILE Read new markdown content from file
25
+ -t TITLE New page title
26
+ -b BODY New body text (converted to storage format)
27
+ -m MESSAGE Version message (appears in page history)
28
+ --help, -h Show this help
29
+
30
+ The current page version is auto-fetched and incremented. At least one of
31
+ --from-file, -t, or -b must be provided.
32
+
33
+ Examples:
34
+ confluence update 12345 --from-file ./docs/api.md -m "Updated API docs"
35
+ confluence update 12345 -t "New Title"
36
+ confluence update 12345 -b "Updated body content." -m "Quick fix"
37
+ `;
38
+
39
+ /**
40
+ * Run the update command.
41
+ *
42
+ * @param {string[]} argv Remaining args after 'update'
43
+ * @param {object} [deps] Dependency overrides
44
+ * @returns {Promise<number>} Exit code
45
+ */
46
+ export async function run(argv = [], deps = {}) {
47
+ const log = deps.log || console.log;
48
+ const logError = deps.logError || console.error;
49
+ const { parseArgs } = await import('../cli.mjs');
50
+ const args = parseArgs(argv);
51
+
52
+ if (args.help || args.h) {
53
+ log(HELP);
54
+ return 0;
55
+ }
56
+
57
+ const pageId = args._[0];
58
+ if (!pageId) {
59
+ logError('Usage: confluence update <ID> [--from-file FILE.md] [-t TITLE] [-b BODY] [-m MSG]');
60
+ return 1;
61
+ }
62
+
63
+ const fromFile = args['from-file'] || null;
64
+ const title = args.t || args.title || null;
65
+ const bodyText = args.b || args.body || null;
66
+ const message = args.m || args.message || null;
67
+
68
+ if (!fromFile && !title && !bodyText) {
69
+ logError('No updates specified. Provide at least one of: --from-file, -t, -b');
70
+ return 1;
71
+ }
72
+
73
+ const client = deps.client || createClient();
74
+
75
+ try {
76
+ // Fetch current page to get version number and title
77
+ const { data: current } = await client.get(
78
+ `/wiki/api/v2/pages/${encodeURIComponent(pageId)}?body-format=storage`
79
+ );
80
+
81
+ const currentVersion = current.version?.number || 1;
82
+ const currentTitle = current.title || '';
83
+
84
+ // Build update payload
85
+ const payload = {
86
+ id: pageId,
87
+ status: 'current',
88
+ title: title || currentTitle,
89
+ version: {
90
+ number: currentVersion + 1,
91
+ },
92
+ };
93
+
94
+ if (message) {
95
+ payload.version.message = message;
96
+ }
97
+
98
+ // Build body if provided
99
+ if (fromFile) {
100
+ const readFn = deps.readFileSync || readFileSync;
101
+ const md = readFn(fromFile, 'utf-8');
102
+ payload.body = {
103
+ representation: 'storage',
104
+ value: markdownToStorage(md),
105
+ };
106
+ } else if (bodyText) {
107
+ payload.body = {
108
+ representation: 'storage',
109
+ value: textToStorage(bodyText),
110
+ };
111
+ } else {
112
+ // Title-only update - keep existing body
113
+ payload.body = {
114
+ representation: 'storage',
115
+ value: current.body?.storage?.value || '',
116
+ };
117
+ }
118
+
119
+ await client.put(`/wiki/api/v2/pages/${encodeURIComponent(pageId)}`, payload);
120
+
121
+ log(green(`${bold(payload.title)} updated to version ${currentVersion + 1}.`));
122
+ if (message) {
123
+ log(` Message: ${message}`);
124
+ }
125
+
126
+ return 0;
127
+ } catch (err) {
128
+ if (err instanceof ConfluenceError) {
129
+ logError(`Failed to update page: ${err.message}`);
130
+ return err.exitCode;
131
+ }
132
+ throw err;
133
+ }
134
+ }