@gaia-ai/addon-gaia-ui-artifacts 0.10.0
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/LICENSE +21 -0
- package/README.md +5 -0
- package/dist/src/panel.d.ts +6 -0
- package/dist/src/panel.js +53 -0
- package/dist/src/preset.d.ts +2 -0
- package/dist/src/preset.js +4 -0
- package/dist/src/tree.d.ts +30 -0
- package/dist/src/tree.js +141 -0
- package/dist/src/ui.d.ts +1 -0
- package/dist/src/ui.js +6 -0
- package/package.json +27 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 keytec GmbH
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# @gaia-ai/addon-gaia-ui-artifacts
|
|
2
|
+
|
|
3
|
+
GAIA TUI: an Artifacts tab on ticket detail, reading assets-ai/<GAIA-nnn>/.
|
|
4
|
+
|
|
5
|
+
Part of the GAIA CLI. Install the meta package `@gaia-ai/gaia` to get the `gaia` CLI with all addons. Source: https://git.key-tec.de/keytec/gaia (gaia-cli/).
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { UiPanel } from '@gaia-ai/addon-gaia-ui/ui';
|
|
2
|
+
/**
|
|
3
|
+
* The ticket's generated artifacts — the tree GAIA-371 D3 defines, which D1 left
|
|
4
|
+
* with no reader at all: transient, gitignored, and listed nowhere else.
|
|
5
|
+
*/
|
|
6
|
+
export declare const artifactsPanel: UiPanel;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { artifactRoots, artifactSections } from './tree.js';
|
|
2
|
+
/** How many lines of one artifact an expanded entry shows (spec D9). */
|
|
3
|
+
const HEAD_LINES = 200;
|
|
4
|
+
const attribute = (doc, name) => {
|
|
5
|
+
const value = doc?.data?.attributes?.[name];
|
|
6
|
+
return typeof value === 'string' ? value : '';
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* The ticket's generated artifacts — the tree GAIA-371 D3 defines, which D1 left
|
|
10
|
+
* with no reader at all: transient, gitignored, and listed nowhere else.
|
|
11
|
+
*/
|
|
12
|
+
export const artifactsPanel = {
|
|
13
|
+
type: 'panel',
|
|
14
|
+
id: 'gaia/artifacts',
|
|
15
|
+
title: 'Artifacts',
|
|
16
|
+
// Present on every open ticket: an absent tree is an EMPTY STATE, not a
|
|
17
|
+
// hidden tab. Whether artifacts exist is exactly what the operator opens this
|
|
18
|
+
// to find out — a tab that vanished when there were none would answer the
|
|
19
|
+
// question by disappearing.
|
|
20
|
+
match: (ctx) => ctx.routeId === 'ticket.detail',
|
|
21
|
+
sections: (doc, ctx) => {
|
|
22
|
+
const identifier = attribute(doc, 'identifier');
|
|
23
|
+
if (ctx.cwd === undefined || identifier === '') {
|
|
24
|
+
return [
|
|
25
|
+
{
|
|
26
|
+
heading: 'Artifacts',
|
|
27
|
+
lines: [
|
|
28
|
+
'This cockpit has no checkout to read artifacts from.',
|
|
29
|
+
'Artifacts are transient and local (GAIA-371 D1/D10).',
|
|
30
|
+
],
|
|
31
|
+
},
|
|
32
|
+
];
|
|
33
|
+
}
|
|
34
|
+
const roots = artifactRoots(ctx.cwd, identifier, attribute(doc, 'branch_name'));
|
|
35
|
+
const sections = artifactSections(roots, { headLines: HEAD_LINES });
|
|
36
|
+
// The empty state NAMES BOTH PROBED PATHS. GAIA-371 D10 is a real hole —
|
|
37
|
+
// on another machine the worktree is gone — and naming the paths is what
|
|
38
|
+
// turns a blank tab into a diagnosis.
|
|
39
|
+
return sections.length > 0
|
|
40
|
+
? sections
|
|
41
|
+
: [
|
|
42
|
+
{
|
|
43
|
+
heading: 'Artifacts',
|
|
44
|
+
lines: [
|
|
45
|
+
'No artifacts in this checkout. Probed:',
|
|
46
|
+
...roots.map((root) => ` ${root}`),
|
|
47
|
+
'',
|
|
48
|
+
'Artifacts die with the worktree (GAIA-371 D1/D10).',
|
|
49
|
+
],
|
|
50
|
+
},
|
|
51
|
+
];
|
|
52
|
+
},
|
|
53
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { PanelSection } from '@gaia-ai/addon-gaia-ui/ui';
|
|
2
|
+
/**
|
|
3
|
+
* The roots to probe, in order (spec D9).
|
|
4
|
+
*
|
|
5
|
+
* Pure path joins: `cwd` comes from `TuiOptions`, `identifier` and `branch` off
|
|
6
|
+
* the open document, so there is no `git` call and nothing async. The second
|
|
7
|
+
* root is not a nicety — an operator starts `gaia ui` in the main checkout while
|
|
8
|
+
* a run writes its artifacts inside the worktree, so probing only the first
|
|
9
|
+
* would show nothing, systematically.
|
|
10
|
+
*/
|
|
11
|
+
export declare function artifactRoots(cwd: string, identifier: string, branch: string): string[];
|
|
12
|
+
/**
|
|
13
|
+
* One section per ARTIFACT, headed by its path under the ticket's tree so the
|
|
14
|
+
* producing tool leads every heading (`vitest/store-green.txt`,
|
|
15
|
+
* `playwright/logs/walk.txt`) and one tool's artifacts stand together.
|
|
16
|
+
*
|
|
17
|
+
* Why per artifact and not per tool directory: the panel contract is
|
|
18
|
+
* `PanelSection[]` with one collapsible entry per section (spec D4), and D9 asks
|
|
19
|
+
* for each FILE to be the expandable entry — its collapsed row the producing
|
|
20
|
+
* command, its expanded body the head of the capture. A section per tool could
|
|
21
|
+
* carry only one of those two, so the grouping lives in the heading instead,
|
|
22
|
+
* where it costs the narrow contract nothing.
|
|
23
|
+
*
|
|
24
|
+
* The first row of every text artifact is its own first line, which by GAIA-371
|
|
25
|
+
* D3 is the command that produced it — so the collapsed panel reads as the list
|
|
26
|
+
* of commands this ticket ran.
|
|
27
|
+
*/
|
|
28
|
+
export declare function artifactSections(roots: readonly string[], { headLines }: {
|
|
29
|
+
headLines: number;
|
|
30
|
+
}): readonly PanelSection[];
|
package/dist/src/tree.js
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { readdirSync, readFileSync, statSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
/**
|
|
4
|
+
* The roots to probe, in order (spec D9).
|
|
5
|
+
*
|
|
6
|
+
* Pure path joins: `cwd` comes from `TuiOptions`, `identifier` and `branch` off
|
|
7
|
+
* the open document, so there is no `git` call and nothing async. The second
|
|
8
|
+
* root is not a nicety — an operator starts `gaia ui` in the main checkout while
|
|
9
|
+
* a run writes its artifacts inside the worktree, so probing only the first
|
|
10
|
+
* would show nothing, systematically.
|
|
11
|
+
*/
|
|
12
|
+
export function artifactRoots(cwd, identifier, branch) {
|
|
13
|
+
const own = join(cwd, 'assets-ai', identifier);
|
|
14
|
+
return branch.trim() === ''
|
|
15
|
+
? [own]
|
|
16
|
+
: [own, join(cwd, '.gaia-worktrees', branch, 'assets-ai', identifier)];
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Text file heads, memoised per `(path, mtime, size)` — spec D9.
|
|
20
|
+
*
|
|
21
|
+
* That key is what makes the cache CORRECT rather than merely fast: keying on
|
|
22
|
+
* the directory's own mtime would go stale the moment a re-run overwrites a log
|
|
23
|
+
* in place, because rewriting a file does not touch its directory. There is no
|
|
24
|
+
* reliance on the `R` key either — it reloads the document and knows nothing
|
|
25
|
+
* about this tree.
|
|
26
|
+
*/
|
|
27
|
+
const heads = new Map();
|
|
28
|
+
const head = (path, lines) => {
|
|
29
|
+
const stat = statSync(path);
|
|
30
|
+
const key = `${path}:${stat.mtimeMs}:${stat.size}`;
|
|
31
|
+
const hit = heads.get(key);
|
|
32
|
+
if (hit)
|
|
33
|
+
return hit;
|
|
34
|
+
const all = readFileSync(path, 'utf8').replace(/\n$/, '').split('\n');
|
|
35
|
+
const kept = all.slice(0, lines);
|
|
36
|
+
if (all.length > lines)
|
|
37
|
+
kept.push(`… ${all.length - lines} more lines`);
|
|
38
|
+
heads.set(key, kept);
|
|
39
|
+
return kept;
|
|
40
|
+
};
|
|
41
|
+
/** Extensions never read: their bytes say nothing a terminal can show. */
|
|
42
|
+
const BINARY = /\.(png|jpe?g|gif|webp|zip|pdf|webm|mp4)$/i;
|
|
43
|
+
/**
|
|
44
|
+
* One directory's entries, or none when it cannot be read.
|
|
45
|
+
*
|
|
46
|
+
* A directory that vanished between the walk and the read is not an error worth
|
|
47
|
+
* a stack trace — this runs on the DRAW path, and a run writing into the tree is
|
|
48
|
+
* exactly the situation the panel exists for.
|
|
49
|
+
*/
|
|
50
|
+
const entriesOf = (dir) => {
|
|
51
|
+
try {
|
|
52
|
+
return readdirSync(dir, { withFileTypes: true });
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return [];
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* One FILE's section — and the same reasoning `entriesOf` states, applied to the
|
|
60
|
+
* calls that actually touch bytes.
|
|
61
|
+
*
|
|
62
|
+
* The race that comment names is not hypothetical for a file: a run unlinks or
|
|
63
|
+
* rewrites a capture between `readdirSync` and this read and the `stat`/`open`
|
|
64
|
+
* comes back ENOENT; a log written by a container as root comes back EACCES. On
|
|
65
|
+
* the DRAW path either one used to escape as a stack trace, and the host's guard
|
|
66
|
+
* would then have degraded the WHOLE listing to a single failure row — so one
|
|
67
|
+
* unreadable file would hide every artifact beside it. It costs its own row
|
|
68
|
+
* instead: the file stays listed under its real heading, the row says plainly
|
|
69
|
+
* that it could not be read, and it is NOT expandable, because there is nothing
|
|
70
|
+
* behind the caret to show. The errno leads, since `ENOENT`/`EACCES` is the part
|
|
71
|
+
* that tells the operator whether to re-run or to fix permissions.
|
|
72
|
+
*/
|
|
73
|
+
const fileSection = (heading, path, headLines) => {
|
|
74
|
+
try {
|
|
75
|
+
return BINARY.test(heading)
|
|
76
|
+
? // Listed, never read — and NOT expandable, because there is nothing
|
|
77
|
+
// behind the caret a terminal could show.
|
|
78
|
+
{ heading, lines: [`${statSync(path).size} bytes`] }
|
|
79
|
+
: { heading, expandable: true, lines: [...head(path, headLines)] };
|
|
80
|
+
}
|
|
81
|
+
catch (caught) {
|
|
82
|
+
const code = caught?.code;
|
|
83
|
+
return {
|
|
84
|
+
heading,
|
|
85
|
+
lines: [
|
|
86
|
+
`could not be read: ${typeof code === 'string'
|
|
87
|
+
? code
|
|
88
|
+
: caught instanceof Error
|
|
89
|
+
? caught.message
|
|
90
|
+
: String(caught)}`,
|
|
91
|
+
],
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
/**
|
|
96
|
+
* One section per ARTIFACT, headed by its path under the ticket's tree so the
|
|
97
|
+
* producing tool leads every heading (`vitest/store-green.txt`,
|
|
98
|
+
* `playwright/logs/walk.txt`) and one tool's artifacts stand together.
|
|
99
|
+
*
|
|
100
|
+
* Why per artifact and not per tool directory: the panel contract is
|
|
101
|
+
* `PanelSection[]` with one collapsible entry per section (spec D4), and D9 asks
|
|
102
|
+
* for each FILE to be the expandable entry — its collapsed row the producing
|
|
103
|
+
* command, its expanded body the head of the capture. A section per tool could
|
|
104
|
+
* carry only one of those two, so the grouping lives in the heading instead,
|
|
105
|
+
* where it costs the narrow contract nothing.
|
|
106
|
+
*
|
|
107
|
+
* The first row of every text artifact is its own first line, which by GAIA-371
|
|
108
|
+
* D3 is the command that produced it — so the collapsed panel reads as the list
|
|
109
|
+
* of commands this ticket ran.
|
|
110
|
+
*/
|
|
111
|
+
export function artifactSections(roots, { headLines }) {
|
|
112
|
+
const root = roots.find((candidate) => {
|
|
113
|
+
try {
|
|
114
|
+
return statSync(candidate).isDirectory();
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
if (root === undefined)
|
|
121
|
+
return [];
|
|
122
|
+
const sections = [];
|
|
123
|
+
const walk = (dir, prefix) => {
|
|
124
|
+
// Sorted by name, so the listing is the same on every draw and on every
|
|
125
|
+
// filesystem — `readdirSync` promises no order of its own.
|
|
126
|
+
const sorted = [...entriesOf(dir)].sort((a, b) => a.name.localeCompare(b.name));
|
|
127
|
+
for (const entry of sorted) {
|
|
128
|
+
const path = join(dir, entry.name);
|
|
129
|
+
const heading = prefix === '' ? entry.name : `${prefix}/${entry.name}`;
|
|
130
|
+
if (entry.isDirectory()) {
|
|
131
|
+
walk(path, heading);
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
if (!entry.isFile())
|
|
135
|
+
continue;
|
|
136
|
+
sections.push(fileSection(heading, path, headLines));
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
walk(root, '');
|
|
140
|
+
return sections;
|
|
141
|
+
}
|
package/dist/src/ui.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/src/ui.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// The entry module: it registers at IMPORT time, which is Storybook-faithful for
|
|
2
|
+
// the addon author and is why the kernel drains the collector once rather than
|
|
3
|
+
// handing a store around (spec D3).
|
|
4
|
+
import { register } from '@gaia-ai/addon-gaia-ui/ui';
|
|
5
|
+
import { artifactsPanel } from './panel.js';
|
|
6
|
+
register('gaia/artifacts', (api) => api.add(artifactsPanel.id, artifactsPanel));
|
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@gaia-ai/addon-gaia-ui-artifacts",
|
|
3
|
+
"version": "0.10.0",
|
|
4
|
+
"description": "GAIA TUI: an Artifacts tab on ticket detail, reading assets-ai/<GAIA-nnn>/.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./dist/src/panel.js",
|
|
9
|
+
"./preset": "./dist/src/preset.js",
|
|
10
|
+
"./ui": "./dist/src/ui.js"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"dist/src"
|
|
14
|
+
],
|
|
15
|
+
"publishConfig": {
|
|
16
|
+
"access": "public"
|
|
17
|
+
},
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://git.key-tec.de/keytec/gaia.git",
|
|
21
|
+
"directory": "gaia-cli/addons/gaia-ui-artifacts"
|
|
22
|
+
},
|
|
23
|
+
"peerDependencies": {
|
|
24
|
+
"@gaia-ai/addon-gaia-ui": "^0.10.0",
|
|
25
|
+
"@gaia-ai/core": "^0.10.0"
|
|
26
|
+
}
|
|
27
|
+
}
|