@automagik/genie 4.260331.6 → 4.260331.8
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/dist/genie.js +134 -253
- 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/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 +0 -11
- 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/lib/task-service.ts +4 -0
- package/src/term-commands/agent/directory.ts +2 -37
- package/src/term-commands/dir.ts +3 -68
- package/src/term-commands/export.ts +0 -13
- package/src/term-commands/team.ts +14 -11
- 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
package/src/term-commands/dir.ts
CHANGED
|
@@ -12,22 +12,11 @@
|
|
|
12
12
|
* Commands:
|
|
13
13
|
* genie agent register <name> — Register agent locally + auto-register in Omni
|
|
14
14
|
*
|
|
15
|
-
* Storage:
|
|
16
|
-
* Legacy `agents` table kept for backward compat with spawn.
|
|
17
|
-
* JSON cache (~/.genie/agent-directory.json) regenerated after every mutation.
|
|
15
|
+
* Storage: agent-directory.json is the source of truth for registered agents.
|
|
18
16
|
*/
|
|
19
17
|
|
|
20
18
|
import { resolve as resolvePath } from 'node:path';
|
|
21
19
|
import type { Command } from 'commander';
|
|
22
|
-
import {
|
|
23
|
-
type StoreRow,
|
|
24
|
-
listItemsFromStore,
|
|
25
|
-
migrateAgentDirectory,
|
|
26
|
-
regenerateAgentCache,
|
|
27
|
-
registerItemInStore,
|
|
28
|
-
removeItemFromStore,
|
|
29
|
-
updateItemInStore,
|
|
30
|
-
} from '../lib/agent-cache.js';
|
|
31
20
|
import * as directory from '../lib/agent-directory.js';
|
|
32
21
|
import { printSyncResult, syncAgentDirectory } from '../lib/agent-sync.js';
|
|
33
22
|
import { getActor, recordAuditEvent } from '../lib/audit.js';
|
|
@@ -65,9 +54,6 @@ export function registerDirNamespace(program: Command): void {
|
|
|
65
54
|
.action(async (name: string, options: { global?: boolean }) => {
|
|
66
55
|
try {
|
|
67
56
|
const removed = await directory.rm(name, { global: options.global });
|
|
68
|
-
// Also remove from app_store
|
|
69
|
-
await removeItemFromStore(name).catch(() => {});
|
|
70
|
-
await regenerateAgentCache();
|
|
71
57
|
recordAuditEvent('item', name, 'item_removed', getActor(), { type: 'agent', source: 'dir_rm' }).catch(() => {});
|
|
72
58
|
|
|
73
59
|
if (removed) {
|
|
@@ -164,18 +150,6 @@ async function handleDirAdd(name: string, options: DirAddOptions): Promise<void>
|
|
|
164
150
|
{ global: options.global },
|
|
165
151
|
);
|
|
166
152
|
|
|
167
|
-
// Also register in app_store (primary source of truth)
|
|
168
|
-
try {
|
|
169
|
-
await registerItemInStore({
|
|
170
|
-
name,
|
|
171
|
-
itemType: 'agent',
|
|
172
|
-
installPath: resolvedDir,
|
|
173
|
-
manifest: { promptMode, model: options.model, roles: normalizeRoles(options.roles), repo: options.repo },
|
|
174
|
-
});
|
|
175
|
-
} catch {
|
|
176
|
-
// Best-effort — legacy agents table is still the spawn path
|
|
177
|
-
}
|
|
178
|
-
await regenerateAgentCache();
|
|
179
153
|
recordAuditEvent('item', name, 'item_registered', getActor(), { type: 'agent', source: 'dir_add' }).catch(() => {});
|
|
180
154
|
|
|
181
155
|
const scope = options.global ? 'global' : 'project';
|
|
@@ -207,16 +181,6 @@ async function handleEdit(name: string, options: EditOptions): Promise<void> {
|
|
|
207
181
|
|
|
208
182
|
const entry = await directory.edit(name, updates, { global: options.global });
|
|
209
183
|
|
|
210
|
-
// Also update app_store
|
|
211
|
-
try {
|
|
212
|
-
await updateItemInStore(name, {
|
|
213
|
-
installPath: updates.dir,
|
|
214
|
-
manifest: { promptMode: updates.promptMode, model: updates.model, roles: updates.roles, repo: updates.repo },
|
|
215
|
-
});
|
|
216
|
-
} catch {
|
|
217
|
-
// Best-effort
|
|
218
|
-
}
|
|
219
|
-
await regenerateAgentCache();
|
|
220
184
|
recordAuditEvent('item', name, 'item_updated', getActor(), { type: 'agent', source: 'dir_edit' }).catch(() => {});
|
|
221
185
|
|
|
222
186
|
const scope = options.global ? 'global' : 'project';
|
|
@@ -279,37 +243,8 @@ async function showEntry(name: string, json?: boolean): Promise<void> {
|
|
|
279
243
|
console.log('');
|
|
280
244
|
}
|
|
281
245
|
|
|
282
|
-
async function listEntries(json?: boolean, includeBuiltins?: boolean,
|
|
283
|
-
|
|
284
|
-
await migrateAgentDirectory().catch(() => {});
|
|
285
|
-
|
|
286
|
-
// Try app_store first (primary), fall back to legacy agents table
|
|
287
|
-
let entries: directory.ScopedDirectoryEntry[];
|
|
288
|
-
try {
|
|
289
|
-
const storeItems = await listItemsFromStore('agent');
|
|
290
|
-
entries = storeItems
|
|
291
|
-
.filter((item: StoreRow) => {
|
|
292
|
-
if (includeArchived) return true;
|
|
293
|
-
const manifest = (item.manifest ?? {}) as Record<string, unknown>;
|
|
294
|
-
return !manifest.archived;
|
|
295
|
-
})
|
|
296
|
-
.map((item: StoreRow) => {
|
|
297
|
-
const manifest = (item.manifest ?? {}) as Record<string, unknown>;
|
|
298
|
-
return {
|
|
299
|
-
name: item.name,
|
|
300
|
-
dir: (item.install_path as string) ?? '',
|
|
301
|
-
repo: (manifest.repo as string) ?? '',
|
|
302
|
-
promptMode: ((manifest.promptMode as string) ?? 'append') as directory.PromptMode,
|
|
303
|
-
model: manifest.model as string | undefined,
|
|
304
|
-
roles: normalizeRoles(manifest.roles as string[] | undefined),
|
|
305
|
-
registeredAt: item.installed_at as string,
|
|
306
|
-
scope: (manifest.archived ? 'archived' : 'global') as directory.DirectoryScope,
|
|
307
|
-
};
|
|
308
|
-
});
|
|
309
|
-
} catch {
|
|
310
|
-
// Fallback to legacy
|
|
311
|
-
entries = await directory.ls();
|
|
312
|
-
}
|
|
246
|
+
async function listEntries(json?: boolean, includeBuiltins?: boolean, _includeArchived?: boolean): Promise<void> {
|
|
247
|
+
const entries = await directory.ls();
|
|
313
248
|
|
|
314
249
|
if (json) {
|
|
315
250
|
listEntriesJson(entries, includeBuiltins);
|
|
@@ -9,7 +9,6 @@
|
|
|
9
9
|
* genie export projects — Projects
|
|
10
10
|
* genie export schedules [name] — Schedules with run_spec
|
|
11
11
|
* genie export agents — Agents, templates, checkpoints
|
|
12
|
-
* genie export apps — App store (KhalOS, graceful skip)
|
|
13
12
|
* genie export comms — Conversations, messages, mailbox
|
|
14
13
|
* genie export config — OS config (KhalOS, graceful skip)
|
|
15
14
|
*/
|
|
@@ -374,18 +373,6 @@ export function registerExportCommands(program: Command): void {
|
|
|
374
373
|
},
|
|
375
374
|
);
|
|
376
375
|
|
|
377
|
-
// genie export apps
|
|
378
|
-
sharedOpts(exp.command('apps').description('Export app store (graceful skip if missing)')).action(
|
|
379
|
-
async (options: ExportOptions) => {
|
|
380
|
-
try {
|
|
381
|
-
await runExport(['apps'], 'partial', (sql) => exportGroup(sql, 'apps'), options);
|
|
382
|
-
} catch (error) {
|
|
383
|
-
console.error(`Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
384
|
-
process.exit(1);
|
|
385
|
-
}
|
|
386
|
-
},
|
|
387
|
-
);
|
|
388
|
-
|
|
389
376
|
// genie export comms
|
|
390
377
|
sharedOpts(exp.command('comms').description('Export conversations, messages, mailbox')).action(
|
|
391
378
|
async (options: ExportOptions) => {
|
|
@@ -336,8 +336,11 @@ async function spawnLeaderWithWish(
|
|
|
336
336
|
config.tmuxSessionName = tmuxSession;
|
|
337
337
|
await teamManager.updateTeamConfig(config.name, config);
|
|
338
338
|
|
|
339
|
-
//
|
|
340
|
-
|
|
339
|
+
// Resolve leader from project's leader_agent, spawner = caller identity
|
|
340
|
+
const { getProjectByRepoPath } = await import('../lib/task-service.js');
|
|
341
|
+
const project = await getProjectByRepoPath(resolvedRepo);
|
|
342
|
+
const leaderAgent = project?.leaderAgent || slug;
|
|
343
|
+
config.leader = leaderAgent;
|
|
341
344
|
config.spawner = process.env.GENIE_AGENT_NAME || 'cli';
|
|
342
345
|
await teamManager.updateTeamConfig(config.name, config);
|
|
343
346
|
|
|
@@ -355,21 +358,21 @@ async function spawnLeaderWithWish(
|
|
|
355
358
|
await copyFile(sourceWishPath, destWishPath);
|
|
356
359
|
console.log(` Wish: copied ${slug}/WISH.md into worktree`);
|
|
357
360
|
|
|
358
|
-
// Hire the standard team: leader + engineer + reviewer + qa + fix
|
|
359
|
-
const
|
|
360
|
-
const standardTeam = [leaderName, 'engineer', 'reviewer', 'qa', 'fix'];
|
|
361
|
+
// Hire the standard team: leader (by agent name) + engineer + reviewer + qa + fix
|
|
362
|
+
const standardTeam = [leaderAgent, 'engineer', 'reviewer', 'qa', 'fix'];
|
|
361
363
|
for (const role of standardTeam) {
|
|
362
364
|
await teamManager.hireAgent(config.name, role);
|
|
363
365
|
}
|
|
364
366
|
console.log(` Team: hired ${standardTeam.join(', ')}`);
|
|
365
367
|
|
|
366
|
-
// Spawn leader —
|
|
367
|
-
const members = standardTeam.filter((r) => r !==
|
|
368
|
+
// Spawn leader — resolve agent definition from leaderAgent, use slug as role identity
|
|
369
|
+
const members = standardTeam.filter((r) => r !== leaderAgent).join(', ');
|
|
368
370
|
const spawner = config.spawner || 'cli';
|
|
369
371
|
const kickoffPrompt = `Your team is "${config.name}". Repo: ${config.repo}. Branch: ${config.name}. Worktree: ${config.worktreePath}. Wish slug: ${slug}. Your team members are: ${members} (already hired — genie work will spawn them automatically). Report completion to: ${spawner} (via genie send --to ${spawner}). Read the wish at .genie/wishes/${slug}/WISH.md and execute the full lifecycle autonomously.`;
|
|
370
|
-
await handleWorkerSpawn(
|
|
372
|
+
await handleWorkerSpawn(leaderAgent, {
|
|
371
373
|
provider: 'claude',
|
|
372
374
|
team: config.name,
|
|
375
|
+
role: slug,
|
|
373
376
|
cwd: config.worktreePath,
|
|
374
377
|
session: tmuxSession,
|
|
375
378
|
initialPrompt: kickoffPrompt,
|
|
@@ -377,11 +380,11 @@ async function spawnLeaderWithWish(
|
|
|
377
380
|
|
|
378
381
|
// Deliver kickoff prompt via mailbox as backup (durable, queued to disk)
|
|
379
382
|
const protocolRouter = await import('../lib/protocol-router.js');
|
|
380
|
-
const result = await protocolRouter.sendMessage(config.worktreePath, 'cli',
|
|
383
|
+
const result = await protocolRouter.sendMessage(config.worktreePath, 'cli', leaderAgent, kickoffPrompt);
|
|
381
384
|
if (!result.delivered) {
|
|
382
|
-
console.warn(`⚠ Backup delivery to ${
|
|
385
|
+
console.warn(`⚠ Backup delivery to ${leaderAgent} failed: ${result.reason ?? 'unknown'}`);
|
|
383
386
|
}
|
|
384
|
-
console.log(
|
|
387
|
+
console.log(` Leader: ${leaderAgent} spawned as ${slug}`);
|
|
385
388
|
}
|
|
386
389
|
|
|
387
390
|
// ============================================================================
|
package/src/lib/agent-cache.ts
DELETED
|
@@ -1,282 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Agent Cache — Maintains agent-directory.json as a cache of app_store agents.
|
|
3
|
-
*
|
|
4
|
-
* Also provides generic CRUD helpers for the `app_store` table (register,
|
|
5
|
-
* remove, update, get, list). The cache file is a denormalised snapshot
|
|
6
|
-
* written to GENIE_HOME so that non-PG consumers (hooks, shell scripts)
|
|
7
|
-
* can read agent metadata without a database connection.
|
|
8
|
-
*
|
|
9
|
-
* Best-effort: DB failures never block the CLI.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import { existsSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
13
|
-
import { homedir } from 'node:os';
|
|
14
|
-
import { join } from 'node:path';
|
|
15
|
-
import { getActor, recordAuditEvent } from './audit.js';
|
|
16
|
-
import { getConnection, isAvailable } from './db.js';
|
|
17
|
-
|
|
18
|
-
// ============================================================================
|
|
19
|
-
// Constants
|
|
20
|
-
// ============================================================================
|
|
21
|
-
|
|
22
|
-
const GENIE_HOME = process.env.GENIE_HOME ?? join(homedir(), '.genie');
|
|
23
|
-
const CACHE_FILE = 'agent-directory.json';
|
|
24
|
-
const CACHE_BACKUP = 'agent-directory.json.bak';
|
|
25
|
-
|
|
26
|
-
// ============================================================================
|
|
27
|
-
// Types
|
|
28
|
-
// ============================================================================
|
|
29
|
-
|
|
30
|
-
/** Denormalised cache entry written to agent-directory.json. */
|
|
31
|
-
interface CacheEntry {
|
|
32
|
-
name: string;
|
|
33
|
-
dir: string;
|
|
34
|
-
repo?: string;
|
|
35
|
-
promptMode: string;
|
|
36
|
-
model?: string;
|
|
37
|
-
roles?: string[];
|
|
38
|
-
registeredAt: string;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/** Row shape returned by SELECT * FROM app_store. */
|
|
42
|
-
export interface StoreRow {
|
|
43
|
-
id: string;
|
|
44
|
-
name: string;
|
|
45
|
-
item_type: string;
|
|
46
|
-
version: string;
|
|
47
|
-
description: string | null;
|
|
48
|
-
author_name: string | null;
|
|
49
|
-
author_url: string | null;
|
|
50
|
-
git_url: string | null;
|
|
51
|
-
install_path: string | null;
|
|
52
|
-
manifest: Record<string, unknown>;
|
|
53
|
-
approval_status: string;
|
|
54
|
-
tags: string[];
|
|
55
|
-
category: string | null;
|
|
56
|
-
license: string | null;
|
|
57
|
-
dependencies: string[];
|
|
58
|
-
installed_at: string;
|
|
59
|
-
updated_at: string;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/** Insert payload for registerItemInStore. */
|
|
63
|
-
interface StoreInsert {
|
|
64
|
-
name: string;
|
|
65
|
-
itemType: string;
|
|
66
|
-
version?: string;
|
|
67
|
-
description?: string;
|
|
68
|
-
authorName?: string;
|
|
69
|
-
authorUrl?: string;
|
|
70
|
-
gitUrl?: string;
|
|
71
|
-
installPath?: string;
|
|
72
|
-
manifest?: Record<string, unknown>;
|
|
73
|
-
tags?: string[];
|
|
74
|
-
category?: string;
|
|
75
|
-
license?: string;
|
|
76
|
-
dependencies?: string[];
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// ============================================================================
|
|
80
|
-
// Cache regeneration
|
|
81
|
-
// ============================================================================
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Rebuild agent-directory.json from all `item_type = 'agent'` rows in app_store.
|
|
85
|
-
*
|
|
86
|
-
* Silently returns if the database is unavailable — the stale cache (if any)
|
|
87
|
-
* remains on disk until the next successful regeneration.
|
|
88
|
-
*/
|
|
89
|
-
export async function regenerateAgentCache(): Promise<void> {
|
|
90
|
-
try {
|
|
91
|
-
if (!(await isAvailable())) return;
|
|
92
|
-
|
|
93
|
-
const sql = await getConnection();
|
|
94
|
-
const rows = await sql`
|
|
95
|
-
SELECT name, install_path, manifest, installed_at
|
|
96
|
-
FROM app_store
|
|
97
|
-
WHERE item_type = 'agent'
|
|
98
|
-
ORDER BY name
|
|
99
|
-
`;
|
|
100
|
-
|
|
101
|
-
const entries: CacheEntry[] = rows.map((r: Record<string, unknown>) => {
|
|
102
|
-
const manifest = (r.manifest ?? {}) as Record<string, unknown>;
|
|
103
|
-
const entry: CacheEntry = {
|
|
104
|
-
name: r.name as string,
|
|
105
|
-
dir: (r.install_path as string) ?? '',
|
|
106
|
-
promptMode: (manifest.promptMode as string) ?? 'append',
|
|
107
|
-
registeredAt: r.installed_at ? new Date(r.installed_at as string).toISOString() : new Date().toISOString(),
|
|
108
|
-
};
|
|
109
|
-
if (manifest.repo) entry.repo = manifest.repo as string;
|
|
110
|
-
if (manifest.model) entry.model = manifest.model as string;
|
|
111
|
-
if (Array.isArray(manifest.roles) && manifest.roles.length > 0) {
|
|
112
|
-
entry.roles = manifest.roles as string[];
|
|
113
|
-
}
|
|
114
|
-
return entry;
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
writeFileSync(join(GENIE_HOME, CACHE_FILE), JSON.stringify(entries, null, 2));
|
|
118
|
-
} catch {
|
|
119
|
-
// Best effort — never block the CLI on cache regeneration failure
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// ============================================================================
|
|
124
|
-
// One-time migration from JSON → PG
|
|
125
|
-
// ============================================================================
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Migrate the legacy agent-directory.json into the app_store table.
|
|
129
|
-
*
|
|
130
|
-
* Idempotent: skips if the backup file already exists (meaning migration
|
|
131
|
-
* already ran) or if the source file is missing (nothing to migrate).
|
|
132
|
-
* Uses ON CONFLICT (name) DO NOTHING so partially-completed runs are safe.
|
|
133
|
-
*/
|
|
134
|
-
export async function migrateAgentDirectory(): Promise<void> {
|
|
135
|
-
const sourcePath = join(GENIE_HOME, CACHE_FILE);
|
|
136
|
-
const backupPath = join(GENIE_HOME, CACHE_BACKUP);
|
|
137
|
-
|
|
138
|
-
// Already migrated or nothing to migrate
|
|
139
|
-
if (existsSync(backupPath) || !existsSync(sourcePath)) return;
|
|
140
|
-
|
|
141
|
-
try {
|
|
142
|
-
const raw = readFileSync(sourcePath, 'utf-8');
|
|
143
|
-
const entries: CacheEntry[] = JSON.parse(raw);
|
|
144
|
-
if (!Array.isArray(entries) || entries.length === 0) return;
|
|
145
|
-
|
|
146
|
-
const sql = await getConnection();
|
|
147
|
-
|
|
148
|
-
for (const entry of entries) {
|
|
149
|
-
const manifest: Record<string, unknown> = {};
|
|
150
|
-
if (entry.promptMode) manifest.promptMode = entry.promptMode;
|
|
151
|
-
if (entry.model) manifest.model = entry.model;
|
|
152
|
-
if (entry.roles) manifest.roles = entry.roles;
|
|
153
|
-
if (entry.repo) manifest.repo = entry.repo;
|
|
154
|
-
|
|
155
|
-
await sql`
|
|
156
|
-
INSERT INTO app_store (name, item_type, version, install_path, manifest)
|
|
157
|
-
VALUES (${entry.name}, 'agent', '0.0.0', ${entry.dir ?? null}, ${sql.json(manifest)})
|
|
158
|
-
ON CONFLICT (name) DO NOTHING
|
|
159
|
-
`;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
renameSync(sourcePath, backupPath);
|
|
163
|
-
|
|
164
|
-
await recordAuditEvent('item', 'migration', 'agent_directory_migrated', getActor(), {
|
|
165
|
-
count: entries.length,
|
|
166
|
-
});
|
|
167
|
-
} catch {
|
|
168
|
-
// Best effort — migration can be retried on the next run
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
// ============================================================================
|
|
173
|
-
// CRUD helpers for app_store
|
|
174
|
-
// ============================================================================
|
|
175
|
-
|
|
176
|
-
/**
|
|
177
|
-
* Insert a new item into the app_store. Returns the generated id.
|
|
178
|
-
*
|
|
179
|
-
* Throws if an item with the same name already exists — callers should
|
|
180
|
-
* check beforehand or use `--force` to remove + re-insert.
|
|
181
|
-
*/
|
|
182
|
-
export async function registerItemInStore(item: StoreInsert): Promise<string> {
|
|
183
|
-
const sql = await getConnection();
|
|
184
|
-
|
|
185
|
-
const rows = await sql`
|
|
186
|
-
INSERT INTO app_store (
|
|
187
|
-
name, item_type, version, description,
|
|
188
|
-
author_name, author_url, git_url, install_path,
|
|
189
|
-
manifest, tags, category, license, dependencies
|
|
190
|
-
) VALUES (
|
|
191
|
-
${item.name},
|
|
192
|
-
${item.itemType},
|
|
193
|
-
${item.version ?? '0.0.0'},
|
|
194
|
-
${item.description ?? null},
|
|
195
|
-
${item.authorName ?? null},
|
|
196
|
-
${item.authorUrl ?? null},
|
|
197
|
-
${item.gitUrl ?? null},
|
|
198
|
-
${item.installPath ?? null},
|
|
199
|
-
${sql.json(item.manifest ?? {})},
|
|
200
|
-
${item.tags ?? []},
|
|
201
|
-
${item.category ?? null},
|
|
202
|
-
${item.license ?? null},
|
|
203
|
-
${item.dependencies ?? []}
|
|
204
|
-
)
|
|
205
|
-
RETURNING id
|
|
206
|
-
`;
|
|
207
|
-
|
|
208
|
-
if (rows.length === 0) {
|
|
209
|
-
throw new Error(`Failed to insert item "${item.name}" — no id returned.`);
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
return rows[0].id as string;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
/**
|
|
216
|
-
* Delete an item from the app_store by name.
|
|
217
|
-
*
|
|
218
|
-
* @returns true if an item was deleted, false if no matching item was found.
|
|
219
|
-
*/
|
|
220
|
-
export async function removeItemFromStore(name: string): Promise<boolean> {
|
|
221
|
-
const sql = await getConnection();
|
|
222
|
-
const result = await sql`DELETE FROM app_store WHERE name = ${name}`;
|
|
223
|
-
return result.count > 0;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
/**
|
|
227
|
-
* Update an existing item in the app_store by name.
|
|
228
|
-
* Only provided fields are updated; updated_at is always set to now().
|
|
229
|
-
*
|
|
230
|
-
* @returns true if the item was updated, false if no matching item was found.
|
|
231
|
-
*/
|
|
232
|
-
export async function updateItemInStore(name: string, updates: Partial<StoreInsert>): Promise<boolean> {
|
|
233
|
-
const sql = await getConnection();
|
|
234
|
-
|
|
235
|
-
const s: Record<string, unknown> = {};
|
|
236
|
-
if (updates.name !== undefined) s.name = updates.name;
|
|
237
|
-
if (updates.itemType !== undefined) s.item_type = updates.itemType;
|
|
238
|
-
if (updates.version !== undefined) s.version = updates.version;
|
|
239
|
-
if (updates.description !== undefined) s.description = updates.description;
|
|
240
|
-
if (updates.authorName !== undefined) s.author_name = updates.authorName;
|
|
241
|
-
if (updates.authorUrl !== undefined) s.author_url = updates.authorUrl;
|
|
242
|
-
if (updates.gitUrl !== undefined) s.git_url = updates.gitUrl;
|
|
243
|
-
if (updates.installPath !== undefined) s.install_path = updates.installPath;
|
|
244
|
-
if (updates.manifest !== undefined) s.manifest = sql.json(updates.manifest);
|
|
245
|
-
if (updates.tags !== undefined) s.tags = updates.tags;
|
|
246
|
-
if (updates.category !== undefined) s.category = updates.category;
|
|
247
|
-
if (updates.license !== undefined) s.license = updates.license;
|
|
248
|
-
if (updates.dependencies !== undefined) s.dependencies = updates.dependencies;
|
|
249
|
-
|
|
250
|
-
if (Object.keys(s).length === 0) return false;
|
|
251
|
-
|
|
252
|
-
s.updated_at = sql`now()`;
|
|
253
|
-
const result = await sql`UPDATE app_store SET ${sql(s)} WHERE name = ${name}`;
|
|
254
|
-
return result.count > 0;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
/**
|
|
258
|
-
* Fetch a single item from the app_store by name.
|
|
259
|
-
*
|
|
260
|
-
* @returns the row or null if not found.
|
|
261
|
-
*/
|
|
262
|
-
export async function getItemFromStore(name: string): Promise<StoreRow | null> {
|
|
263
|
-
const sql = await getConnection();
|
|
264
|
-
const rows = await sql`SELECT * FROM app_store WHERE name = ${name}`;
|
|
265
|
-
return rows.length > 0 ? (rows[0] as unknown as StoreRow) : null;
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
/**
|
|
269
|
-
* List items from the app_store, optionally filtered by item_type.
|
|
270
|
-
*
|
|
271
|
-
* @param itemType - When provided, only rows matching this type are returned.
|
|
272
|
-
* @returns rows ordered by name.
|
|
273
|
-
*/
|
|
274
|
-
export async function listItemsFromStore(itemType?: string): Promise<StoreRow[]> {
|
|
275
|
-
const sql = await getConnection();
|
|
276
|
-
|
|
277
|
-
const rows = itemType
|
|
278
|
-
? await sql`SELECT * FROM app_store WHERE item_type = ${itemType} ORDER BY name`
|
|
279
|
-
: await sql`SELECT * FROM app_store ORDER BY name`;
|
|
280
|
-
|
|
281
|
-
return rows as unknown as StoreRow[];
|
|
282
|
-
}
|