agentcage 0.1.6 → 0.1.7
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 +2 -1
- package/bin/agentcage.js +1 -0
- package/package.json +1 -1
- package/src/icon.js +42 -5
- package/src/settings.js +7 -1
- package/src/wizard.js +9 -5
package/README.md
CHANGED
|
@@ -106,6 +106,7 @@ created afterwards.
|
|
|
106
106
|
| Docker container prefix | `ac-` | container and volume names, e.g. `ac-myproject`, `ac-myproject-src` |
|
|
107
107
|
| OpenCode server prefix | `ac-` | the server's name in OpenCode Desktop |
|
|
108
108
|
| OpenCode project prefix | `ac-` | the project's name in OpenCode (the folder stays `/workspaces/<name>`) |
|
|
109
|
+
| Git token per project | `don't use` | `ask every time` asks token or forwarded credentials for each new container, `always use` always asks for a token. See [Git push access](#git-push-access) |
|
|
109
110
|
|
|
110
111
|
**Clean up dead containers** (also in Settings) lists leftovers of containers that no longer exist in Docker: agentcage
|
|
111
112
|
configs and OpenCode Desktop servers. Stopped containers are kept. A config whose volumes still hold code is not
|
|
@@ -113,7 +114,7 @@ preselected, because removing it deletes those volumes.
|
|
|
113
114
|
|
|
114
115
|
## Git push access
|
|
115
116
|
|
|
116
|
-
The copy in the container is separate from your laptop's, so changes leave the container via **git push**.
|
|
117
|
+
The copy in the container is separate from your laptop's, so changes leave the container via **git push**. There are two ways. By default no token is used and the dialog doesn't ask; the **Git token per project** setting changes that:
|
|
117
118
|
|
|
118
119
|
- **A token for this repo only (recommended).** The dialog shows how to create one: a fine-grained GitHub token limited to
|
|
119
120
|
the repo with *Contents: read and write*, a GitLab project access token, or a Bitbucket repository access token. The token
|
package/bin/agentcage.js
CHANGED
|
@@ -85,6 +85,7 @@ const commands = {
|
|
|
85
85
|
b.opencodeVersion = v; writeDevcontainer(b);
|
|
86
86
|
await up(b, true).then(() => s.stop(`Rebuilt with OpenCode ${v}`), e => { s.error('Rebuild failed'); throw e; });
|
|
87
87
|
saveContainer(b);
|
|
88
|
+
if (b.tools.includes('opencode')) await setupProject(b).catch(e => p.log.warn(`Could not set the project name and icon in OpenCode: ${e.message}`));
|
|
88
89
|
},
|
|
89
90
|
async remove() {
|
|
90
91
|
const b = container();
|
package/package.json
CHANGED
package/src/icon.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
// Generates the 128x128 project icon for OpenCode: dark green background, white text in two rows —
|
|
2
|
-
// the project prefix ("ac") in small letters on top, the first three letters of the container name below
|
|
2
|
+
// the project prefix ("ac") in small letters on top, the first three letters of the container name in capitals below —
|
|
3
|
+
// and a light color band along the bottom, its hue picked from the name (looks random, stays the same per project).
|
|
3
4
|
// A hand-rolled PNG encoder (zlib + CRC) with a 5x7 bitmap font keeps this dependency-free.
|
|
4
5
|
import { deflateSync } from 'node:zlib';
|
|
5
6
|
|
|
6
7
|
const SIZE = 128, SS = 4, BG = [20, 83, 45], FG = [255, 255, 255]; // SS: supersampling for smooth edges
|
|
7
8
|
|
|
8
|
-
|
|
9
|
+
const BAND = 4; // bottom pixel rows in the accent color
|
|
10
|
+
|
|
11
|
+
// 5x7 pixel font ('#' = ink); lowercase and capital letters, digits and - cover all names and prefixes
|
|
9
12
|
const FONT = {
|
|
10
13
|
a: ['.....', '.....', '.###.', '....#', '.####', '#...#', '.####'],
|
|
11
14
|
b: ['#....', '#....', '####.', '#...#', '#...#', '#...#', '####.'],
|
|
@@ -44,6 +47,39 @@ const FONT = {
|
|
|
44
47
|
8: ['.###.', '#...#', '#...#', '.###.', '#...#', '#...#', '.###.'],
|
|
45
48
|
9: ['.###.', '#...#', '#...#', '.####', '....#', '....#', '.###.'],
|
|
46
49
|
'-': ['.....', '.....', '.....', '.###.', '.....', '.....', '.....'],
|
|
50
|
+
A: ['.###.', '#...#', '#...#', '#####', '#...#', '#...#', '#...#'],
|
|
51
|
+
B: ['####.', '#...#', '#...#', '####.', '#...#', '#...#', '####.'],
|
|
52
|
+
C: ['.###.', '#...#', '#....', '#....', '#....', '#...#', '.###.'],
|
|
53
|
+
D: ['####.', '#...#', '#...#', '#...#', '#...#', '#...#', '####.'],
|
|
54
|
+
E: ['#####', '#....', '#....', '####.', '#....', '#....', '#####'],
|
|
55
|
+
F: ['#####', '#....', '#....', '####.', '#....', '#....', '#....'],
|
|
56
|
+
G: ['.###.', '#...#', '#....', '#.###', '#...#', '#...#', '.####'],
|
|
57
|
+
H: ['#...#', '#...#', '#...#', '#####', '#...#', '#...#', '#...#'],
|
|
58
|
+
I: ['.###.', '..#..', '..#..', '..#..', '..#..', '..#..', '.###.'],
|
|
59
|
+
J: ['..###', '...#.', '...#.', '...#.', '...#.', '#..#.', '.##..'],
|
|
60
|
+
K: ['#...#', '#..#.', '#.#..', '##...', '#.#..', '#..#.', '#...#'],
|
|
61
|
+
L: ['#....', '#....', '#....', '#....', '#....', '#....', '#####'],
|
|
62
|
+
M: ['#...#', '##.##', '#.#.#', '#.#.#', '#...#', '#...#', '#...#'],
|
|
63
|
+
N: ['#...#', '#...#', '##..#', '#.#.#', '#..##', '#...#', '#...#'],
|
|
64
|
+
O: ['.###.', '#...#', '#...#', '#...#', '#...#', '#...#', '.###.'],
|
|
65
|
+
P: ['####.', '#...#', '#...#', '####.', '#....', '#....', '#....'],
|
|
66
|
+
Q: ['.###.', '#...#', '#...#', '#...#', '#.#.#', '#..#.', '.##.#'],
|
|
67
|
+
R: ['####.', '#...#', '#...#', '####.', '#.#..', '#..#.', '#...#'],
|
|
68
|
+
S: ['.####', '#....', '#....', '.###.', '....#', '....#', '####.'],
|
|
69
|
+
T: ['#####', '..#..', '..#..', '..#..', '..#..', '..#..', '..#..'],
|
|
70
|
+
U: ['#...#', '#...#', '#...#', '#...#', '#...#', '#...#', '.###.'],
|
|
71
|
+
V: ['#...#', '#...#', '#...#', '#...#', '#...#', '.#.#.', '..#..'],
|
|
72
|
+
W: ['#...#', '#...#', '#...#', '#.#.#', '#.#.#', '#.#.#', '.#.#.'],
|
|
73
|
+
X: ['#...#', '#...#', '.#.#.', '..#..', '.#.#.', '#...#', '#...#'],
|
|
74
|
+
Y: ['#...#', '#...#', '.#.#.', '..#..', '..#..', '..#..', '..#..'],
|
|
75
|
+
Z: ['#####', '....#', '...#.', '..#..', '.#...', '#....', '#####'],
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
// Light accent color with a hue from the name, HSL(hue, 75%, 72%). Greens (hue 80-160) are skipped: no contrast with the background
|
|
79
|
+
const accent = name => {
|
|
80
|
+
const h = (160 + [...name].reduce((n, ch) => (n * 31 + ch.charCodeAt(0)) >>> 0, 7) % 280) % 360, s = 0.75, l = 0.72;
|
|
81
|
+
const f = k => { const a = s * Math.min(l, 1 - l), t = (k + h / 30) % 12; return Math.round(255 * (l - a * Math.max(-1, Math.min(t - 3, 9 - t, 1)))); };
|
|
82
|
+
return [f(0), f(8), f(4)];
|
|
47
83
|
};
|
|
48
84
|
|
|
49
85
|
const crcTable = Array.from({ length: 256 }, (_, n) => { let c = n; for (let k = 0; k < 8; k++) c = c & 1 ? 0xEDB88320 ^ (c >>> 1) : c >>> 1; return c; });
|
|
@@ -76,10 +112,11 @@ const draw = (img, dim, text, s, y) => {
|
|
|
76
112
|
// The icon as a data URL for OpenCode's project icon override
|
|
77
113
|
export const projectIcon = (name, prefix = 'ac') => {
|
|
78
114
|
const dim = SIZE * SS, img = Buffer.alloc(dim * dim * 3);
|
|
79
|
-
|
|
80
|
-
const
|
|
115
|
+
const band = accent(name);
|
|
116
|
+
for (let i = 0; i < dim * dim; i++) { const c = i >= dim * (SIZE - BAND) * SS ? band : BG; img[i * 3] = c[0]; img[i * 3 + 1] = c[1]; img[i * 3 + 2] = c[2]; }
|
|
117
|
+
const rows = [[prefix.replace(/[^a-z0-9]/g, ''), 3], [name.slice(0, 3).toUpperCase(), 6]].filter(([t]) => t);
|
|
81
118
|
const gap = 12, total = rows.reduce((n, [, s]) => n + 7 * s, 0) + gap * (rows.length - 1);
|
|
82
|
-
let y = Math.round((SIZE - total) / 2);
|
|
119
|
+
let y = Math.round((SIZE - BAND - total) / 2) - 4; // centered above the band, then nudged up
|
|
83
120
|
for (const [t, s] of rows) { draw(img, dim, t, s, y); y += 7 * s + gap; }
|
|
84
121
|
const out = Buffer.alloc(SIZE * SIZE * 4); // downsample SS x SS blocks for smooth edges
|
|
85
122
|
for (let py = 0; py < SIZE; py++) for (let px = 0; px < SIZE; px++) {
|
package/src/settings.js
CHANGED
|
@@ -6,10 +6,16 @@ import { join } from 'node:path';
|
|
|
6
6
|
const FILE = join(homedir(), '.agentcage', 'settings.json');
|
|
7
7
|
// Project icon colors OpenCode Desktop can show (others render as gray). green first: our default, gray: OpenCode's.
|
|
8
8
|
export const COLORS = ['green', 'gray', 'pink', 'blue', 'red', 'purple', 'yellow', 'orange', 'cyan'];
|
|
9
|
-
export const DEFAULTS = { color: 'green', containerPrefix: 'ac-', serverPrefix: 'ac-', projectPrefix: 'ac-' };
|
|
9
|
+
export const DEFAULTS = { color: 'green', containerPrefix: 'ac-', serverPrefix: 'ac-', projectPrefix: 'ac-', gitToken: 'never' };
|
|
10
10
|
export const LABELS = {
|
|
11
11
|
color: 'OpenCode project color', containerPrefix: 'Docker container prefix',
|
|
12
12
|
serverPrefix: 'OpenCode server prefix', projectPrefix: 'OpenCode project prefix',
|
|
13
|
+
gitToken: 'Git token per project (GitHub, GitLab, Bitbucket)',
|
|
14
|
+
};
|
|
15
|
+
// Settings with fixed choices ({ value: label }); the others are free-text prefixes
|
|
16
|
+
export const CHOICES = {
|
|
17
|
+
color: Object.fromEntries(COLORS.map(c => [c, c === 'gray' ? "gray (OpenCode's default)" : c])),
|
|
18
|
+
gitToken: { never: "don't use (push with the IDE's forwarded credentials)", ask: 'ask every time', always: 'always use' },
|
|
13
19
|
};
|
|
14
20
|
|
|
15
21
|
export const loadSettings = () => ({ ...DEFAULTS, ...(existsSync(FILE) && JSON.parse(readFileSync(FILE, 'utf8'))) });
|
package/src/wizard.js
CHANGED
|
@@ -7,7 +7,7 @@ import { cleanName, createContainer, listContainers, nameTaken, openIde, publish
|
|
|
7
7
|
import { isGitUrl, localRemote, parseRemote } from './git.js';
|
|
8
8
|
import { connectDesktop, desktopRunning, desktopServers, detectDesktop, latestVersion, loginProvider, removeDesktopServers, serverName, setupProject } from './opencode.js';
|
|
9
9
|
import { ok, runSync } from './run.js';
|
|
10
|
-
import {
|
|
10
|
+
import { CHOICES, DEFAULTS, LABELS, loadSettings, saveSettings } from './settings.js';
|
|
11
11
|
|
|
12
12
|
export const ask = async q => {
|
|
13
13
|
const v = await q;
|
|
@@ -112,13 +112,17 @@ export async function newWizard(f = {}) {
|
|
|
112
112
|
const remoteUrl = isGitUrl(src) ? src : localRemote(src), remote = parseRemote(remoteUrl);
|
|
113
113
|
let git = 'none', token;
|
|
114
114
|
if (remote) {
|
|
115
|
-
|
|
115
|
+
// The gitToken setting decides whether to ask (never / always skip the question); --git overrides it
|
|
116
|
+
const fixed = f.git ?? { never: 'forward', always: 'token' }[s.gitToken];
|
|
117
|
+
git = await pick(fixed, 'forward', () => ask(p.select({ message: `How should the container push to ${remote.host}/${remote.path}?`, options: [
|
|
116
118
|
{ value: 'token', label: 'A token for this repo only', hint: 'recommended: agents can only push here' },
|
|
117
119
|
{ value: 'forward', label: "Skip: use my IDE's forwarded credentials", hint: 'access to all your repos while the IDE is attached' },
|
|
118
120
|
] })));
|
|
119
121
|
if (git === 'token') {
|
|
120
122
|
p.note(hostInfo(remote.host).help(remote.path), 'Repo token');
|
|
121
123
|
token = process.env.AGENTCAGE_GIT_TOKEN ?? await ask(p.password({ message: 'Paste the token', validate: v => (v ? undefined : 'Required') }));
|
|
124
|
+
} else if (fixed && !f.git) {
|
|
125
|
+
p.log.info(`Git push: your IDE's forwarded credentials, no repo token (change in: agentcage settings)`);
|
|
122
126
|
} else if (!f.yes) {
|
|
123
127
|
p.note(FORWARD_WARNING, 'Warning: cross-repo access');
|
|
124
128
|
if (!(await ask(p.confirm({ message: 'Continue with forwarded credentials?', initialValue: false })))) return newWizard({ ...f, src, name });
|
|
@@ -176,12 +180,12 @@ export async function settingsWizard() {
|
|
|
176
180
|
const s = loadSettings();
|
|
177
181
|
for (;;) {
|
|
178
182
|
const key = await ask(p.select({ message: 'Which setting? (applies to new containers)', options: [
|
|
179
|
-
...Object.keys(DEFAULTS).map(k => ({ value: k, label: LABELS[k], hint: s[k] === '' ? 'none' : s[k] })),
|
|
183
|
+
...Object.keys(DEFAULTS).map(k => ({ value: k, label: LABELS[k], hint: s[k] === '' ? 'none' : CHOICES[k]?.[s[k]] ?? s[k] })),
|
|
180
184
|
{ value: '-cleanup', label: 'Clean up dead containers', hint: 'configs and OpenCode servers whose container is gone' }, { value: '', label: 'Done' }] }));
|
|
181
185
|
if (!key) break;
|
|
182
186
|
if (key === '-cleanup') { await cleanupFlow(); continue; }
|
|
183
|
-
s[key] = await ask(key
|
|
184
|
-
? p.select({ message: LABELS
|
|
187
|
+
s[key] = await ask(CHOICES[key]
|
|
188
|
+
? p.select({ message: LABELS[key], initialValue: s[key], options: Object.entries(CHOICES[key]).map(([value, label]) => ({ value, label })) })
|
|
185
189
|
: p.text({ message: `${LABELS[key]} (empty for none)`, initialValue: s[key], defaultValue: '', validate: v => (/^[a-z0-9-]*$/.test(v ?? '') ? undefined : 'Use lowercase letters, digits and -') }));
|
|
186
190
|
saveSettings(s);
|
|
187
191
|
}
|