@stravica-ai/digital-operator-pa 0.1.42 → 0.1.43
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/dist/engineering-pa-core-extensions-atlassian/package.json +1 -1
- package/dist/engineering-pa-core-extensions-atlassian/src/confluence/client.mjs +3 -23
- package/dist/engineering-pa-core-extensions-atlassian/src/jira/client.mjs +69 -26
- package/dist/engineering-pa-scaffold/package.json +1 -1
- package/dist/engineering-pa-scaffold/src/commands/init.mjs +12 -10
- package/dist/engineering-pa-scaffold/src/generator.mjs +33 -23
- package/docs/onboarding-solo.md +22 -4
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stravica-ai/engineering-pa-core-extensions-atlassian",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
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",
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
// Zero external dependencies - uses Node.js built-in fetch() and child_process.
|
|
10
10
|
|
|
11
11
|
import { loadAtlassianCredentials } from '../common/secrets.mjs';
|
|
12
|
+
import { readBody, formatHttpError } from '../jira/client.mjs';
|
|
12
13
|
|
|
13
14
|
// ---- Exit-code-aware error ----------------------------------------------
|
|
14
15
|
|
|
@@ -145,33 +146,12 @@ export function createClient(options = {}) {
|
|
|
145
146
|
// Parse response
|
|
146
147
|
let data = null;
|
|
147
148
|
if (res.status !== 204) {
|
|
148
|
-
|
|
149
|
-
if (contentType.includes('application/json')) {
|
|
150
|
-
data = await res.json();
|
|
151
|
-
} else {
|
|
152
|
-
const text = await res.text();
|
|
153
|
-
if (text) data = text;
|
|
154
|
-
}
|
|
149
|
+
data = await readBody(res);
|
|
155
150
|
}
|
|
156
151
|
|
|
157
152
|
// 4xx errors
|
|
158
153
|
if (res.status >= 400) {
|
|
159
|
-
|
|
160
|
-
if (data && data.errorMessages) msgs.push(...data.errorMessages);
|
|
161
|
-
if (data && data.errors) {
|
|
162
|
-
if (Array.isArray(data.errors)) {
|
|
163
|
-
for (const e of data.errors) {
|
|
164
|
-
msgs.push(e.title || e.message || JSON.stringify(e));
|
|
165
|
-
}
|
|
166
|
-
} else {
|
|
167
|
-
for (const [field, msg] of Object.entries(data.errors)) {
|
|
168
|
-
msgs.push(`${field}: ${msg}`);
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
if (data && data.message) msgs.push(data.message);
|
|
173
|
-
const detail = msgs.length > 0 ? msgs.join('; ') : `HTTP ${res.status}`;
|
|
174
|
-
throw new ConfluenceError(`Confluence API error (${res.status}): ${detail}`, 2);
|
|
154
|
+
throw new ConfluenceError(formatHttpError('Confluence', res.status, data), 2);
|
|
175
155
|
}
|
|
176
156
|
|
|
177
157
|
return { status: res.status, data, headers: res.headers };
|
|
@@ -17,6 +17,71 @@ export class JiraError extends Error {
|
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
// ---- Shared response parsing --------------------------------------------
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Read a fetch Response body without ever throwing on a bad body.
|
|
24
|
+
*
|
|
25
|
+
* Atlassian sometimes returns a non-JSON body (bare text, an HTML error
|
|
26
|
+
* page from Cloudflare, a proxy edge string) with a `Content-Type` that
|
|
27
|
+
* still says `application/json`, and a raw `res.json()` on that body throws
|
|
28
|
+
* `SyntaxError: Unexpected token 'C', "Client mus"... is not valid JSON`,
|
|
29
|
+
* which surfaces as an unhandled stack trace instead of an
|
|
30
|
+
* "authentication failed" message. Falls back to raw text on any parse
|
|
31
|
+
* failure and returns null for empty bodies.
|
|
32
|
+
*
|
|
33
|
+
* @param {Response} res
|
|
34
|
+
* @returns {Promise<any|string|null>}
|
|
35
|
+
*/
|
|
36
|
+
export async function readBody(res) {
|
|
37
|
+
const contentType = res.headers.get('Content-Type') || '';
|
|
38
|
+
const wantsJson = contentType.includes('application/json');
|
|
39
|
+
const raw = await res.text();
|
|
40
|
+
if (!raw) return null;
|
|
41
|
+
if (wantsJson) {
|
|
42
|
+
try {
|
|
43
|
+
return JSON.parse(raw);
|
|
44
|
+
} catch {
|
|
45
|
+
return raw.trim();
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return raw.trim();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Turn a parsed error body into a one-line, colleague-facing HTTP
|
|
53
|
+
* error message. `data` may be a parsed JSON object (Atlassian error
|
|
54
|
+
* shape), a plain string (non-JSON body), or null.
|
|
55
|
+
*
|
|
56
|
+
* @param {string} product "Jira" or "Confluence"
|
|
57
|
+
* @param {number} status HTTP status
|
|
58
|
+
* @param {any} data Parsed body, string, or null
|
|
59
|
+
* @returns {string}
|
|
60
|
+
*/
|
|
61
|
+
export function formatHttpError(product, status, data) {
|
|
62
|
+
const parts = [];
|
|
63
|
+
if (data && typeof data === 'object') {
|
|
64
|
+
if (Array.isArray(data.errorMessages)) parts.push(...data.errorMessages);
|
|
65
|
+
if (data.errors) {
|
|
66
|
+
if (Array.isArray(data.errors)) {
|
|
67
|
+
for (const e of data.errors) {
|
|
68
|
+
parts.push(e.title || e.message || JSON.stringify(e));
|
|
69
|
+
}
|
|
70
|
+
} else if (typeof data.errors === 'object') {
|
|
71
|
+
for (const [field, msg] of Object.entries(data.errors)) {
|
|
72
|
+
parts.push(`${field}: ${msg}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (data.message && parts.length === 0) parts.push(data.message);
|
|
77
|
+
} else if (typeof data === 'string' && data.length > 0) {
|
|
78
|
+
const trimmed = data.length > 120 ? `${data.slice(0, 120)}...` : data;
|
|
79
|
+
parts.push(trimmed);
|
|
80
|
+
}
|
|
81
|
+
const detail = parts.length > 0 ? parts.join('; ') : `HTTP ${status}`;
|
|
82
|
+
return `${product} API error (${status}): ${detail}`;
|
|
83
|
+
}
|
|
84
|
+
|
|
20
85
|
// ---- Credential loading -------------------------------------------------
|
|
21
86
|
|
|
22
87
|
/**
|
|
@@ -142,26 +207,12 @@ export function createClient(options = {}) {
|
|
|
142
207
|
// Parse response
|
|
143
208
|
let data = null;
|
|
144
209
|
if (res.status !== 204) {
|
|
145
|
-
|
|
146
|
-
if (contentType.includes('application/json')) {
|
|
147
|
-
data = await res.json();
|
|
148
|
-
} else {
|
|
149
|
-
const text = await res.text();
|
|
150
|
-
if (text) data = text;
|
|
151
|
-
}
|
|
210
|
+
data = await readBody(res);
|
|
152
211
|
}
|
|
153
212
|
|
|
154
213
|
// 4xx errors
|
|
155
214
|
if (res.status >= 400) {
|
|
156
|
-
|
|
157
|
-
if (data && data.errorMessages) msgs.push(...data.errorMessages);
|
|
158
|
-
if (data && data.errors) {
|
|
159
|
-
for (const [field, msg] of Object.entries(data.errors)) {
|
|
160
|
-
msgs.push(`${field}: ${msg}`);
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
const detail = msgs.length > 0 ? msgs.join('; ') : `HTTP ${res.status}`;
|
|
164
|
-
throw new JiraError(`Jira API error (${res.status}): ${detail}`, 2);
|
|
215
|
+
throw new JiraError(formatHttpError('Jira', res.status, data), 2);
|
|
165
216
|
}
|
|
166
217
|
|
|
167
218
|
return { status: res.status, data, headers: res.headers };
|
|
@@ -309,19 +360,11 @@ export function createClient(options = {}) {
|
|
|
309
360
|
|
|
310
361
|
let data = null;
|
|
311
362
|
if (res.status !== 204) {
|
|
312
|
-
|
|
313
|
-
if (contentType.includes('application/json')) data = await res.json();
|
|
314
|
-
else { const text = await res.text(); if (text) data = text; }
|
|
363
|
+
data = await readBody(res);
|
|
315
364
|
}
|
|
316
365
|
|
|
317
366
|
if (res.status >= 400) {
|
|
318
|
-
|
|
319
|
-
if (data && data.errorMessages) msgs.push(...data.errorMessages);
|
|
320
|
-
if (data && data.errors) {
|
|
321
|
-
for (const [field, msg] of Object.entries(data.errors)) msgs.push(`${field}: ${msg}`);
|
|
322
|
-
}
|
|
323
|
-
const detail = msgs.length > 0 ? msgs.join('; ') : `HTTP ${res.status}`;
|
|
324
|
-
throw new JiraError(`Jira API error (${res.status}): ${detail}`, 2);
|
|
367
|
+
throw new JiraError(formatHttpError('Jira', res.status, data), 2);
|
|
325
368
|
}
|
|
326
369
|
|
|
327
370
|
return { status: res.status, data, headers: res.headers };
|
|
@@ -71,22 +71,24 @@ export async function runInit(options) {
|
|
|
71
71
|
return 2;
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
-
// Version-drift safety net (WSD-laptop cold install of 0.1.41
|
|
75
|
-
// scaffold pins an exact version and
|
|
76
|
-
//
|
|
77
|
-
//
|
|
78
|
-
//
|
|
79
|
-
//
|
|
80
|
-
//
|
|
74
|
+
// Version-drift safety net (WSD-laptop cold install of 0.1.41, second-
|
|
75
|
+
// machine re-run of 0.1.42). The scaffold pins an exact version and
|
|
76
|
+
// ships a `pnpm-workspace.yaml` with `minimumReleaseAgeExclude` for
|
|
77
|
+
// this package, so pnpm should never resolve a different version, but
|
|
78
|
+
// if the guard ever misses, this prints one line naming the mismatch
|
|
79
|
+
// so a colleague can see it immediately instead of hitting a "Command
|
|
80
|
+
// X not found" on an older bin surface. Read from the SAME source
|
|
81
|
+
// `pa status` reads: the resolved package.json inside the instance's
|
|
82
|
+
// node_modules.
|
|
81
83
|
const scaffoldedVersion = config.pkgVersion;
|
|
82
84
|
const installedVersion = readInstalledDopVersion(config.targetDir);
|
|
83
85
|
if (scaffoldedVersion && installedVersion && installedVersion !== scaffoldedVersion) {
|
|
84
86
|
process.stderr.write(
|
|
85
|
-
`pa-init: WARNING
|
|
87
|
+
`pa-init: WARNING - scaffold pinned @stravica-ai/digital-operator-pa@${scaffoldedVersion} `
|
|
86
88
|
+ `but pnpm resolved ${installedVersion}. This usually means pnpm's `
|
|
87
89
|
+ `minimumReleaseAge gate filtered the pinned version out; the shipped `
|
|
88
|
-
+
|
|
89
|
-
+ `Run \`pnpm install\` inside ${config.targetDir} again to reconcile.\n`,
|
|
90
|
+
+ `pnpm-workspace.yaml excludes @stravica-ai/digital-operator-pa from the gate `
|
|
91
|
+
+ `which normally cures it. Run \`pnpm install\` inside ${config.targetDir} again to reconcile.\n`,
|
|
90
92
|
);
|
|
91
93
|
}
|
|
92
94
|
|
|
@@ -327,32 +327,42 @@ export function buildGitignore() {
|
|
|
327
327
|
}
|
|
328
328
|
|
|
329
329
|
/**
|
|
330
|
-
* Build the instance-level
|
|
330
|
+
* Build the instance-level `pnpm-workspace.yaml` contents.
|
|
331
331
|
*
|
|
332
|
-
*
|
|
333
|
-
*
|
|
334
|
-
*
|
|
335
|
-
*
|
|
336
|
-
*
|
|
337
|
-
*
|
|
338
|
-
*
|
|
339
|
-
*
|
|
340
|
-
* `
|
|
341
|
-
*
|
|
342
|
-
*
|
|
343
|
-
*
|
|
344
|
-
*
|
|
345
|
-
*
|
|
332
|
+
* Excludes `@stravica-ai/digital-operator-pa` from pnpm's supply-chain
|
|
333
|
+
* age gate (`minimumReleaseAge`) BY NAME. Everything else the instance
|
|
334
|
+
* ever installs still honours whatever `minimumReleaseAge` the operator
|
|
335
|
+
* has set. The exclude ships instead of the older `.npmrc` line because
|
|
336
|
+
* pnpm reads `minimumReleaseAge` / `minimumReleaseAgeExclude` from
|
|
337
|
+
* `pnpm-workspace.yaml` only; `.npmrc` is not honoured for these
|
|
338
|
+
* settings (empirically verified on pnpm 10.32 against the live
|
|
339
|
+
* registry: `.npmrc @stravica-ai:minimumReleaseAge=0` did not defeat a
|
|
340
|
+
* `minimumReleaseAge=10080` set at the same scope; a
|
|
341
|
+
* `pnpm-workspace.yaml` with `minimumReleaseAgeExclude: [<pkg>]` did).
|
|
342
|
+
*
|
|
343
|
+
* Rationale from the second-machine re-run of 0.1.42 (2026-09-16): the
|
|
344
|
+
* `.npmrc` override written into the instance was a no-op, so the
|
|
345
|
+
* belt-and-brace promised alongside the exact pin never bit. This file
|
|
346
|
+
* is the real belt-and-brace: `pnpm install` inside the instance always
|
|
347
|
+
* resolves the pinned version even when the operator's pnpm carries a
|
|
348
|
+
* non-zero `minimumReleaseAge` (pnpm 11 ships one by default).
|
|
349
|
+
*
|
|
350
|
+
* The file also marks the instance as its own pnpm workspace root,
|
|
351
|
+
* which is fine: instances never nest inside another pnpm workspace
|
|
352
|
+
* (the onboarding guide's warning covers the reverse direction).
|
|
346
353
|
*/
|
|
347
|
-
export function
|
|
354
|
+
export function buildInstanceWorkspaceYaml() {
|
|
348
355
|
return [
|
|
349
|
-
'# @stravica-ai
|
|
350
|
-
'# pa-init pins
|
|
351
|
-
'# pnpm\'s default
|
|
352
|
-
'#
|
|
353
|
-
'# with a different bin surface (WSD-laptop cold-install regression,
|
|
356
|
+
'# @stravica-ai/digital-operator-pa exclusion for pnpm supply-chain gating.',
|
|
357
|
+
'# pa-init pins the exact @stravica-ai/digital-operator-pa version in package.json,',
|
|
358
|
+
'# but pnpm\'s minimumReleaseAge (default 0 on pnpm 10, non-zero on some 11 defaults)',
|
|
359
|
+
'# can still filter that pin out during `pnpm install` and drop the instance onto a',
|
|
360
|
+
'# previous version with a different bin surface (WSD-laptop cold-install regression,',
|
|
361
|
+
'# 0.1.41 / 0.1.42). pnpm reads minimumReleaseAge only from pnpm-workspace.yaml, so',
|
|
362
|
+
'# the exclusion has to live here rather than in .npmrc.',
|
|
354
363
|
'# Non-@stravica-ai packages still honour any minimumReleaseAge set globally.',
|
|
355
|
-
'
|
|
364
|
+
'minimumReleaseAgeExclude:',
|
|
365
|
+
' - "@stravica-ai/digital-operator-pa"',
|
|
356
366
|
'',
|
|
357
367
|
].join('\n');
|
|
358
368
|
}
|
|
@@ -655,7 +665,7 @@ export function buildPlanFromTemplates(config, templates) {
|
|
|
655
665
|
plan.push({ kind: 'json', path: 'package.json', contents: pkg });
|
|
656
666
|
plan.push({ kind: 'file', path: '.gitignore', contents: gitignore });
|
|
657
667
|
plan.push({ kind: 'file', path: '.nvmrc', contents: '24\n' });
|
|
658
|
-
plan.push({ kind: 'file', path: '.
|
|
668
|
+
plan.push({ kind: 'file', path: 'pnpm-workspace.yaml', contents: buildInstanceWorkspaceYaml() });
|
|
659
669
|
|
|
660
670
|
plan.push({ kind: 'json', path: '.claude/settings.json', contents: buildClaudeSettingsJson() });
|
|
661
671
|
// /research slash command: rendered from the packaged core doc so the
|
package/docs/onboarding-solo.md
CHANGED
|
@@ -21,11 +21,25 @@ Linux and WSL are equivalent. Install Node 24 through your platform's normal cha
|
|
|
21
21
|
|
|
22
22
|
```sh
|
|
23
23
|
mkdir ~/pa-bootstrap && cd ~/pa-bootstrap
|
|
24
|
-
#
|
|
25
|
-
# and `-y` is silently ignored, so write a minimal
|
|
24
|
+
# Two files, one heredoc each. `pnpm init -y` writes a `devEngines.packageManager`
|
|
25
|
+
# line pnpm 11.14 then rejects, and `-y` is silently ignored, so write a minimal
|
|
26
|
+
# package.json directly. The `pnpm-workspace.yaml` is the load-bearing file:
|
|
27
|
+
# pnpm holds packages published in the last week back by default (the
|
|
28
|
+
# `minimumReleaseAge` supply-chain gate; it defaults to 0 on pnpm 10 and to a
|
|
29
|
+
# non-zero window on some pnpm 11 defaults), and this repo releases often, so
|
|
30
|
+
# without an explicit exclude an unversioned `pnpm add` silently resolves down
|
|
31
|
+
# to a previous version with a different bin surface. pnpm reads
|
|
32
|
+
# `minimumReleaseAge*` settings ONLY from `pnpm-workspace.yaml` (not from
|
|
33
|
+
# `.npmrc`), and the exclude below trusts only this one package by name so
|
|
34
|
+
# every other dependency your bootstrap ever pulls still honours the operator's
|
|
35
|
+
# global gate.
|
|
26
36
|
cat > package.json <<'JSON'
|
|
27
37
|
{ "name": "pa-bootstrap", "private": true }
|
|
28
38
|
JSON
|
|
39
|
+
cat > pnpm-workspace.yaml <<'YAML'
|
|
40
|
+
minimumReleaseAgeExclude:
|
|
41
|
+
- "@stravica-ai/digital-operator-pa"
|
|
42
|
+
YAML
|
|
29
43
|
pnpm add @stravica-ai/digital-operator-pa
|
|
30
44
|
pnpm exec pa-init --mode=solo --name=<pa-name> --operator=<your-name> \
|
|
31
45
|
--target=$HOME/<pa-name>-instance --non-interactive
|
|
@@ -37,11 +51,13 @@ git add -A && git commit -m "scaffold"
|
|
|
37
51
|
|
|
38
52
|
Pick a `<pa-name>` in lowercase kebab-case (`alex`, `sam`, `hal-9000`). It becomes the agent directory (`agents/<pa-name>/`) and shows up in every log line.
|
|
39
53
|
|
|
40
|
-
`pa-init` writes the instance's `package.json` with an EXACT `@stravica-ai/digital-operator-pa` dependency (the version it just installed into `pa-bootstrap`, not a caret range) and drops
|
|
54
|
+
`pa-init` writes the instance's `package.json` with an EXACT `@stravica-ai/digital-operator-pa` dependency (the version it just installed into `pa-bootstrap`, not a caret range) and drops a `pnpm-workspace.yaml` inside the instance that excludes `@stravica-ai/digital-operator-pa` from the same age gate. Together they belt-and-brace pnpm's supply-chain gate: without them a freshly-published tarball can silently resolve down to the previous version, which may ship a different bin surface.
|
|
55
|
+
|
|
56
|
+
**Sanity check the version.** `pa-init` prints `package: @stravica-ai/digital-operator-pa@<version>` right after the target line. If the version you see is older than the one `npm view @stravica-ai/digital-operator-pa version` reports, the exclude in your bootstrap `pnpm-workspace.yaml` did not take (rare; usually a stale pnpm store or a typo in the yaml). The `@latest` tag does NOT bypass the release-age gate, so the recovery is to pin the version npm names: `pnpm add @stravica-ai/digital-operator-pa@<version npm view prints>` inside the bootstrap dir, then re-run `pa-init`. When pnpm 11 sees a pinned version that would otherwise be gated, it adds an entry of its own to `pnpm-workspace.yaml` under `minimumReleaseAgeExclude`; the file the guide already asked you to write covers that write.
|
|
41
57
|
|
|
42
58
|
`pnpm install` in the instance runs automatically as the last step of `pa-init` (skip it with `--skip-install` for CI). The `pa status` verification also runs at the end and prints the resolved package version; a mismatch between the scaffold pin and the installed version prints a WARNING line naming both, so a broken guard surfaces immediately instead of hiding behind a "Command not found" later.
|
|
43
59
|
|
|
44
|
-
**Do not put the instance under a directory that already contains a `pnpm-workspace.yaml`.** pnpm treats the instance as a workspace member, `pnpm install` silently no-ops, and `node_modules/.bin/pa` never appears. Keep `--target` outside any pnpm workspace root (a top-level `$HOME/<pa-name>-instance
|
|
60
|
+
**Do not put the instance under a directory that already contains a `pnpm-workspace.yaml`.** pnpm treats the instance as a workspace member, `pnpm install` silently no-ops, and `node_modules/.bin/pa` never appears. Keep `--target` outside any pnpm workspace root (a top-level `$HOME/<pa-name>-instance`, as shown above, is safe; `~/pa-bootstrap` itself is such a workspace root now, so never nest the `--target` inside it).
|
|
45
61
|
|
|
46
62
|
## 2. Sanity-check the install
|
|
47
63
|
|
|
@@ -136,6 +152,8 @@ pnpm exec secrets set JIRA_API_TOKEN <paste-token> # id.atlassian.com/mana
|
|
|
136
152
|
|
|
137
153
|
`secrets set` self-initialises the vault (age keypair, `.vault/recipients.txt`, empty encrypted store) on the first write when there is nothing at `.vault/` yet. It prints a one-line notice on stderr saying so and continues. You do not need to run `secrets init` manually first. Every `secrets set` also prints one stderr line naming which vault it wrote to; on a fresh solo install with no scope configured that says the write went to the `(legacy)` pot (no scope specified, no default configured). Informational, safe to ignore. Set `SECRETS_DEFAULT_SCOPE` in your environment or configure `defaultScope` in `~/.config/stravica-pa/defaults.json` if you want a named scope; solo mode does not require one.
|
|
138
154
|
|
|
155
|
+
The age key that self-init generates lives at `~/.config/stravica-pa/age-key.txt` (one per laptop, not per instance). If that file already exists when you scaffold a second PA on the same machine, self-init reuses it, so both instances read and write the same encrypted vault content when a `.vault/` sits under both. That is the point: a single key per operator per machine. To force a fresh key, move the existing one aside (`mv ~/.config/stravica-pa/age-key.txt ~/.config/stravica-pa/age-key.txt.prev`) before the first `secrets set` in the new instance; the vault contents in the old instance will then be unreadable to the new key.
|
|
156
|
+
|
|
139
157
|
Verify with `pnpm exec jira auth` and `pnpm exec confluence auth`; each prints the current user on success. Credentials also resolve from environment variables of the same names, so a `.env` or shell profile works as an alternative to `secrets`. The `atlassian` entry in `operator/extensions/manifest.json` records the extension is enabled and repeats the setup line.
|
|
140
158
|
|
|
141
159
|
`jira mine` returns your open issues across every project the account can see. If you spend most of your Jira time in one project, set `JIRA_DEFAULT_PROJECT` (env var or a vault key of the same name) and `jira mine` narrows to it unless you pass `--project KEY` or `--all`:
|
package/package.json
CHANGED