@stravica-ai/digital-operator-pa 0.1.40 → 0.1.42
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 +5 -0
- package/bin/confluence +9 -0
- package/bin/confluence-fetch +9 -0
- package/bin/jira +9 -0
- package/dist/engineering-pa-cli/src/pa/cli.mjs +25 -1
- package/dist/engineering-pa-cli/src/secrets/commands/set.mjs +36 -1
- package/dist/engineering-pa-core/knowledge/platform-tooling/workspace-extensions.md +62 -0
- package/dist/engineering-pa-core/src/distill/run.mjs +45 -2
- 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 +64 -8
- package/dist/engineering-pa-scaffold/src/generator.mjs +215 -3
- package/dist/engineering-pa-scaffold/templates/CLAUDE.md.template +5 -5
- 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/docs/onboarding-solo.md +73 -8
- package/docs/what-it-can-do.md +85 -0
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -20,6 +20,11 @@ New to the platform and setting up a single-person PA on one laptop? Follow
|
|
|
20
20
|
ten-minute walkthrough from an empty directory to a working PA under your
|
|
21
21
|
own Claude Code subscription.
|
|
22
22
|
|
|
23
|
+
Want a plain-language tour of what the PA does before you install it? Read
|
|
24
|
+
[`./docs/what-it-can-do.md`](./docs/what-it-can-do.md) — a day-to-day view
|
|
25
|
+
of the capabilities, how it learns you, and what is (and is not) in this
|
|
26
|
+
release.
|
|
27
|
+
|
|
23
28
|
## Bins
|
|
24
29
|
|
|
25
30
|
The wrapper exposes the platform's operator-facing CLIs:
|
package/bin/confluence
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// confluence - Confluence Cloud CLI (v1 CQL + v2 CRUD). Shares JIRA_* creds.
|
|
3
|
+
// Bundled entry: delegates to the copied @stravica-ai/engineering-pa-core-extensions-atlassian sources.
|
|
4
|
+
import('../dist/engineering-pa-core-extensions-atlassian/src/confluence/cli.mjs').then((m) => {
|
|
5
|
+
return m.main(process.argv.slice(2));
|
|
6
|
+
}).catch((err) => {
|
|
7
|
+
process.stderr.write(`[confluence] ${err?.message ?? err}\n`);
|
|
8
|
+
process.exit(typeof err?.exitCode === 'number' ? err.exitCode : 2);
|
|
9
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// confluence-fetch - search + fetch + save Confluence pages into operator/notes/.
|
|
3
|
+
// Bundled entry: delegates to the copied @stravica-ai/engineering-pa-core-extensions-atlassian sources.
|
|
4
|
+
import('../dist/engineering-pa-core-extensions-atlassian/src/confluence-fetch/cli.mjs').then((m) => {
|
|
5
|
+
return m.main(process.argv.slice(2));
|
|
6
|
+
}).catch((err) => {
|
|
7
|
+
process.stderr.write(`[confluence-fetch] ${err?.message ?? err}\n`);
|
|
8
|
+
process.exit(typeof err?.exitCode === 'number' ? err.exitCode : 2);
|
|
9
|
+
});
|
package/bin/jira
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// jira - Jira REST v3 CLI. Auth reads env vars first, else `secrets get JIRA_*`.
|
|
3
|
+
// Bundled entry: delegates to the copied @stravica-ai/engineering-pa-core-extensions-atlassian sources.
|
|
4
|
+
import('../dist/engineering-pa-core-extensions-atlassian/src/jira/cli.mjs').then((m) => {
|
|
5
|
+
return m.main(process.argv.slice(2));
|
|
6
|
+
}).catch((err) => {
|
|
7
|
+
process.stderr.write(`[jira] ${err?.message ?? err}\n`);
|
|
8
|
+
process.exit(typeof err?.exitCode === 'number' ? err.exitCode : 2);
|
|
9
|
+
});
|
|
@@ -205,6 +205,13 @@ function resolveWorkspacePort(repoRoot, opts = {}) {
|
|
|
205
205
|
return 4200;
|
|
206
206
|
}
|
|
207
207
|
|
|
208
|
+
/**
|
|
209
|
+
* The `server` field the workspace's own /__api/identity endpoint returns
|
|
210
|
+
* (see packages/workspace/src/api.mjs). Any other body on the port means
|
|
211
|
+
* the port is bound by an unrelated process, not our workspace.
|
|
212
|
+
*/
|
|
213
|
+
const WORKSPACE_IDENTITY_SERVER = 'digital-operator-pa-workspace';
|
|
214
|
+
|
|
208
215
|
async function probeWorkspace(url, fetchImpl) {
|
|
209
216
|
const fetchFn = fetchImpl ?? globalThis.fetch;
|
|
210
217
|
if (typeof fetchFn !== 'function') {
|
|
@@ -214,8 +221,25 @@ async function probeWorkspace(url, fetchImpl) {
|
|
|
214
221
|
const timer = setTimeout(() => controller.abort(), 1500);
|
|
215
222
|
try {
|
|
216
223
|
const res = await fetchFn(url, { signal: controller.signal });
|
|
224
|
+
// Any HTTP response has to be positively identified as OUR workspace
|
|
225
|
+
// before the probe reports "reachable". WSD-laptop cold install of
|
|
226
|
+
// 0.1.41 (2026-09-16): a docker container bound to :4200 answered
|
|
227
|
+
// /__api/identity with HTTP 404 and `pa status` reported the
|
|
228
|
+
// workspace "reachable (HTTP 404)". Identity-gate the probe so a
|
|
229
|
+
// foreign process on the port surfaces as "in use by another
|
|
230
|
+
// process" instead of a false green.
|
|
217
231
|
if (res.status >= 200 && res.status < 500) {
|
|
218
|
-
|
|
232
|
+
let body = null;
|
|
233
|
+
try {
|
|
234
|
+
body = await res.json();
|
|
235
|
+
} catch { /* not JSON — falls through to the identity-mismatch branch */ }
|
|
236
|
+
if (body && body.server === WORKSPACE_IDENTITY_SERVER) {
|
|
237
|
+
return { status: 'reachable', detail: `HTTP ${res.status}` };
|
|
238
|
+
}
|
|
239
|
+
return {
|
|
240
|
+
status: 'in use by another process',
|
|
241
|
+
detail: `HTTP ${res.status}, /__api/identity did not identify as ${WORKSPACE_IDENTITY_SERVER}`,
|
|
242
|
+
};
|
|
219
243
|
}
|
|
220
244
|
return { status: 'error', detail: `HTTP ${res.status}` };
|
|
221
245
|
} catch (e) {
|
|
@@ -35,6 +35,7 @@ import {
|
|
|
35
35
|
sopsEncryptScoped,
|
|
36
36
|
VaultError,
|
|
37
37
|
} from '../vault.mjs';
|
|
38
|
+
import { runInit } from './init.mjs';
|
|
38
39
|
|
|
39
40
|
// --canonical-clone / --no-canonical may appear anywhere in argv, matching
|
|
40
41
|
// the --scope/--file convention in cli.mjs. Local to `set` (not global
|
|
@@ -91,7 +92,41 @@ export async function runSet(args, overrides = {}) {
|
|
|
91
92
|
throw new VaultError('missing dependencies', 1);
|
|
92
93
|
}
|
|
93
94
|
|
|
94
|
-
|
|
95
|
+
let vaultState = checkVault(paths);
|
|
96
|
+
// Self-init on `secrets set` when the legacy vault has not been
|
|
97
|
+
// initialised on this host yet. WSD-laptop cold install of 0.1.41
|
|
98
|
+
// (2026-09-16): the onboarding guide's Atlassian setup asks the
|
|
99
|
+
// colleague to run three `secrets set` commands, and the first one
|
|
100
|
+
// died with `Error: No recipients file at .vault/recipients.txt —
|
|
101
|
+
// Run 'secrets init' to set up the vault.` The guide's two-step
|
|
102
|
+
// Atlassian setup never named that mandatory extra step. `set` now
|
|
103
|
+
// runs the same `runInit()` code path in-process before the first
|
|
104
|
+
// write when the vault is uninitialised, emitting one stderr notice
|
|
105
|
+
// so the operator can see what happened.
|
|
106
|
+
//
|
|
107
|
+
// The self-init fires only when it is SAFE — the legacy (non-scoped)
|
|
108
|
+
// vault, no encrypted store already on disk (otherwise a fresh key
|
|
109
|
+
// would orphan the existing contents; that failure mode still surfaces
|
|
110
|
+
// as `missing-key` exit 2 with the "restore your key from backup"
|
|
111
|
+
// guidance). `missing-recipients` on a solo host is always safe (a
|
|
112
|
+
// present store with no recipients file would already have failed
|
|
113
|
+
// decryption elsewhere; the case the WSD laptop hit was "no vault at
|
|
114
|
+
// all yet"). `missing-key` on a solo host is also safe when the
|
|
115
|
+
// encrypted store is absent — pure first-run state. Scoped vaults
|
|
116
|
+
// (`paths.scoped`) are provisioned by scope-registration tooling and
|
|
117
|
+
// are never self-inited here; a `secrets set --scope <name>` against
|
|
118
|
+
// a missing scope surfaces the not-provisioned error unchanged.
|
|
119
|
+
const canSelfInit = !vaultState.ok
|
|
120
|
+
&& !paths.scoped
|
|
121
|
+
&& (vaultState.reason === 'missing-recipients' || vaultState.reason === 'missing-key')
|
|
122
|
+
&& !secretsFileExists(paths);
|
|
123
|
+
if (canSelfInit) {
|
|
124
|
+
console.error(
|
|
125
|
+
`notice: no vault at ${paths.vaultDir}: self-initialising before first write (equivalent to running \`secrets init\`).`,
|
|
126
|
+
);
|
|
127
|
+
await runInit({ paths });
|
|
128
|
+
vaultState = checkVault(paths);
|
|
129
|
+
}
|
|
95
130
|
if (!vaultState.ok) {
|
|
96
131
|
console.error(vaultState.message);
|
|
97
132
|
throw new VaultError(vaultState.reason || 'vault not initialised', vaultState.exitCode ?? 1);
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
scaffold: workspace-extensions.md
|
|
3
|
+
bucket: platform-tooling (Bucket 7)
|
|
4
|
+
audience: the PA, deciding when to propose that the operator build a workspace page.
|
|
5
|
+
what belongs here:
|
|
6
|
+
- placement rule (common-use CLIs in core, personal pages as extensions).
|
|
7
|
+
- triggers the PA watches for.
|
|
8
|
+
- what to offer, and what NOT to prompt for.
|
|
9
|
+
- how to prompt.
|
|
10
|
+
- cross-link to the workspace README for the mechanism.
|
|
11
|
+
-->
|
|
12
|
+
|
|
13
|
+
# Workspace extensions: when to propose one
|
|
14
|
+
|
|
15
|
+
## Placement rule
|
|
16
|
+
|
|
17
|
+
Common-use CLIs live in core. Personal workspace pages are a more personal expression of how the operator works, so they belong in an extension. The PA carries the knowledge that decides when to prompt the operator to build one.
|
|
18
|
+
|
|
19
|
+
The mechanism itself is already live. Static page overlay under `operator/extensions/workspace/<name>.html`, optional API routes via `operator/extensions/workspace/api.mjs` (exporting `register(addRoute)`), nav registration via `operator/extensions/workspace/nav.json` (or team-scoped `nav.<team>.json` fragments). Full detail: `@stravica-ai/engineering-pa-workspace/README.md` (extension mechanism section).
|
|
20
|
+
|
|
21
|
+
## Triggers
|
|
22
|
+
|
|
23
|
+
Three shapes of operator behaviour that mean a workspace page would pay for itself.
|
|
24
|
+
|
|
25
|
+
1. **Repeated manual view.** The operator opens the same files, runs the same query, or screenshots the same panel more than twice in a week. The manual view is already a page; it just does not exist yet.
|
|
26
|
+
2. **Explicit ask for a dashboard or "one page to see X".** Any request framed as "I want to see all my X in one place", "give me a page for Y", "I want to check Z at a glance". The operator has done the design work already.
|
|
27
|
+
3. **An operator-installed tool with a natural page.** A Jira CLI plus a "my Jira today" habit is the archetype. Any tool the operator installed and now reaches for daily has a page shape latent in it.
|
|
28
|
+
|
|
29
|
+
## What to offer
|
|
30
|
+
|
|
31
|
+
Match the surface to the shape.
|
|
32
|
+
|
|
33
|
+
- **Read-only view of files or state on disk.** Static page overlay. HTML file at `operator/extensions/workspace/<name>.html` served in place of a core same-named page, or as a new page at `/<name>.html`.
|
|
34
|
+
- **Anything that needs server access or a write.** Static page plus an API route in `operator/extensions/workspace/api.mjs`. The route hangs off `register(addRoute)` and lands under `/__api/<name>/<verb>`.
|
|
35
|
+
- **Discoverability in the sidebar.** Add a `nav.json` entry naming the page. If the operator runs multiple teams on the same box, use `nav.<team>.json` fragments so each team's entries survive the library merge.
|
|
36
|
+
|
|
37
|
+
## What NOT to prompt for
|
|
38
|
+
|
|
39
|
+
Not every operator habit is a workspace page.
|
|
40
|
+
|
|
41
|
+
- **One-shot views.** A page for a single question the operator asked once is over-engineering. The answer belongs in the chat, or in a `viewer.html?file=...` link.
|
|
42
|
+
- **Throwaway reports.** If the output has a shelf-life of one day, a workspace page is the wrong home. Stage the file, link it, move on.
|
|
43
|
+
- **Anything that is a workspace page and a knowledge doc and a scheduled task rolled into one.** That is a tool. Propose `tool-developer` under Bucket 7 rule 9, not a workspace extension.
|
|
44
|
+
|
|
45
|
+
## How to prompt
|
|
46
|
+
|
|
47
|
+
One sentence. Name the page, the source of data, and the interaction shape.
|
|
48
|
+
|
|
49
|
+
> "Want me to build a workspace page for X? Reads from Y, refresh on nav-load, no writes."
|
|
50
|
+
|
|
51
|
+
Get an explicit yes before doing anything durable. On approval, either:
|
|
52
|
+
|
|
53
|
+
- Invoke `pa-init workspace-extension <name>` for the file skeleton plus nav entry (add `--with-api` if the page needs a server route), then dispatch `tool-developer` for the real page content and any handler logic, OR
|
|
54
|
+
- Skip the scaffold verb and dispatch `tool-developer` directly if the shape is unusual enough that the skeleton would just be deleted.
|
|
55
|
+
|
|
56
|
+
The scaffold verb is a thirty-second cost saver; it does not replace the design conversation.
|
|
57
|
+
|
|
58
|
+
## Related
|
|
59
|
+
|
|
60
|
+
- CLAUDE.md.template Bucket 7 rule 9 (proactive tool proposal): the parent rule; workspace pages count as a tool for that rule.
|
|
61
|
+
- `@stravica-ai/engineering-pa-workspace/README.md`: the extension mechanism, page overlay, API-route registration, nav merge, team scoping.
|
|
62
|
+
- `packages/core/knowledge/platform-tooling/workspace-server.md`: workspace server operations (start, stop, viewer.html).
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
// omitted on `ok`. `log` is null on exits that happened before the
|
|
33
33
|
// per-run log file was created.
|
|
34
34
|
|
|
35
|
-
import { appendFile, readFile } from 'node:fs/promises';
|
|
35
|
+
import { appendFile, readFile, unlink } from 'node:fs/promises';
|
|
36
36
|
import { basename, join } from 'node:path';
|
|
37
37
|
|
|
38
38
|
import { resolvePrereqs } from './prereqs.mjs';
|
|
@@ -211,7 +211,21 @@ export async function runDistill({
|
|
|
211
211
|
log(`distillation OK (${durationSec}s) — log: ${runLogPath}`, { stderr });
|
|
212
212
|
log('changes (if any) will be committed by the next sync run', { stderr });
|
|
213
213
|
|
|
214
|
-
// Step
|
|
214
|
+
// Step 4a — invalidate the retrieval artefacts so the next
|
|
215
|
+
// `pa knowledge` call rebuilds them and sees any doc the librarian just
|
|
216
|
+
// promoted. Macbook re-proof of 0.1.41 (2026-09-16): on a solo install
|
|
217
|
+
// there is no `library-sync-cycle:graph-rebuild` leg running behind the
|
|
218
|
+
// scenes, so a distilled knowledge doc stayed invisible to
|
|
219
|
+
// `pa knowledge search` until `.graph.json` was deleted by hand. Delete
|
|
220
|
+
// is safer than in-process rebuild here: the retrieval loader in
|
|
221
|
+
// `packages/cli/src/knowledge/graph.mjs` already self-heals on a
|
|
222
|
+
// missing artefact (`loadGraph:on-demand`), and a full-mode host has
|
|
223
|
+
// its scheduled graph-rebuild leg rebuild both files on the next cycle
|
|
224
|
+
// regardless. Both are untracked per the 2026-07-29 per-device ruling,
|
|
225
|
+
// so a deletion at commit time is invisible to git.
|
|
226
|
+
await invalidateRetrievalArtefacts(repoRoot, stderr);
|
|
227
|
+
|
|
228
|
+
// Step 4b — append success audit. Sync will pick up any file changes
|
|
215
229
|
// the librarian produced on its next 30-minute tick.
|
|
216
230
|
await writeAudit(runsLog, {
|
|
217
231
|
ts: tsIso,
|
|
@@ -224,6 +238,35 @@ export async function runDistill({
|
|
|
224
238
|
return 0;
|
|
225
239
|
}
|
|
226
240
|
|
|
241
|
+
/**
|
|
242
|
+
* Delete `operator/knowledge/.graph.json` and `.backlinks.json` under
|
|
243
|
+
* `root` if they exist. Missing files are a no-op; any other error is
|
|
244
|
+
* swallowed with a stderr note so a stale artefact never masks an
|
|
245
|
+
* otherwise-successful distillation exit code. Exported for tests.
|
|
246
|
+
*/
|
|
247
|
+
export async function invalidateRetrievalArtefacts(root, stderr = process.stderr) {
|
|
248
|
+
const targets = [
|
|
249
|
+
join(root, 'operator', 'knowledge', '.graph.json'),
|
|
250
|
+
join(root, 'operator', 'knowledge', '.backlinks.json'),
|
|
251
|
+
];
|
|
252
|
+
const removed = [];
|
|
253
|
+
for (const path of targets) {
|
|
254
|
+
try {
|
|
255
|
+
await unlink(path);
|
|
256
|
+
removed.push(basename(path));
|
|
257
|
+
} catch (e) {
|
|
258
|
+
if (e && e.code === 'ENOENT') continue;
|
|
259
|
+
log(`warning: failed to invalidate ${basename(path)}: ${e.message}`, { stderr });
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
if (removed.length > 0) {
|
|
263
|
+
log(
|
|
264
|
+
`invalidated retrieval artefacts (${removed.join(', ')}); pa knowledge rebuilds on demand`,
|
|
265
|
+
{ stderr },
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
227
270
|
function isoCompactForSession(d) {
|
|
228
271
|
// Matches the historical `$(date -u +%Y%m%d-%H%M)` shape used pre-Node.
|
|
229
272
|
const pad = (n) => String(n).padStart(2, '0');
|
|
@@ -1,31 +1,96 @@
|
|
|
1
1
|
# @stravica-ai/engineering-pa-core-extensions-atlassian
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Atlassian (Jira, Confluence) CLIs and library surface, promoted into core
|
|
4
|
+
for the alpha solo scaffold. Common-use tools ship with the platform; the
|
|
5
|
+
personal workspace pages colleagues build on top ship as extensions.
|
|
4
6
|
|
|
5
|
-
##
|
|
7
|
+
## Bins
|
|
6
8
|
|
|
7
|
-
|
|
9
|
+
Wrapped by `@stravica-ai/digital-operator-pa` and installed on `PATH` when
|
|
10
|
+
that package is installed:
|
|
8
11
|
|
|
9
|
-
|
|
12
|
+
- `jira` - Jira REST v3 CLI. Verbs: `auth`, `mine`, `search`, `issue`,
|
|
13
|
+
`create`, `update`, `attach`, `transition`, `advance`, `cancel`,
|
|
14
|
+
`comment`, `worklog`, `projects`, `boards`, `sprint`, `link`,
|
|
15
|
+
`sync-statuses`, `bulk-transition`, `test`.
|
|
16
|
+
- `confluence` - Confluence Cloud CLI (v1 CQL search, v2 page CRUD).
|
|
17
|
+
Verbs: `auth`, `spaces`, `pages`, `page`, `search`, `create`, `update`,
|
|
18
|
+
`comment`, `label`, `tree`, `recent`, `mine`, `mirror-folder`,
|
|
19
|
+
`mirror-page`, `test`.
|
|
20
|
+
- `confluence-fetch` - convenience wrapper: search + fetch + save a page
|
|
21
|
+
to `operator/notes/` and open the workspace viewer.
|
|
10
22
|
|
|
11
|
-
|
|
23
|
+
Run `jira --help`, `confluence --help`, `confluence-fetch --help` for the
|
|
24
|
+
full verb list; each verb has its own `--help`.
|
|
12
25
|
|
|
13
|
-
##
|
|
26
|
+
## Auth
|
|
27
|
+
|
|
28
|
+
Both CLIs read three credentials from the environment first, else from the
|
|
29
|
+
platform `secrets` bin. The keys are shared because Jira and Confluence
|
|
30
|
+
target the same Atlassian site:
|
|
31
|
+
|
|
32
|
+
- `JIRA_DOMAIN` - subdomain of `atlassian.net` (`foo` for `foo.atlassian.net`)
|
|
33
|
+
- `JIRA_EMAIL` - your Atlassian account email
|
|
34
|
+
- `JIRA_API_TOKEN` - a token from `https://id.atlassian.com/manage-profile/security/api-tokens`
|
|
35
|
+
|
|
36
|
+
Two-step colleague setup, after installing `@stravica-ai/digital-operator-pa`:
|
|
14
37
|
|
|
15
38
|
```
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
39
|
+
secrets set JIRA_DOMAIN yoursite
|
|
40
|
+
secrets set JIRA_EMAIL you@example.com
|
|
41
|
+
secrets set JIRA_API_TOKEN <paste-token>
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Resolution order for each key:
|
|
45
|
+
|
|
46
|
+
1. Environment variable of the same name (`JIRA_DOMAIN`, ...).
|
|
47
|
+
2. `secrets get <prefix>JIRA_DOMAIN` (etc.), where `<prefix>` comes from
|
|
48
|
+
`ATLASSIAN_SECRETS_PREFIX` (e.g. an org slug like `acme/` picks the `acme` vault scope in
|
|
49
|
+
an operator estate; empty by default so a solo colleague's single scope
|
|
50
|
+
just works).
|
|
51
|
+
|
|
52
|
+
The tool never prints or logs the values; child-process reads use
|
|
53
|
+
`stdio: ['ignore', 'pipe', 'pipe']`.
|
|
54
|
+
|
|
55
|
+
## Configuration
|
|
56
|
+
|
|
57
|
+
- `config/jira-workflow-chains.default.json` - shipped empty. `jira advance
|
|
58
|
+
<KEY> <STATUS>` walks a per-site chain of statuses; override by writing
|
|
59
|
+
`operator/config/jira-workflow-chains.json` in your workspace root.
|
|
60
|
+
- `operator/config/jira-status-mappings.json` - per-project status mapping
|
|
61
|
+
used by `jira sync-statuses`; not shipped by default.
|
|
62
|
+
- `JIRA_DEFAULT_PROJECT` (optional, env or vault key of the same name) -
|
|
63
|
+
when set, `jira mine` narrows to that project unless `--project KEY`
|
|
64
|
+
or `--all` is passed. With nothing configured, `jira mine` returns
|
|
65
|
+
issues across every project the account can see.
|
|
66
|
+
|
|
67
|
+
## Library surface
|
|
68
|
+
|
|
69
|
+
```js
|
|
70
|
+
import {
|
|
71
|
+
createJiraClient,
|
|
72
|
+
createConfluenceClient,
|
|
73
|
+
storageToMarkdown,
|
|
74
|
+
} from '@stravica-ai/digital-operator-pa/dist/engineering-pa-core-extensions-atlassian/index.mjs';
|
|
75
|
+
|
|
76
|
+
const jira = createJiraClient();
|
|
77
|
+
const { data } = await jira.get('/rest/api/3/myself');
|
|
21
78
|
```
|
|
22
79
|
|
|
23
|
-
|
|
80
|
+
For tests, inject a stub credential resolver rather than the vault:
|
|
24
81
|
|
|
25
|
-
```
|
|
26
|
-
|
|
82
|
+
```js
|
|
83
|
+
const client = createJiraClient({
|
|
84
|
+
domain: 'yoursite',
|
|
85
|
+
email: 'you@example.com',
|
|
86
|
+
apiToken: 'test-token',
|
|
87
|
+
fetchFn: mockFetch,
|
|
88
|
+
});
|
|
27
89
|
```
|
|
28
90
|
|
|
29
|
-
##
|
|
91
|
+
## Workspace hook
|
|
30
92
|
|
|
31
|
-
|
|
93
|
+
`jira` writes POST to `http://127.0.0.1:<workspacePort>/__api/jira/invalidate`
|
|
94
|
+
after mutating verbs, so a workspace page can refresh cached data on
|
|
95
|
+
change. The endpoint is a workspace-extension concern; the CLI treats a
|
|
96
|
+
timeout or refused connection as fine (the write succeeded regardless).
|
package/dist/engineering-pa-core-extensions-atlassian/config/jira-workflow-chains.default.json
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_note": "Per-site Jira workflow chains for `jira advance <KEY> <STATUS>`. Ships empty because every Atlassian instance has its own workflows. Override by writing operator/config/jira-workflow-chains.json in your workspace root; the CLI reads that path first and falls back to this shipped default.",
|
|
3
|
+
"chains": []
|
|
4
|
+
}
|
|
@@ -1,9 +1,20 @@
|
|
|
1
|
-
// @stravica-ai/engineering-pa-core-extensions-atlassian
|
|
2
|
-
// integration (Jira/Confluence tooling + workspace pages).
|
|
1
|
+
// @stravica-ai/engineering-pa-core-extensions-atlassian
|
|
3
2
|
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
3
|
+
// Common-use Atlassian CLIs (jira, confluence, confluence-fetch), promoted
|
|
4
|
+
// into core so the alpha solo scaffold ships them out of the box. The bins
|
|
5
|
+
// live under bin/ and are wrapped from @stravica-ai/digital-operator-pa's
|
|
6
|
+
// bin map. Auth reads env vars first, else `secrets get JIRA_*` (see
|
|
7
|
+
// src/common/secrets.mjs).
|
|
8
|
+
|
|
9
|
+
export { createClient as createJiraClient, getCredentials as getJiraCredentials, JiraError } from './src/jira/client.mjs';
|
|
10
|
+
export { createClient as createConfluenceClient, getCredentials as getConfluenceCredentials, ConfluenceError } from './src/confluence/client.mjs';
|
|
11
|
+
export {
|
|
12
|
+
storageToMarkdown,
|
|
13
|
+
markdownToStorage,
|
|
14
|
+
storageToText,
|
|
15
|
+
textToStorage,
|
|
16
|
+
} from './src/confluence/storage.mjs';
|
|
17
|
+
export { loadAtlassianCredentials, resolveSecret, CredentialResolutionError } from './src/common/secrets.mjs';
|
|
7
18
|
|
|
8
19
|
export const name = './';
|
|
9
|
-
export const status = '
|
|
20
|
+
export const status = 'active';
|
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stravica-ai/engineering-pa-core-extensions-atlassian",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Atlassian (Jira
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Atlassian (Jira, Confluence, confluence-fetch) CLIs and library surface. Promoted into core for the alpha solo scaffold.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.mjs",
|
|
7
7
|
"exports": {
|
|
8
|
-
".": "./index.mjs"
|
|
8
|
+
".": "./index.mjs",
|
|
9
|
+
"./jira": "./src/jira/client.mjs",
|
|
10
|
+
"./confluence": "./src/confluence/client.mjs"
|
|
11
|
+
},
|
|
12
|
+
"bin": {
|
|
13
|
+
"jira": "bin/jira",
|
|
14
|
+
"confluence": "bin/confluence",
|
|
15
|
+
"confluence-fetch": "bin/confluence-fetch"
|
|
9
16
|
},
|
|
10
17
|
"private": true,
|
|
11
18
|
"license": "MIT",
|
|
@@ -15,5 +22,5 @@
|
|
|
15
22
|
"scripts": {
|
|
16
23
|
"test": "node --test --test-isolation=none test/*.test.mjs"
|
|
17
24
|
},
|
|
18
|
-
"//": "test pins --test-isolation=none: default process-isolation mode silently undercounts (drops passed tests from totals) when a test awaits async work while stdout is patched by a capture() helper
|
|
25
|
+
"//": "test pins --test-isolation=none: default process-isolation mode silently undercounts (drops passed tests from totals) when a test awaits async work while stdout is patched by a capture() helper - see packages/cli/test/secrets. Counts must never lie."
|
|
19
26
|
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// client.mjs - Shared fail-silent transport for notifying the local
|
|
2
|
+
// workspace server that a CLI tool's backing data changed.
|
|
3
|
+
//
|
|
4
|
+
// Extracted so any Atlassian CLI (jira today; confluence or others later) can
|
|
5
|
+
// fire a lightweight "hey, invalidate your cache" POST at a tool-specific
|
|
6
|
+
// endpoint without re-implementing the fetch/timeout/disable-flag plumbing.
|
|
7
|
+
// Callers bind their own endpoint, body shape, and disable env var (see
|
|
8
|
+
// operator/extensions/tools/jira/src/invalidate.mjs for the reference
|
|
9
|
+
// caller); this module only owns the wire mechanics.
|
|
10
|
+
//
|
|
11
|
+
// Contract (binding on every caller):
|
|
12
|
+
// - Fail-silent. Never throws - always resolves to { ok, error? }. A
|
|
13
|
+
// workspace-notify feature must never block or crash the CLI command
|
|
14
|
+
// whose actual job is mutating the upstream system (Jira, Confluence, ...).
|
|
15
|
+
// - Non-blocking by design. Bounded by timeoutMs (default 500ms) via
|
|
16
|
+
// AbortController; the caller gets an answer promptly either way.
|
|
17
|
+
// - Zero runtime deps. Node 20+ globalThis.fetch + AbortController only.
|
|
18
|
+
//
|
|
19
|
+
// Port resolution: opts.port wins; else the workspacePort from the merged
|
|
20
|
+
// preferences (device-local preferences.local.json over the synced
|
|
21
|
+
// preferences.json); else the platform default (4200) - same convention as
|
|
22
|
+
// operator/extensions/tools/confluence-fetch/src/fetch.mjs.
|
|
23
|
+
//
|
|
24
|
+
// The overlay (preferences.local.json) is the operator's device-scoped
|
|
25
|
+
// escape hatch: gitignored, so a per-device workspacePort never travels
|
|
26
|
+
// through the synced base file. Precedence matches the platform's own
|
|
27
|
+
// reader in engineering-pa-core src/desk/team-scope.mjs mergePreferences().
|
|
28
|
+
|
|
29
|
+
import { resolve } from 'node:path';
|
|
30
|
+
import { readFileSync } from 'node:fs';
|
|
31
|
+
|
|
32
|
+
// Repo root: env override, else cwd. The tool is invoked from a workspace
|
|
33
|
+
// root so cwd is the correct default; keeps preferences.local.json overlay
|
|
34
|
+
// resolution working under a downstream install.
|
|
35
|
+
const REPO_ROOT =
|
|
36
|
+
process.env.PA_REPO_ROOT || process.env.DAVE_ROOT || process.cwd();
|
|
37
|
+
const PREFERENCES_PATH = resolve(REPO_ROOT, 'operator', 'config', 'preferences.json');
|
|
38
|
+
const PREFERENCES_LOCAL_PATH = resolve(REPO_ROOT, 'operator', 'config', 'preferences.local.json');
|
|
39
|
+
|
|
40
|
+
const DEFAULT_TIMEOUT_MS = 500;
|
|
41
|
+
const PLATFORM_DEFAULT_PORT = 4200;
|
|
42
|
+
|
|
43
|
+
function readJsonObject(path) {
|
|
44
|
+
try {
|
|
45
|
+
const parsed = JSON.parse(readFileSync(path, 'utf-8'));
|
|
46
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : null;
|
|
47
|
+
} catch {
|
|
48
|
+
// Missing or malformed - that layer contributes nothing.
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Resolve the local workspace port from the merged preferences.
|
|
55
|
+
* Overlay keys win; keys it omits fall through to the synced base; a device
|
|
56
|
+
* with no overlay behaves exactly as before this change.
|
|
57
|
+
*
|
|
58
|
+
* Paths are injectable for tests only - production callers pass nothing.
|
|
59
|
+
*
|
|
60
|
+
* @param {object} [paths]
|
|
61
|
+
* @param {string} [paths.base] preferences.json path.
|
|
62
|
+
* @param {string} [paths.local] preferences.local.json path.
|
|
63
|
+
* @returns {number}
|
|
64
|
+
*/
|
|
65
|
+
export function resolveWorkspacePort({ base = PREFERENCES_PATH, local = PREFERENCES_LOCAL_PATH } = {}) {
|
|
66
|
+
const prefs = { ...(readJsonObject(base) || {}), ...(readJsonObject(local) || {}) };
|
|
67
|
+
return typeof prefs.workspacePort === 'number' ? prefs.workspacePort : PLATFORM_DEFAULT_PORT;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function resolvePort(overridePort) {
|
|
71
|
+
if (typeof overridePort === 'number') return overridePort;
|
|
72
|
+
return resolveWorkspacePort();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Fire a fail-silent POST notify against the local workspace server.
|
|
77
|
+
* Always resolves; never throws.
|
|
78
|
+
*
|
|
79
|
+
* @param {object} opts
|
|
80
|
+
* @param {string} opts.endpoint Path to POST to, e.g. '/__api/jira/invalidate'.
|
|
81
|
+
* @param {string} [opts.disableEnv] Env var name; value '0' short-circuits
|
|
82
|
+
* (skips the network call entirely).
|
|
83
|
+
* @param {object} [opts.body] JSON body to send. Defaults to {}.
|
|
84
|
+
* @param {number} [opts.port] Override the resolved workspace port.
|
|
85
|
+
* @param {number} [opts.timeoutMs] Override the abort timeout (default 500ms).
|
|
86
|
+
* @param {Function} [opts.fetchImpl] Override fetch (test injection).
|
|
87
|
+
* @returns {Promise<{ok: boolean, error?: string}>}
|
|
88
|
+
*/
|
|
89
|
+
export async function notifyInvalidate(opts = {}) {
|
|
90
|
+
const { endpoint, disableEnv, body = {}, timeoutMs = DEFAULT_TIMEOUT_MS, fetchImpl } = opts;
|
|
91
|
+
|
|
92
|
+
if (disableEnv && process.env[disableEnv] === '0') {
|
|
93
|
+
return { ok: false, error: 'disabled' };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const fetchFn = typeof fetchImpl === 'function' ? fetchImpl : (fetchImpl || globalThis.fetch);
|
|
97
|
+
if (typeof fetchFn !== 'function') {
|
|
98
|
+
return { ok: false, error: 'fetch unavailable' };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const port = resolvePort(opts.port);
|
|
102
|
+
const url = `http://127.0.0.1:${port}${endpoint}`;
|
|
103
|
+
|
|
104
|
+
const controller = new AbortController();
|
|
105
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
106
|
+
try {
|
|
107
|
+
const res = await fetchFn(url, {
|
|
108
|
+
method: 'POST',
|
|
109
|
+
headers: { 'Content-Type': 'application/json' },
|
|
110
|
+
body: JSON.stringify(body),
|
|
111
|
+
signal: controller.signal,
|
|
112
|
+
});
|
|
113
|
+
if (!res.ok) {
|
|
114
|
+
return { ok: false, error: `http ${res.status}` };
|
|
115
|
+
}
|
|
116
|
+
return { ok: true };
|
|
117
|
+
} catch (err) {
|
|
118
|
+
return { ok: false, error: err && err.message ? err.message : String(err) };
|
|
119
|
+
} finally {
|
|
120
|
+
clearTimeout(timer);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Fire-and-forget wrapper. Returns immediately usable promise that never
|
|
126
|
+
* rejects, even if the caller doesn't await it - safe to call without a
|
|
127
|
+
* trailing .catch().
|
|
128
|
+
*
|
|
129
|
+
* @param {object} [opts] Same shape as notifyInvalidate.
|
|
130
|
+
* @returns {Promise<{ok: boolean, error?: string}>}
|
|
131
|
+
*/
|
|
132
|
+
export function notifyInvalidateAsync(opts = {}) {
|
|
133
|
+
return notifyInvalidate(opts).catch((err) => ({
|
|
134
|
+
ok: false,
|
|
135
|
+
error: err && err.message ? err.message : String(err),
|
|
136
|
+
}));
|
|
137
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// pa-name.mjs - Resolve the configured PA name for colleague-visible
|
|
2
|
+
// artefacts (e.g. the marker prefix on Jira comments the CLIs post).
|
|
3
|
+
//
|
|
4
|
+
// Precedence:
|
|
5
|
+
// 1. env PA_NAME - explicit per-shell override, wins outright.
|
|
6
|
+
// 2. paName in the operator preferences (device-local
|
|
7
|
+
// preferences.local.json overlay over the synced preferences.json) -
|
|
8
|
+
// the scaffold writes this at pa-init time; it matches the
|
|
9
|
+
// `agents/<paName>/` directory shape.
|
|
10
|
+
// 3. `'pa'` - a generic fallback so the marker is never blank.
|
|
11
|
+
//
|
|
12
|
+
// The reader mirrors invalidate-client.mjs on repo-root resolution and
|
|
13
|
+
// preferences layering so operators get one story.
|
|
14
|
+
|
|
15
|
+
import { resolve } from 'node:path';
|
|
16
|
+
import { readFileSync } from 'node:fs';
|
|
17
|
+
|
|
18
|
+
const REPO_ROOT =
|
|
19
|
+
process.env.PA_REPO_ROOT || process.env.DAVE_ROOT || process.cwd();
|
|
20
|
+
const PREFERENCES_PATH = resolve(REPO_ROOT, 'operator', 'config', 'preferences.json');
|
|
21
|
+
const PREFERENCES_LOCAL_PATH = resolve(REPO_ROOT, 'operator', 'config', 'preferences.local.json');
|
|
22
|
+
|
|
23
|
+
const DEFAULT_PA_NAME = 'pa';
|
|
24
|
+
|
|
25
|
+
function readJsonObject(path) {
|
|
26
|
+
try {
|
|
27
|
+
const parsed = JSON.parse(readFileSync(path, 'utf-8'));
|
|
28
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : null;
|
|
29
|
+
} catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function isNonEmptyString(v) {
|
|
35
|
+
return typeof v === 'string' && v.trim().length > 0;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Resolve the configured PA name.
|
|
40
|
+
*
|
|
41
|
+
* Paths are injectable for tests only - production callers pass nothing.
|
|
42
|
+
*
|
|
43
|
+
* @param {object} [opts]
|
|
44
|
+
* @param {string} [opts.envValue] Override the PA_NAME env value (test injection).
|
|
45
|
+
* @param {string} [opts.base] preferences.json path.
|
|
46
|
+
* @param {string} [opts.local] preferences.local.json path.
|
|
47
|
+
* @returns {string}
|
|
48
|
+
*/
|
|
49
|
+
export function resolvePaName({
|
|
50
|
+
envValue = process.env.PA_NAME,
|
|
51
|
+
base = PREFERENCES_PATH,
|
|
52
|
+
local = PREFERENCES_LOCAL_PATH,
|
|
53
|
+
} = {}) {
|
|
54
|
+
if (isNonEmptyString(envValue)) return envValue.trim();
|
|
55
|
+
const prefs = { ...(readJsonObject(base) || {}), ...(readJsonObject(local) || {}) };
|
|
56
|
+
if (isNonEmptyString(prefs.paName)) return prefs.paName.trim();
|
|
57
|
+
return DEFAULT_PA_NAME;
|
|
58
|
+
}
|