@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.
- package/README.md +12 -2
- package/bin/confluence +9 -0
- package/bin/confluence-fetch +9 -0
- package/bin/jira +9 -0
- package/dist/engineering-pa-core/knowledge/content-voice/banned-ai-tells-taxonomy.md +1 -1
- package/dist/engineering-pa-core/knowledge/knowledge-memory/staging-and-observations.md +3 -3
- package/dist/engineering-pa-core/knowledge/platform-tooling/workspace-extensions.md +62 -0
- package/dist/engineering-pa-core-extensions-atlassian/README.md +81 -16
- package/dist/engineering-pa-core-extensions-atlassian/bin/confluence +2 -0
- package/dist/engineering-pa-core-extensions-atlassian/bin/confluence-fetch +2 -0
- package/dist/engineering-pa-core-extensions-atlassian/bin/jira +2 -0
- package/dist/engineering-pa-core-extensions-atlassian/config/jira-workflow-chains.default.json +4 -0
- package/dist/engineering-pa-core-extensions-atlassian/index.mjs +17 -6
- package/dist/engineering-pa-core-extensions-atlassian/package.json +11 -4
- package/dist/engineering-pa-core-extensions-atlassian/src/common/invalidate-client.mjs +137 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/common/pa-name.mjs +58 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/common/secrets.mjs +158 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/confluence/cli.mjs +165 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/confluence/client.mjs +294 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/confluence/commands/auth.mjs +59 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/confluence/commands/comment.mjs +174 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/confluence/commands/create.mjs +136 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/confluence/commands/label.mjs +98 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/confluence/commands/mine.mjs +105 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/confluence/commands/mirror-folder.mjs +487 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/confluence/commands/mirror-page.mjs +351 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/confluence/commands/page.mjs +127 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/confluence/commands/pages.mjs +107 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/confluence/commands/recent.mjs +107 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/confluence/commands/search.mjs +123 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/confluence/commands/spaces.mjs +82 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/confluence/commands/test.mjs +155 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/confluence/commands/tree.mjs +103 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/confluence/commands/update.mjs +134 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/confluence/format.mjs +260 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/confluence/mirror.mjs +353 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/confluence/storage.mjs +880 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/confluence-fetch/cli.mjs +313 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/confluence-fetch/fetch.mjs +327 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/jira/adf.mjs +388 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/jira/bug-fields.mjs +294 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/jira/cli.mjs +197 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/jira/client.mjs +350 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/advance.mjs +486 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/attach.mjs +129 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/auth.mjs +53 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/boards.mjs +88 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/bulk-transition.mjs +223 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/cancel.mjs +135 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/comment.mjs +169 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/create.mjs +376 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/issue.mjs +105 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/link.mjs +204 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/mine.mjs +97 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/projects.mjs +91 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/search.mjs +80 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/sprint.mjs +111 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/sync-statuses.mjs +313 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/test.mjs +142 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/transition.mjs +197 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/update.mjs +284 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/jira/commands/worklog.mjs +380 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/jira/format.mjs +267 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/jira/invalidate.mjs +72 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/jira/metadata.mjs +225 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/jira/required-fields.mjs +334 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/jira/workflow-chains.mjs +405 -0
- package/dist/engineering-pa-core-extensions-atlassian/src/jira/workflow-map.mjs +107 -0
- package/dist/engineering-pa-scaffold/README.md +32 -1
- package/dist/engineering-pa-scaffold/src/cli.mjs +83 -6
- package/dist/engineering-pa-scaffold/src/commands/init-workspace-extension.mjs +234 -0
- package/dist/engineering-pa-scaffold/src/commands/init.mjs +6 -5
- package/dist/engineering-pa-scaffold/src/generator.mjs +240 -4
- package/dist/engineering-pa-scaffold/templates/CLAUDE.md.template +7 -7
- package/dist/engineering-pa-scaffold/templates/operator-README.md.template +4 -4
- package/dist/engineering-pa-scaffold/templates/root-CLAUDE.md.template +13 -0
- package/dist/engineering-pa-types-engineering/index.mjs +14 -6
- package/dist/engineering-pa-types-engineering/knowledge/rcf-lite/create-a-small-app.md +81 -0
- package/dist/engineering-pa-types-engineering/knowledge/rcf-lite/for-agents.md +25 -0
- package/dist/engineering-pa-types-engineering/knowledge/rcf-lite/index.md +34 -0
- package/dist/engineering-pa-types-engineering/knowledge/rcf-lite/reference.md +16 -0
- package/dist/engineering-pa-types-engineering/knowledge/rcf-lite/wire-your-agent.md +17 -0
- package/dist/engineering-pa-types-engineering/package.json +2 -2
- package/dist/engineering-pa-types-engineering/roles/rcf-lite-engineer/definition.md +183 -0
- package/dist/engineering-pa-workspace/app/home.html +7 -7
- package/dist/engineering-pa-workspace/app/onboarding.html +4 -4
- package/dist/engineering-pa-workspace/src/api.mjs +1 -1
- package/dist/engineering-pa-workspace/src/server.mjs +1 -1
- package/docs/installing.md +78 -0
- package/docs/onboarding-solo.md +162 -0
- package/docs/what-it-can-do.md +85 -0
- 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
|
+
}
|