@automagik/genie 4.260331.5 → 4.260331.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/.claude-plugin/marketplace.json +1 -1
- package/CLAUDE.md +31 -5
- package/dist/genie.js +600 -691
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/plugins/genie/.claude-plugin/plugin.json +1 -1
- package/plugins/genie/package.json +1 -1
- package/skills/genie/SKILL.md +71 -233
- package/skills/genie/reference/lifecycle.md +65 -0
- package/src/db/migrations/015_agent_archived_state.sql +0 -20
- package/src/db/migrations/018_drop_app_store.sql +4 -0
- package/src/genie.ts +26 -14
- package/src/lib/agent-sync.ts +55 -194
- package/src/lib/export-format.ts +1 -12
- package/src/lib/import-order.ts +1 -11
- package/src/term-commands/agent/directory.ts +2 -37
- package/src/term-commands/agent/index.ts +6 -0
- package/src/term-commands/dir.ts +3 -160
- package/src/term-commands/export.ts +0 -13
- package/src/term-commands/msg.ts +8 -0
- package/src/term-commands/team.ts +8 -0
- package/src/lib/agent-cache.ts +0 -282
- package/src/lib/manifest.ts +0 -342
- package/src/term-commands/install.ts +0 -372
- package/src/term-commands/item-uninstall.ts +0 -118
- package/src/term-commands/item-update.ts +0 -205
- package/src/term-commands/publish.ts +0 -187
|
@@ -1,372 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Install Command — `genie install <git-url>[@version]`
|
|
3
|
-
*
|
|
4
|
-
* Clones a git repository, detects/validates the manifest, registers in
|
|
5
|
-
* app_store, and performs type-specific setup (cache regen, board creation, etc.).
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { execSync } from 'node:child_process';
|
|
9
|
-
import { existsSync, mkdirSync, rmSync } from 'node:fs';
|
|
10
|
-
import { homedir } from 'node:os';
|
|
11
|
-
import { basename, join } from 'node:path';
|
|
12
|
-
import type { Command } from 'commander';
|
|
13
|
-
import {
|
|
14
|
-
getItemFromStore,
|
|
15
|
-
regenerateAgentCache,
|
|
16
|
-
registerItemInStore,
|
|
17
|
-
removeItemFromStore,
|
|
18
|
-
} from '../lib/agent-cache.js';
|
|
19
|
-
import { getActor, recordAuditEvent } from '../lib/audit.js';
|
|
20
|
-
import { getConnection, isAvailable } from '../lib/db.js';
|
|
21
|
-
import { type GenieManifest, type StageConfig, detectManifest, validateManifest } from '../lib/manifest.js';
|
|
22
|
-
|
|
23
|
-
const GENIE_HOME = process.env.GENIE_HOME ?? join(homedir(), '.genie');
|
|
24
|
-
const ITEMS_DIR = join(GENIE_HOME, 'items');
|
|
25
|
-
|
|
26
|
-
// ============================================================================
|
|
27
|
-
// URL parsing
|
|
28
|
-
// ============================================================================
|
|
29
|
-
|
|
30
|
-
interface ParsedUrl {
|
|
31
|
-
url: string;
|
|
32
|
-
version?: string;
|
|
33
|
-
name: string;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Parse a git install target like `github.com/user/repo@v1.2.0`
|
|
38
|
-
* into its components.
|
|
39
|
-
*/
|
|
40
|
-
function parseInstallTarget(target: string): ParsedUrl {
|
|
41
|
-
let url = target;
|
|
42
|
-
let version: string | undefined;
|
|
43
|
-
|
|
44
|
-
// Extract @version suffix
|
|
45
|
-
const atIdx = url.lastIndexOf('@');
|
|
46
|
-
if (atIdx > 0 && !url.slice(atIdx).includes('/')) {
|
|
47
|
-
version = url.slice(atIdx + 1);
|
|
48
|
-
url = url.slice(0, atIdx);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// Normalise bare github.com/user/repo → https://github.com/user/repo.git
|
|
52
|
-
if (!url.startsWith('http') && !url.startsWith('git@') && !url.startsWith('ssh://')) {
|
|
53
|
-
url = `https://${url}`;
|
|
54
|
-
}
|
|
55
|
-
if (url.startsWith('https://') && !url.endsWith('.git')) {
|
|
56
|
-
url = `${url}.git`;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// Derive name from repo URL
|
|
60
|
-
const name = basename(url, '.git');
|
|
61
|
-
|
|
62
|
-
return { url, version, name };
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// ============================================================================
|
|
66
|
-
// Git operations
|
|
67
|
-
// ============================================================================
|
|
68
|
-
|
|
69
|
-
function cloneRepo(url: string, dest: string, options: { shallow?: boolean; version?: string }): void {
|
|
70
|
-
const args = ['git', 'clone'];
|
|
71
|
-
if (options.shallow !== false) args.push('--depth', '1');
|
|
72
|
-
if (options.version) args.push('--branch', options.version);
|
|
73
|
-
args.push(url, dest);
|
|
74
|
-
|
|
75
|
-
execSync(args.join(' '), { stdio: 'pipe', timeout: 120_000 });
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function cleanupDir(dir: string): void {
|
|
79
|
-
if (existsSync(dir)) {
|
|
80
|
-
rmSync(dir, { recursive: true, force: true });
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// ============================================================================
|
|
85
|
-
// Type-specific registration
|
|
86
|
-
// ============================================================================
|
|
87
|
-
|
|
88
|
-
async function registerByType(manifest: GenieManifest, installPath: string): Promise<void> {
|
|
89
|
-
switch (manifest.type) {
|
|
90
|
-
case 'agent':
|
|
91
|
-
await regenerateAgentCache();
|
|
92
|
-
break;
|
|
93
|
-
case 'board':
|
|
94
|
-
await registerBoard(manifest);
|
|
95
|
-
break;
|
|
96
|
-
case 'workflow':
|
|
97
|
-
await registerWorkflow(manifest);
|
|
98
|
-
break;
|
|
99
|
-
case 'stack':
|
|
100
|
-
await installStack(manifest, installPath);
|
|
101
|
-
break;
|
|
102
|
-
// skill, app, template, hook — no extra registration needed beyond app_store
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
async function registerBoard(manifest: GenieManifest): Promise<void> {
|
|
107
|
-
if (!manifest.board?.stages) return;
|
|
108
|
-
if (!(await isAvailable())) return;
|
|
109
|
-
|
|
110
|
-
const sql = await getConnection();
|
|
111
|
-
const stages = manifest.board.stages.map((s, i) => ({
|
|
112
|
-
id: crypto.randomUUID(),
|
|
113
|
-
name: s.name,
|
|
114
|
-
label: s.label ?? s.name,
|
|
115
|
-
gate: s.gate,
|
|
116
|
-
action: s.action ?? null,
|
|
117
|
-
auto_advance: s.auto_advance ?? false,
|
|
118
|
-
roles: s.roles ?? ['*'],
|
|
119
|
-
color: s.color ?? '#94a3b8',
|
|
120
|
-
parallel: false,
|
|
121
|
-
on_fail: null,
|
|
122
|
-
position: i,
|
|
123
|
-
transitions: [],
|
|
124
|
-
}));
|
|
125
|
-
|
|
126
|
-
await sql`
|
|
127
|
-
INSERT INTO task_types (id, name, description, stages, is_builtin)
|
|
128
|
-
VALUES (
|
|
129
|
-
${manifest.name},
|
|
130
|
-
${manifest.name},
|
|
131
|
-
${manifest.description ?? null},
|
|
132
|
-
${sql.json(stages)},
|
|
133
|
-
false
|
|
134
|
-
)
|
|
135
|
-
ON CONFLICT (id) DO UPDATE SET
|
|
136
|
-
stages = EXCLUDED.stages,
|
|
137
|
-
description = EXCLUDED.description,
|
|
138
|
-
updated_at = now()
|
|
139
|
-
`;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
async function registerWorkflow(manifest: GenieManifest): Promise<void> {
|
|
143
|
-
if (!manifest.workflow) return;
|
|
144
|
-
if (!(await isAvailable())) return;
|
|
145
|
-
|
|
146
|
-
const sql = await getConnection();
|
|
147
|
-
const wf = manifest.workflow;
|
|
148
|
-
|
|
149
|
-
await sql`
|
|
150
|
-
INSERT INTO schedules (id, name, cron_expression, timezone, command, run_spec, status)
|
|
151
|
-
VALUES (
|
|
152
|
-
${`sched-${manifest.name}`},
|
|
153
|
-
${manifest.name},
|
|
154
|
-
${wf.cron},
|
|
155
|
-
${wf.timezone ?? 'UTC'},
|
|
156
|
-
${wf.command},
|
|
157
|
-
${sql.json(wf.run_spec ?? {})},
|
|
158
|
-
'active'
|
|
159
|
-
)
|
|
160
|
-
ON CONFLICT (id) DO UPDATE SET
|
|
161
|
-
cron_expression = EXCLUDED.cron_expression,
|
|
162
|
-
timezone = EXCLUDED.timezone,
|
|
163
|
-
command = EXCLUDED.command,
|
|
164
|
-
run_spec = EXCLUDED.run_spec,
|
|
165
|
-
updated_at = now()
|
|
166
|
-
`;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
async function handleInlineStackItem(
|
|
170
|
-
item: import('../lib/manifest.js').StackItem,
|
|
171
|
-
version: string,
|
|
172
|
-
tx: Awaited<ReturnType<typeof getConnection>>,
|
|
173
|
-
): Promise<void> {
|
|
174
|
-
if (item.type === 'board' && item.config) {
|
|
175
|
-
await registerBoard({
|
|
176
|
-
name: item.name,
|
|
177
|
-
type: 'board',
|
|
178
|
-
version,
|
|
179
|
-
board: { stages: (item.config.stages ?? []) as StageConfig[] },
|
|
180
|
-
});
|
|
181
|
-
} else if (item.type === 'workflow' && item.config) {
|
|
182
|
-
await registerWorkflow({
|
|
183
|
-
name: item.name,
|
|
184
|
-
type: 'workflow',
|
|
185
|
-
version,
|
|
186
|
-
workflow: item.config as unknown as NonNullable<GenieManifest['workflow']>,
|
|
187
|
-
});
|
|
188
|
-
}
|
|
189
|
-
await tx`
|
|
190
|
-
INSERT INTO app_store (name, item_type, version, manifest)
|
|
191
|
-
VALUES (${item.name}, ${item.type}, ${version}, ${tx.json(item.config ?? {})})
|
|
192
|
-
ON CONFLICT (name) DO NOTHING
|
|
193
|
-
`;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
async function handleExternalStackItem(
|
|
197
|
-
item: import('../lib/manifest.js').StackItem,
|
|
198
|
-
tx: Awaited<ReturnType<typeof getConnection>>,
|
|
199
|
-
): Promise<string> {
|
|
200
|
-
if (!item.source) throw new Error(`Stack item "${item.name}" is missing source`);
|
|
201
|
-
const parsed = parseInstallTarget(item.source);
|
|
202
|
-
const itemDir = join(ITEMS_DIR, parsed.name);
|
|
203
|
-
mkdirSync(ITEMS_DIR, { recursive: true });
|
|
204
|
-
cloneRepo(parsed.url, itemDir, { version: parsed.version });
|
|
205
|
-
|
|
206
|
-
const detection = await detectManifest(itemDir);
|
|
207
|
-
if ('error' in detection) {
|
|
208
|
-
throw new Error(`Stack item "${item.name}" (${item.source}): ${detection.error}`);
|
|
209
|
-
}
|
|
210
|
-
const validation = validateManifest(detection.manifest, itemDir);
|
|
211
|
-
if (!validation.valid) {
|
|
212
|
-
throw new Error(`Stack item "${item.name}" validation failed: ${validation.errors.join(', ')}`);
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
await tx`
|
|
216
|
-
INSERT INTO app_store (name, item_type, version, git_url, install_path, manifest)
|
|
217
|
-
VALUES (
|
|
218
|
-
${detection.manifest.name}, ${detection.manifest.type}, ${detection.manifest.version},
|
|
219
|
-
${item.source}, ${itemDir}, ${tx.json(detection.manifest)}
|
|
220
|
-
)
|
|
221
|
-
ON CONFLICT (name) DO NOTHING
|
|
222
|
-
`;
|
|
223
|
-
await registerByType(detection.manifest, itemDir);
|
|
224
|
-
return parsed.name;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
async function installStack(manifest: GenieManifest, _installPath: string): Promise<void> {
|
|
228
|
-
const stackItems = manifest.stack?.items;
|
|
229
|
-
if (!stackItems) return;
|
|
230
|
-
if (!(await isAvailable())) return;
|
|
231
|
-
|
|
232
|
-
const sql = await getConnection();
|
|
233
|
-
const installed: string[] = [];
|
|
234
|
-
|
|
235
|
-
try {
|
|
236
|
-
await sql.begin(async (tx: typeof sql) => {
|
|
237
|
-
for (const item of stackItems) {
|
|
238
|
-
if (item.inline) {
|
|
239
|
-
await handleInlineStackItem(item, manifest.version, tx);
|
|
240
|
-
installed.push(item.name);
|
|
241
|
-
} else if (item.source) {
|
|
242
|
-
const name = await handleExternalStackItem(item, tx);
|
|
243
|
-
installed.push(name);
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
});
|
|
247
|
-
} catch (err) {
|
|
248
|
-
// Rollback: clean up cloned dirs
|
|
249
|
-
for (const name of installed) {
|
|
250
|
-
cleanupDir(join(ITEMS_DIR, name));
|
|
251
|
-
await removeItemFromStore(name).catch(() => {});
|
|
252
|
-
}
|
|
253
|
-
throw err;
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
// ============================================================================
|
|
258
|
-
// Install command
|
|
259
|
-
// ============================================================================
|
|
260
|
-
|
|
261
|
-
interface InstallOptions {
|
|
262
|
-
force?: boolean;
|
|
263
|
-
full?: boolean;
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
async function handleInstall(target: string, options: InstallOptions): Promise<void> {
|
|
267
|
-
const parsed = parseInstallTarget(target);
|
|
268
|
-
const installDir = join(ITEMS_DIR, parsed.name);
|
|
269
|
-
|
|
270
|
-
// Check for name conflict
|
|
271
|
-
const existing = await getItemFromStore(parsed.name).catch(() => null);
|
|
272
|
-
if (existing && !options.force) {
|
|
273
|
-
console.error(`Item "${parsed.name}" is already installed. Use --force to override.`);
|
|
274
|
-
process.exit(1);
|
|
275
|
-
}
|
|
276
|
-
if (existing && options.force) {
|
|
277
|
-
await removeItemFromStore(parsed.name).catch(() => {});
|
|
278
|
-
cleanupDir(installDir);
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
// Clone
|
|
282
|
-
mkdirSync(ITEMS_DIR, { recursive: true });
|
|
283
|
-
console.log(`Cloning ${parsed.url}${parsed.version ? ` @ ${parsed.version}` : ''}...`);
|
|
284
|
-
try {
|
|
285
|
-
cloneRepo(parsed.url, installDir, { shallow: !options.full, version: parsed.version });
|
|
286
|
-
} catch (err) {
|
|
287
|
-
cleanupDir(installDir);
|
|
288
|
-
console.error(`Clone failed: ${(err as Error).message}`);
|
|
289
|
-
process.exit(1);
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
// Detect manifest
|
|
293
|
-
const detection = await detectManifest(installDir);
|
|
294
|
-
if ('error' in detection) {
|
|
295
|
-
cleanupDir(installDir);
|
|
296
|
-
console.error(`Manifest detection failed: ${detection.error}`);
|
|
297
|
-
process.exit(1);
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
const { manifest, source } = detection;
|
|
301
|
-
console.log(`Detected ${manifest.type} manifest from ${source}`);
|
|
302
|
-
|
|
303
|
-
// Validate
|
|
304
|
-
const validation = validateManifest(manifest, installDir);
|
|
305
|
-
for (const w of validation.warnings) {
|
|
306
|
-
console.log(` Warning: ${w}`);
|
|
307
|
-
}
|
|
308
|
-
if (!validation.valid) {
|
|
309
|
-
cleanupDir(installDir);
|
|
310
|
-
console.error(`Validation failed:\n${validation.errors.map((e) => ` - ${e}`).join('\n')}`);
|
|
311
|
-
process.exit(1);
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
// Register in app_store
|
|
315
|
-
try {
|
|
316
|
-
await registerItemInStore({
|
|
317
|
-
name: manifest.name,
|
|
318
|
-
itemType: manifest.type,
|
|
319
|
-
version: manifest.version,
|
|
320
|
-
description: manifest.description,
|
|
321
|
-
authorName: manifest.author?.name,
|
|
322
|
-
authorUrl: manifest.author?.url,
|
|
323
|
-
gitUrl: parsed.url,
|
|
324
|
-
installPath: installDir,
|
|
325
|
-
manifest: manifest as unknown as Record<string, unknown>,
|
|
326
|
-
tags: manifest.tags,
|
|
327
|
-
category: manifest.category,
|
|
328
|
-
license: manifest.license,
|
|
329
|
-
dependencies: manifest.dependencies,
|
|
330
|
-
});
|
|
331
|
-
} catch (err) {
|
|
332
|
-
cleanupDir(installDir);
|
|
333
|
-
console.error(`Registration failed: ${(err as Error).message}`);
|
|
334
|
-
process.exit(1);
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
// Type-specific registration
|
|
338
|
-
await registerByType(manifest, installDir);
|
|
339
|
-
|
|
340
|
-
// Audit
|
|
341
|
-
recordAuditEvent('item', manifest.name, 'item_installed', getActor(), {
|
|
342
|
-
type: manifest.type,
|
|
343
|
-
version: manifest.version,
|
|
344
|
-
source: parsed.url,
|
|
345
|
-
manifestSource: source,
|
|
346
|
-
}).catch(() => {});
|
|
347
|
-
|
|
348
|
-
console.log(`\nInstalled ${manifest.type} "${manifest.name}" v${manifest.version}`);
|
|
349
|
-
console.log(` Source: ${parsed.url}`);
|
|
350
|
-
console.log(` Path: ${installDir}`);
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
// ============================================================================
|
|
354
|
-
// Command registration
|
|
355
|
-
// ============================================================================
|
|
356
|
-
|
|
357
|
-
export function registerInstallCommand(program: Command): void {
|
|
358
|
-
program
|
|
359
|
-
.command('install <target>')
|
|
360
|
-
.description('Install a genie item from a git URL (e.g. github.com/user/repo[@version])')
|
|
361
|
-
.option('--force', 'Override existing item with same name')
|
|
362
|
-
.option('--full', 'Full git clone instead of shallow')
|
|
363
|
-
.action(async (target: string, options: InstallOptions) => {
|
|
364
|
-
try {
|
|
365
|
-
await handleInstall(target, options);
|
|
366
|
-
} catch (error) {
|
|
367
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
368
|
-
console.error(`Error: ${message}`);
|
|
369
|
-
process.exit(1);
|
|
370
|
-
}
|
|
371
|
-
});
|
|
372
|
-
}
|
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Uninstall Command — `genie uninstall <name>`
|
|
3
|
-
*
|
|
4
|
-
* Removes a previously installed item: deregisters from app_store,
|
|
5
|
-
* performs type-specific cleanup, and removes the cloned directory.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { existsSync, readFileSync, rmSync } from 'node:fs';
|
|
9
|
-
import { homedir } from 'node:os';
|
|
10
|
-
import { join } from 'node:path';
|
|
11
|
-
import type { Command } from 'commander';
|
|
12
|
-
import { getItemFromStore, regenerateAgentCache, removeItemFromStore } from '../lib/agent-cache.js';
|
|
13
|
-
import { getActor, recordAuditEvent } from '../lib/audit.js';
|
|
14
|
-
import { getConnection, isAvailable } from '../lib/db.js';
|
|
15
|
-
|
|
16
|
-
const GENIE_HOME = process.env.GENIE_HOME ?? join(homedir(), '.genie');
|
|
17
|
-
const ITEMS_DIR = join(GENIE_HOME, 'items');
|
|
18
|
-
|
|
19
|
-
// ============================================================================
|
|
20
|
-
// Type-specific deregistration
|
|
21
|
-
// ============================================================================
|
|
22
|
-
|
|
23
|
-
async function deregisterByType(itemType: string, name: string): Promise<void> {
|
|
24
|
-
if (!(await isAvailable())) return;
|
|
25
|
-
const sql = await getConnection();
|
|
26
|
-
|
|
27
|
-
switch (itemType) {
|
|
28
|
-
case 'agent':
|
|
29
|
-
// Remove from legacy agents table too
|
|
30
|
-
await sql`DELETE FROM agents WHERE id = ${`dir:${name}`}`.catch(() => {});
|
|
31
|
-
await regenerateAgentCache();
|
|
32
|
-
break;
|
|
33
|
-
case 'board':
|
|
34
|
-
await sql`DELETE FROM task_types WHERE id = ${name}`.catch(() => {});
|
|
35
|
-
break;
|
|
36
|
-
case 'workflow':
|
|
37
|
-
await sql`DELETE FROM schedules WHERE id = ${`sched-${name}`}`.catch(() => {});
|
|
38
|
-
break;
|
|
39
|
-
case 'app':
|
|
40
|
-
await sql`DELETE FROM installed_apps WHERE app_store_id IN (
|
|
41
|
-
SELECT id FROM app_store WHERE name = ${name}
|
|
42
|
-
)`.catch(() => {});
|
|
43
|
-
break;
|
|
44
|
-
case 'stack': {
|
|
45
|
-
// Try to read manifest and uninstall sub-items
|
|
46
|
-
const manifestPath = join(ITEMS_DIR, name, 'genie.yaml');
|
|
47
|
-
if (existsSync(manifestPath)) {
|
|
48
|
-
try {
|
|
49
|
-
const yaml = await import('js-yaml');
|
|
50
|
-
const raw = yaml.load(readFileSync(manifestPath, 'utf-8')) as Record<string, unknown>;
|
|
51
|
-
const stack = raw.stack as { items?: Array<{ name: string; type: string }> } | undefined;
|
|
52
|
-
if (stack?.items) {
|
|
53
|
-
for (const item of stack.items) {
|
|
54
|
-
await deregisterByType(item.type, item.name);
|
|
55
|
-
await removeItemFromStore(item.name).catch(() => {});
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
} catch {
|
|
59
|
-
// Best effort
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
break;
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// ============================================================================
|
|
68
|
-
// Uninstall handler
|
|
69
|
-
// ============================================================================
|
|
70
|
-
|
|
71
|
-
async function handleUninstall(name: string): Promise<void> {
|
|
72
|
-
const existing = await getItemFromStore(name).catch(() => null);
|
|
73
|
-
if (!existing) {
|
|
74
|
-
console.error(`Item "${name}" is not installed.`);
|
|
75
|
-
process.exit(1);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
const itemType = existing.item_type;
|
|
79
|
-
|
|
80
|
-
// Type-specific deregistration
|
|
81
|
-
await deregisterByType(itemType, name);
|
|
82
|
-
|
|
83
|
-
// Remove from app_store
|
|
84
|
-
await removeItemFromStore(name);
|
|
85
|
-
|
|
86
|
-
// Remove cloned directory
|
|
87
|
-
const installDir = join(ITEMS_DIR, name);
|
|
88
|
-
if (existsSync(installDir)) {
|
|
89
|
-
rmSync(installDir, { recursive: true, force: true });
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// Audit
|
|
93
|
-
recordAuditEvent('item', name, 'item_uninstalled', getActor(), {
|
|
94
|
-
type: itemType,
|
|
95
|
-
version: existing.version,
|
|
96
|
-
}).catch(() => {});
|
|
97
|
-
|
|
98
|
-
console.log(`Uninstalled ${itemType} "${name}".`);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// ============================================================================
|
|
102
|
-
// Command registration
|
|
103
|
-
// ============================================================================
|
|
104
|
-
|
|
105
|
-
export function registerItemUninstallCommand(parent: Command): void {
|
|
106
|
-
parent
|
|
107
|
-
.command('uninstall <name>')
|
|
108
|
-
.description('Remove an installed genie item')
|
|
109
|
-
.action(async (name: string) => {
|
|
110
|
-
try {
|
|
111
|
-
await handleUninstall(name);
|
|
112
|
-
} catch (error) {
|
|
113
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
114
|
-
console.error(`Error: ${message}`);
|
|
115
|
-
process.exit(1);
|
|
116
|
-
}
|
|
117
|
-
});
|
|
118
|
-
}
|
|
@@ -1,205 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Update Command — `genie update <name>` or `genie update --all`
|
|
3
|
-
*
|
|
4
|
-
* Pulls the latest version (or a specific tag) of an installed item,
|
|
5
|
-
* re-validates its manifest, and updates the app_store entry.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { execSync } from 'node:child_process';
|
|
9
|
-
import { existsSync } from 'node:fs';
|
|
10
|
-
import { homedir } from 'node:os';
|
|
11
|
-
import { join } from 'node:path';
|
|
12
|
-
import type { Command } from 'commander';
|
|
13
|
-
import { getItemFromStore, listItemsFromStore, regenerateAgentCache, updateItemInStore } from '../lib/agent-cache.js';
|
|
14
|
-
import { getActor, recordAuditEvent } from '../lib/audit.js';
|
|
15
|
-
import { getConnection, isAvailable } from '../lib/db.js';
|
|
16
|
-
import { type GenieManifest, detectManifest, validateManifest } from '../lib/manifest.js';
|
|
17
|
-
|
|
18
|
-
const GENIE_HOME = process.env.GENIE_HOME ?? join(homedir(), '.genie');
|
|
19
|
-
const ITEMS_DIR = join(GENIE_HOME, 'items');
|
|
20
|
-
|
|
21
|
-
// ============================================================================
|
|
22
|
-
// Type-specific re-registration
|
|
23
|
-
// ============================================================================
|
|
24
|
-
|
|
25
|
-
async function reregisterByType(manifest: GenieManifest): Promise<void> {
|
|
26
|
-
if (!(await isAvailable())) return;
|
|
27
|
-
const sql = await getConnection();
|
|
28
|
-
|
|
29
|
-
switch (manifest.type) {
|
|
30
|
-
case 'agent':
|
|
31
|
-
await regenerateAgentCache();
|
|
32
|
-
break;
|
|
33
|
-
case 'board':
|
|
34
|
-
if (manifest.board?.stages) {
|
|
35
|
-
const stages = manifest.board.stages.map((s, i) => ({
|
|
36
|
-
id: crypto.randomUUID(),
|
|
37
|
-
name: s.name,
|
|
38
|
-
label: s.label ?? s.name,
|
|
39
|
-
gate: s.gate,
|
|
40
|
-
action: s.action ?? null,
|
|
41
|
-
auto_advance: s.auto_advance ?? false,
|
|
42
|
-
roles: s.roles ?? ['*'],
|
|
43
|
-
color: s.color ?? '#94a3b8',
|
|
44
|
-
parallel: false,
|
|
45
|
-
on_fail: null,
|
|
46
|
-
position: i,
|
|
47
|
-
transitions: [],
|
|
48
|
-
}));
|
|
49
|
-
await sql`
|
|
50
|
-
UPDATE task_types SET stages = ${sql.json(stages)}, updated_at = now()
|
|
51
|
-
WHERE id = ${manifest.name}
|
|
52
|
-
`.catch(() => {});
|
|
53
|
-
}
|
|
54
|
-
break;
|
|
55
|
-
case 'workflow':
|
|
56
|
-
if (manifest.workflow) {
|
|
57
|
-
await sql`
|
|
58
|
-
UPDATE schedules SET
|
|
59
|
-
cron_expression = ${manifest.workflow.cron},
|
|
60
|
-
timezone = ${manifest.workflow.timezone ?? 'UTC'},
|
|
61
|
-
command = ${manifest.workflow.command},
|
|
62
|
-
run_spec = ${sql.json(manifest.workflow.run_spec ?? {})},
|
|
63
|
-
updated_at = now()
|
|
64
|
-
WHERE id = ${`sched-${manifest.name}`}
|
|
65
|
-
`.catch(() => {});
|
|
66
|
-
}
|
|
67
|
-
break;
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// ============================================================================
|
|
72
|
-
// Update handler
|
|
73
|
-
// ============================================================================
|
|
74
|
-
|
|
75
|
-
interface UpdateOptions {
|
|
76
|
-
all?: boolean;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
async function handleUpdateSingle(name: string, version?: string): Promise<boolean> {
|
|
80
|
-
const existing = await getItemFromStore(name).catch(() => null);
|
|
81
|
-
if (!existing) {
|
|
82
|
-
console.error(`Item "${name}" is not installed.`);
|
|
83
|
-
return false;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
const installDir = existing.install_path ?? join(ITEMS_DIR, name);
|
|
87
|
-
if (!existsSync(installDir)) {
|
|
88
|
-
console.error(`Install directory not found: ${installDir}`);
|
|
89
|
-
return false;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// Pull latest or checkout specific version
|
|
93
|
-
try {
|
|
94
|
-
if (version) {
|
|
95
|
-
execSync(`git fetch --tags && git checkout ${version}`, {
|
|
96
|
-
cwd: installDir,
|
|
97
|
-
stdio: 'pipe',
|
|
98
|
-
timeout: 60_000,
|
|
99
|
-
});
|
|
100
|
-
} else {
|
|
101
|
-
execSync('git pull --ff-only', {
|
|
102
|
-
cwd: installDir,
|
|
103
|
-
stdio: 'pipe',
|
|
104
|
-
timeout: 60_000,
|
|
105
|
-
});
|
|
106
|
-
}
|
|
107
|
-
} catch (err) {
|
|
108
|
-
console.error(`Git update failed for "${name}": ${(err as Error).message}`);
|
|
109
|
-
return false;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// Re-detect and validate manifest
|
|
113
|
-
const detection = await detectManifest(installDir);
|
|
114
|
-
if ('error' in detection) {
|
|
115
|
-
console.error(`Manifest detection failed after update: ${detection.error}`);
|
|
116
|
-
return false;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
const { manifest } = detection;
|
|
120
|
-
const validation = validateManifest(manifest, installDir);
|
|
121
|
-
for (const w of validation.warnings) {
|
|
122
|
-
console.log(` Warning: ${w}`);
|
|
123
|
-
}
|
|
124
|
-
if (!validation.valid) {
|
|
125
|
-
console.error(`Validation failed after update:\n${validation.errors.map((e) => ` - ${e}`).join('\n')}`);
|
|
126
|
-
return false;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// Update app_store
|
|
130
|
-
await updateItemInStore(name, {
|
|
131
|
-
version: manifest.version,
|
|
132
|
-
description: manifest.description,
|
|
133
|
-
manifest: manifest as unknown as Record<string, unknown>,
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
// Type-specific re-registration
|
|
137
|
-
await reregisterByType(manifest);
|
|
138
|
-
|
|
139
|
-
// Audit
|
|
140
|
-
recordAuditEvent('item', name, 'item_updated', getActor(), {
|
|
141
|
-
type: manifest.type,
|
|
142
|
-
version: manifest.version,
|
|
143
|
-
previousVersion: existing.version,
|
|
144
|
-
}).catch(() => {});
|
|
145
|
-
|
|
146
|
-
console.log(`Updated ${manifest.type} "${name}" → v${manifest.version}`);
|
|
147
|
-
return true;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
async function handleUpdate(nameOrVersion: string | undefined, options: UpdateOptions): Promise<void> {
|
|
151
|
-
if (options.all) {
|
|
152
|
-
const items = await listItemsFromStore();
|
|
153
|
-
const gitItems = items.filter((i) => i.git_url);
|
|
154
|
-
if (gitItems.length === 0) {
|
|
155
|
-
console.log('No git-installed items to update.');
|
|
156
|
-
return;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
console.log(`Updating ${gitItems.length} item(s)...`);
|
|
160
|
-
let updated = 0;
|
|
161
|
-
for (const item of gitItems) {
|
|
162
|
-
const ok = await handleUpdateSingle(item.name);
|
|
163
|
-
if (ok) updated++;
|
|
164
|
-
}
|
|
165
|
-
console.log(`\n${updated}/${gitItems.length} items updated successfully.`);
|
|
166
|
-
return;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
if (!nameOrVersion) {
|
|
170
|
-
console.error('Usage: genie update <name>[@version] or genie update --all');
|
|
171
|
-
process.exit(1);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
// Parse name@version
|
|
175
|
-
let name = nameOrVersion;
|
|
176
|
-
let version: string | undefined;
|
|
177
|
-
const atIdx = name.lastIndexOf('@');
|
|
178
|
-
if (atIdx > 0) {
|
|
179
|
-
version = name.slice(atIdx + 1);
|
|
180
|
-
name = name.slice(0, atIdx);
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
const ok = await handleUpdateSingle(name, version);
|
|
184
|
-
if (!ok) process.exit(1);
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
// ============================================================================
|
|
188
|
-
// Command registration
|
|
189
|
-
// ============================================================================
|
|
190
|
-
|
|
191
|
-
export function registerItemUpdateCommand(parent: Command): void {
|
|
192
|
-
parent
|
|
193
|
-
.command('update [name]')
|
|
194
|
-
.description('Update an installed item to the latest version or a specific tag')
|
|
195
|
-
.option('--all', 'Update all git-installed items')
|
|
196
|
-
.action(async (name: string | undefined, options: UpdateOptions) => {
|
|
197
|
-
try {
|
|
198
|
-
await handleUpdate(name, options);
|
|
199
|
-
} catch (error) {
|
|
200
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
201
|
-
console.error(`Error: ${message}`);
|
|
202
|
-
process.exit(1);
|
|
203
|
-
}
|
|
204
|
-
});
|
|
205
|
-
}
|