@commandgarden/cli 2.14.0 → 2.14.1
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/main.js +248 -278
- package/node_modules/@commandgarden/app/dist/client/assets/{Slides-DWQmJfkp.js → Slides-DMIm_HgI.js} +5 -5
- package/node_modules/@commandgarden/app/dist/client/assets/{index-C8yqRQk-.js → index-CupEN1w-.js} +53 -53
- package/node_modules/@commandgarden/app/dist/client/index.html +18 -18
- package/node_modules/@commandgarden/app/dist/server/daemon-client.test.js +47 -0
- package/node_modules/@commandgarden/app/dist/server/main.js +1 -1
- package/node_modules/@commandgarden/app/dist/server/routes/audit.js +16 -27
- package/node_modules/@commandgarden/app/dist/server/routes/config.js +2 -8
- package/node_modules/@commandgarden/app/dist/server/routes/connectors.js +26 -37
- package/node_modules/@commandgarden/app/dist/server/routes/routes.test.js +137 -0
- package/node_modules/@commandgarden/app/dist/server/routes/run.js +4 -3
- package/node_modules/@commandgarden/app/dist/server/routes/status.js +1 -2
- package/node_modules/@commandgarden/app/dist/server/store.js +53 -53
- package/node_modules/@commandgarden/app/dist/server/store.test.js +59 -0
- package/node_modules/@commandgarden/app/package.json +42 -42
- package/node_modules/@commandgarden/chrome/dist/content-script.js.map +1 -1
- package/node_modules/@commandgarden/chrome/dist/fetch-interceptor.js +44 -0
- package/node_modules/@commandgarden/chrome/dist/manifest.json +26 -26
- package/node_modules/@commandgarden/chrome/dist/popup.html +218 -218
- package/node_modules/@commandgarden/chrome/dist/popup.js.map +1 -1
- package/node_modules/@commandgarden/chrome/dist/service-worker.js +40 -40
- package/node_modules/@commandgarden/chrome/dist/service-worker.js.map +2 -2
- package/node_modules/@commandgarden/chrome/package.json +26 -26
- package/node_modules/@commandgarden/daemon/connectors/ado-git-commits.eval.js +100 -100
- package/node_modules/@commandgarden/daemon/connectors/ado-git-commits.yaml +71 -71
- package/node_modules/@commandgarden/daemon/connectors/gcs-kb-content.yaml +61 -61
- package/node_modules/@commandgarden/daemon/connectors/gcs-kb-pages.yaml +54 -54
- package/node_modules/@commandgarden/daemon/connectors/jira-my-tickets.eval.js +115 -115
- package/node_modules/@commandgarden/daemon/connectors/jira-my-tickets.yaml +54 -54
- package/node_modules/@commandgarden/daemon/connectors/lib/msal-token.js +20 -20
- package/node_modules/@commandgarden/daemon/connectors/outlook-my-meetings.eval.js +204 -204
- package/node_modules/@commandgarden/daemon/connectors/outlook-my-meetings.yaml +42 -42
- package/node_modules/@commandgarden/daemon/connectors/saba-pending-training.eval.js +94 -94
- package/node_modules/@commandgarden/daemon/connectors/saba-pending-training.yaml +34 -34
- package/node_modules/@commandgarden/daemon/connectors/socket-security-news.yaml +63 -63
- package/node_modules/@commandgarden/daemon/connectors/teams-room-availability.eval.js +437 -437
- package/node_modules/@commandgarden/daemon/connectors/teams-room-availability.yaml +53 -53
- package/node_modules/@commandgarden/daemon/connectors/teams-rooms-availability.eval.js +547 -547
- package/node_modules/@commandgarden/daemon/connectors/teams-rooms-availability.yaml +48 -48
- package/node_modules/@commandgarden/daemon/connectors/timetracking-projects.eval.js +52 -52
- package/node_modules/@commandgarden/daemon/connectors/timetracking-projects.yaml +42 -42
- package/node_modules/@commandgarden/daemon/connectors/timetracking-report.eval.js +81 -81
- package/node_modules/@commandgarden/daemon/connectors/timetracking-report.yaml +52 -52
- package/node_modules/@commandgarden/daemon/connectors/tldrsec-newsletter.eval.js +27 -27
- package/node_modules/@commandgarden/daemon/connectors/tldrsec-newsletter.yaml +38 -38
- package/node_modules/@commandgarden/daemon/connectors/tokenmaster-clients-list.yaml +55 -55
- package/node_modules/@commandgarden/daemon/dist/audit-store.js +8 -8
- package/node_modules/@commandgarden/daemon/dist/auth.d.ts +0 -1
- package/node_modules/@commandgarden/daemon/dist/auth.d.ts.map +1 -1
- package/node_modules/@commandgarden/daemon/dist/auth.js +0 -10
- package/node_modules/@commandgarden/daemon/dist/auth.js.map +1 -1
- package/node_modules/@commandgarden/daemon/dist/cdp-client.d.ts +24 -0
- package/node_modules/@commandgarden/daemon/dist/cdp-client.d.ts.map +1 -0
- package/node_modules/@commandgarden/daemon/dist/cdp-client.js +150 -0
- package/node_modules/@commandgarden/daemon/dist/cdp-client.js.map +1 -0
- package/node_modules/@commandgarden/daemon/dist/main.js +5 -12
- package/node_modules/@commandgarden/daemon/dist/main.js.map +1 -1
- package/node_modules/@commandgarden/daemon/dist/server.d.ts.map +1 -1
- package/node_modules/@commandgarden/daemon/dist/server.js +0 -10
- package/node_modules/@commandgarden/daemon/dist/server.js.map +1 -1
- package/node_modules/@commandgarden/daemon/package.json +32 -32
- package/node_modules/@commandgarden/shared/dist/daemon-client.d.ts +1 -5
- package/node_modules/@commandgarden/shared/dist/daemon-client.d.ts.map +1 -1
- package/node_modules/@commandgarden/shared/dist/daemon-client.js +14 -32
- package/node_modules/@commandgarden/shared/dist/daemon-client.js.map +1 -1
- package/node_modules/@commandgarden/shared/package.json +32 -32
- package/package.json +58 -58
- package/node_modules/@commandgarden/app/dist/server/routes/utils.d.ts +0 -2
- package/node_modules/@commandgarden/app/dist/server/routes/utils.js +0 -4
|
@@ -33,7 +33,7 @@ async function start() {
|
|
|
33
33
|
}
|
|
34
34
|
const port = readAppPort();
|
|
35
35
|
const daemonUrl = 'http://127.0.0.1:9091';
|
|
36
|
-
const daemon = new DaemonClient(daemonUrl, token
|
|
36
|
+
const daemon = new DaemonClient(daemonUrl, token);
|
|
37
37
|
const store = await AppStore.create(join(CG_HOME, 'app.db'));
|
|
38
38
|
const app = Fastify();
|
|
39
39
|
registerRoutes(app, daemon, store);
|
|
@@ -1,31 +1,20 @@
|
|
|
1
|
-
import { daemonErrorMessage } from './utils.js';
|
|
2
1
|
export function auditRoutes(app, daemon) {
|
|
3
|
-
app.get('/api/audit', async (req
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
return await daemon.get(`/api/audit${qs ? `?${qs}` : ''}`);
|
|
17
|
-
}
|
|
18
|
-
catch (err) {
|
|
19
|
-
reply.code(502).send({ ok: false, error: daemonErrorMessage(err) });
|
|
20
|
-
}
|
|
2
|
+
app.get('/api/audit', async (req) => {
|
|
3
|
+
const query = req.query;
|
|
4
|
+
const params = new URLSearchParams();
|
|
5
|
+
if (query.since)
|
|
6
|
+
params.set('since', query.since);
|
|
7
|
+
if (query.connector)
|
|
8
|
+
params.set('connector', query.connector);
|
|
9
|
+
if (query.type)
|
|
10
|
+
params.set('type', query.type);
|
|
11
|
+
if (query.limit)
|
|
12
|
+
params.set('limit', query.limit);
|
|
13
|
+
const qs = params.toString();
|
|
14
|
+
return daemon.get(`/api/audit${qs ? `?${qs}` : ''}`);
|
|
21
15
|
});
|
|
22
|
-
app.get('/api/audit/:id', async (req
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
return await daemon.get(`/api/audit/${id}`);
|
|
26
|
-
}
|
|
27
|
-
catch (err) {
|
|
28
|
-
reply.code(502).send({ ok: false, error: daemonErrorMessage(err) });
|
|
29
|
-
}
|
|
16
|
+
app.get('/api/audit/:id', async (req) => {
|
|
17
|
+
const { id } = req.params;
|
|
18
|
+
return daemon.get(`/api/audit/${id}`);
|
|
30
19
|
});
|
|
31
20
|
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { daemonErrorMessage } from './utils.js';
|
|
2
1
|
export function configRoutes(app, daemon) {
|
|
3
2
|
app.get('/api/config', async () => {
|
|
4
3
|
try {
|
|
@@ -8,12 +7,7 @@ export function configRoutes(app, daemon) {
|
|
|
8
7
|
return { ok: false, config: {} };
|
|
9
8
|
}
|
|
10
9
|
});
|
|
11
|
-
app.post('/api/config', async (req
|
|
12
|
-
|
|
13
|
-
return await daemon.post('/api/config', req.body);
|
|
14
|
-
}
|
|
15
|
-
catch (err) {
|
|
16
|
-
reply.code(502).send({ ok: false, error: daemonErrorMessage(err) });
|
|
17
|
-
}
|
|
10
|
+
app.post('/api/config', async (req) => {
|
|
11
|
+
return daemon.post('/api/config', req.body);
|
|
18
12
|
});
|
|
19
13
|
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { HIGH_RISK_CAPABILITIES } from '@commandgarden/shared';
|
|
2
|
-
import { daemonErrorMessage } from './utils.js';
|
|
3
2
|
const APP_ROUTES = {
|
|
4
3
|
'timetracking/report': '/apps/timetracking',
|
|
5
4
|
'teams/room-availability': '/apps/rooms',
|
|
@@ -8,42 +7,32 @@ const APP_ROUTES = {
|
|
|
8
7
|
'tokenmaster/client-trustedby': '/apps/trusted-peer-expiry',
|
|
9
8
|
};
|
|
10
9
|
export function connectorRoutes(app, daemon) {
|
|
11
|
-
app.get('/api/connectors', async (
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
return { ok: true, connectors: enriched };
|
|
35
|
-
}
|
|
36
|
-
catch (err) {
|
|
37
|
-
reply.code(502).send({ ok: false, error: daemonErrorMessage(err) });
|
|
38
|
-
}
|
|
10
|
+
app.get('/api/connectors', async () => {
|
|
11
|
+
const [connectorData, configData] = await Promise.all([
|
|
12
|
+
daemon.get('/api/connectors'),
|
|
13
|
+
daemon.get('/api/config')
|
|
14
|
+
.catch(() => ({ ok: false, config: {} })),
|
|
15
|
+
]);
|
|
16
|
+
const security = (configData.config?.security ?? {});
|
|
17
|
+
const highRiskCaps = new Set(security.highRiskCapabilities ?? [...HIGH_RISK_CAPABILITIES]);
|
|
18
|
+
const approvedHighRisk = new Set(security.approvedHighRisk ?? []);
|
|
19
|
+
const autoApproveConnectors = new Set(security.autoApproveConnectors ?? []);
|
|
20
|
+
const enriched = connectorData.connectors.map((c) => {
|
|
21
|
+
const key = c.key;
|
|
22
|
+
const capabilities = c.capabilities ?? [];
|
|
23
|
+
return {
|
|
24
|
+
...c,
|
|
25
|
+
hasAppPage: key in APP_ROUTES,
|
|
26
|
+
appRoute: APP_ROUTES[key] ?? null,
|
|
27
|
+
isHighRisk: capabilities.some(cap => highRiskCaps.has(cap)),
|
|
28
|
+
isApproved: approvedHighRisk.has(key),
|
|
29
|
+
isAutoApproved: autoApproveConnectors.has(key),
|
|
30
|
+
};
|
|
31
|
+
});
|
|
32
|
+
return { ok: true, connectors: enriched };
|
|
39
33
|
});
|
|
40
|
-
app.get('/api/connectors/:site/:name', async (req
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
return await daemon.get(`/api/connectors/${site}/${name}`);
|
|
44
|
-
}
|
|
45
|
-
catch (err) {
|
|
46
|
-
reply.code(502).send({ ok: false, error: daemonErrorMessage(err) });
|
|
47
|
-
}
|
|
34
|
+
app.get('/api/connectors/:site/:name', async (req) => {
|
|
35
|
+
const { site, name } = req.params;
|
|
36
|
+
return daemon.get(`/api/connectors/${site}/${name}`);
|
|
48
37
|
});
|
|
49
38
|
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import Fastify from 'fastify';
|
|
3
|
+
import { registerRoutes } from './index.js';
|
|
4
|
+
import { AppStore } from '../store.js';
|
|
5
|
+
function mockDaemon() {
|
|
6
|
+
return {
|
|
7
|
+
get: vi.fn(),
|
|
8
|
+
post: vi.fn(),
|
|
9
|
+
pipeRaw: vi.fn(),
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
describe('routes', () => {
|
|
13
|
+
let app;
|
|
14
|
+
let daemon;
|
|
15
|
+
let store;
|
|
16
|
+
beforeEach(async () => {
|
|
17
|
+
app = Fastify();
|
|
18
|
+
daemon = mockDaemon();
|
|
19
|
+
store = new AppStore(':memory:');
|
|
20
|
+
registerRoutes(app, daemon, store);
|
|
21
|
+
await app.ready();
|
|
22
|
+
});
|
|
23
|
+
describe('GET /api/status', () => {
|
|
24
|
+
it('proxies daemon status', async () => {
|
|
25
|
+
daemon.get.mockResolvedValueOnce({
|
|
26
|
+
ok: true, extensionConnected: true, connectorCount: 2,
|
|
27
|
+
});
|
|
28
|
+
const resp = await app.inject({ method: 'GET', url: '/api/status' });
|
|
29
|
+
expect(resp.statusCode).toBe(200);
|
|
30
|
+
const body = JSON.parse(resp.payload);
|
|
31
|
+
expect(body.ok).toBe(true);
|
|
32
|
+
expect(body.extensionConnected).toBe(true);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
describe('GET /api/connectors', () => {
|
|
36
|
+
it('proxies and enriches connector list', async () => {
|
|
37
|
+
daemon.get
|
|
38
|
+
.mockImplementation((path) => {
|
|
39
|
+
if (path === '/api/connectors') {
|
|
40
|
+
return Promise.resolve({
|
|
41
|
+
ok: true,
|
|
42
|
+
connectors: [
|
|
43
|
+
{ key: 'timetracking/report', description: 'test', access: 'read', domains: [], capabilities: [] },
|
|
44
|
+
],
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
if (path === '/api/config') {
|
|
48
|
+
return Promise.resolve({
|
|
49
|
+
ok: true,
|
|
50
|
+
config: { security: { highRiskCapabilities: ['js_evaluate', 'cookie_write'], approvedHighRisk: [], autoApproveConnectors: [] } },
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
return Promise.resolve({ ok: true });
|
|
54
|
+
});
|
|
55
|
+
const resp = await app.inject({ method: 'GET', url: '/api/connectors' });
|
|
56
|
+
expect(resp.statusCode).toBe(200);
|
|
57
|
+
const body = JSON.parse(resp.payload);
|
|
58
|
+
expect(body.connectors).toHaveLength(1);
|
|
59
|
+
expect(body.connectors[0].hasAppPage).toBe(true);
|
|
60
|
+
expect(body.connectors[0].appRoute).toBe('/apps/timetracking');
|
|
61
|
+
});
|
|
62
|
+
it('enriches connectors with security flags from config', async () => {
|
|
63
|
+
daemon.get
|
|
64
|
+
.mockImplementation((path) => {
|
|
65
|
+
if (path === '/api/connectors') {
|
|
66
|
+
return Promise.resolve({
|
|
67
|
+
ok: true,
|
|
68
|
+
connectors: [
|
|
69
|
+
{ key: 'timetracking/report', description: 'test', access: 'read', domains: [], capabilities: ['navigate', 'js_evaluate'] },
|
|
70
|
+
{ key: 'safe/connector', description: 'safe', access: 'read', domains: [], capabilities: ['navigate'] },
|
|
71
|
+
],
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
if (path === '/api/config') {
|
|
75
|
+
return Promise.resolve({
|
|
76
|
+
ok: true,
|
|
77
|
+
config: {
|
|
78
|
+
security: {
|
|
79
|
+
highRiskCapabilities: ['js_evaluate', 'cookie_write'],
|
|
80
|
+
approvedHighRisk: ['timetracking/report'],
|
|
81
|
+
autoApproveConnectors: ['timetracking/report'],
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
return Promise.resolve({ ok: true });
|
|
87
|
+
});
|
|
88
|
+
const resp = await app.inject({ method: 'GET', url: '/api/connectors' });
|
|
89
|
+
const body = JSON.parse(resp.payload);
|
|
90
|
+
expect(body.connectors[0].isHighRisk).toBe(true);
|
|
91
|
+
expect(body.connectors[0].isApproved).toBe(true);
|
|
92
|
+
expect(body.connectors[0].isAutoApproved).toBe(true);
|
|
93
|
+
expect(body.connectors[1].isHighRisk).toBe(false);
|
|
94
|
+
expect(body.connectors[1].isApproved).toBe(false);
|
|
95
|
+
expect(body.connectors[1].isAutoApproved).toBe(false);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
describe('GET /api/preferences', () => {
|
|
99
|
+
it('returns empty preferences', async () => {
|
|
100
|
+
const resp = await app.inject({ method: 'GET', url: '/api/preferences' });
|
|
101
|
+
expect(resp.statusCode).toBe(200);
|
|
102
|
+
expect(JSON.parse(resp.payload)).toEqual({ ok: true, preferences: {} });
|
|
103
|
+
});
|
|
104
|
+
it('sets and gets a preference via PUT', async () => {
|
|
105
|
+
await app.inject({
|
|
106
|
+
method: 'PUT', url: '/api/preferences',
|
|
107
|
+
payload: { key: 'theme', value: 'dark' },
|
|
108
|
+
});
|
|
109
|
+
const resp = await app.inject({ method: 'GET', url: '/api/preferences' });
|
|
110
|
+
expect(JSON.parse(resp.payload).preferences).toEqual({ theme: 'dark' });
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
describe('POST /api/run', () => {
|
|
114
|
+
it('proxies run to daemon', async () => {
|
|
115
|
+
daemon.post.mockResolvedValueOnce({
|
|
116
|
+
ok: true, data: [{ a: 1 }], rowCount: 1,
|
|
117
|
+
});
|
|
118
|
+
const resp = await app.inject({
|
|
119
|
+
method: 'POST', url: '/api/run',
|
|
120
|
+
payload: { connector: 'test/cmd', args: {} },
|
|
121
|
+
});
|
|
122
|
+
expect(resp.statusCode).toBe(200);
|
|
123
|
+
expect(JSON.parse(resp.payload).ok).toBe(true);
|
|
124
|
+
});
|
|
125
|
+
it('returns 502 with error message when daemon throws', async () => {
|
|
126
|
+
daemon.post.mockRejectedValueOnce(new Error('Extension not connected'));
|
|
127
|
+
const resp = await app.inject({
|
|
128
|
+
method: 'POST', url: '/api/run',
|
|
129
|
+
payload: { connector: 'test/cmd', args: {} },
|
|
130
|
+
});
|
|
131
|
+
expect(resp.statusCode).toBe(502);
|
|
132
|
+
const body = JSON.parse(resp.payload);
|
|
133
|
+
expect(body.ok).toBe(false);
|
|
134
|
+
expect(body.error).toBe('Extension not connected');
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
});
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { daemonErrorMessage } from './utils.js';
|
|
2
1
|
export function runRoutes(app, daemon) {
|
|
3
2
|
app.post('/api/run', async (req, reply) => {
|
|
4
3
|
try {
|
|
5
4
|
return await daemon.post('/api/run', req.body);
|
|
6
5
|
}
|
|
7
6
|
catch (err) {
|
|
8
|
-
|
|
7
|
+
const message = err instanceof Error ? err.message : 'Unknown error';
|
|
8
|
+
reply.code(502).send({ ok: false, error: message });
|
|
9
9
|
}
|
|
10
10
|
});
|
|
11
11
|
app.post('/api/approval', async (req, reply) => {
|
|
@@ -13,7 +13,8 @@ export function runRoutes(app, daemon) {
|
|
|
13
13
|
return await daemon.post('/api/approval', req.body);
|
|
14
14
|
}
|
|
15
15
|
catch (err) {
|
|
16
|
-
|
|
16
|
+
const message = err instanceof Error ? err.message : 'Unknown error';
|
|
17
|
+
reply.code(502).send({ ok: false, error: message });
|
|
17
18
|
}
|
|
18
19
|
});
|
|
19
20
|
app.get('/api/run/events/:requestId', async (req, reply) => {
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { daemonErrorMessage } from './utils.js';
|
|
2
1
|
export function statusRoutes(app, daemon) {
|
|
3
2
|
app.get('/api/status', async (req, reply) => {
|
|
4
3
|
try {
|
|
@@ -6,7 +5,7 @@ export function statusRoutes(app, daemon) {
|
|
|
6
5
|
return data;
|
|
7
6
|
}
|
|
8
7
|
catch (err) {
|
|
9
|
-
reply.code(503).send({ ok: false, error:
|
|
8
|
+
reply.code(503).send({ ok: false, error: err instanceof Error ? err.message : 'Daemon unreachable' });
|
|
10
9
|
}
|
|
11
10
|
});
|
|
12
11
|
}
|
|
@@ -49,16 +49,16 @@ export class AppStore {
|
|
|
49
49
|
});
|
|
50
50
|
}
|
|
51
51
|
migrate() {
|
|
52
|
-
this.db.run(`CREATE TABLE IF NOT EXISTS preferences (
|
|
53
|
-
key TEXT PRIMARY KEY,
|
|
54
|
-
value TEXT NOT NULL
|
|
52
|
+
this.db.run(`CREATE TABLE IF NOT EXISTS preferences (
|
|
53
|
+
key TEXT PRIMARY KEY,
|
|
54
|
+
value TEXT NOT NULL
|
|
55
55
|
)`);
|
|
56
|
-
this.db.run(`CREATE TABLE IF NOT EXISTS saved_views (
|
|
57
|
-
id TEXT PRIMARY KEY,
|
|
58
|
-
app TEXT NOT NULL,
|
|
59
|
-
name TEXT NOT NULL,
|
|
60
|
-
config TEXT NOT NULL,
|
|
61
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
56
|
+
this.db.run(`CREATE TABLE IF NOT EXISTS saved_views (
|
|
57
|
+
id TEXT PRIMARY KEY,
|
|
58
|
+
app TEXT NOT NULL,
|
|
59
|
+
name TEXT NOT NULL,
|
|
60
|
+
config TEXT NOT NULL,
|
|
61
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
62
62
|
)`);
|
|
63
63
|
// Drop legacy goals table (had category column) and recreate with activity
|
|
64
64
|
try {
|
|
@@ -76,41 +76,41 @@ export class AppStore {
|
|
|
76
76
|
}
|
|
77
77
|
catch { /* ignore */ }
|
|
78
78
|
}
|
|
79
|
-
this.db.run(`CREATE TABLE IF NOT EXISTS goals (
|
|
80
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
81
|
-
month TEXT NOT NULL,
|
|
82
|
-
project_id TEXT NOT NULL,
|
|
83
|
-
activity TEXT NOT NULL DEFAULT '',
|
|
84
|
-
target_days REAL NOT NULL,
|
|
85
|
-
target_hours REAL NOT NULL,
|
|
86
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
87
|
-
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
88
|
-
UNIQUE(month, project_id, activity)
|
|
79
|
+
this.db.run(`CREATE TABLE IF NOT EXISTS goals (
|
|
80
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
81
|
+
month TEXT NOT NULL,
|
|
82
|
+
project_id TEXT NOT NULL,
|
|
83
|
+
activity TEXT NOT NULL DEFAULT '',
|
|
84
|
+
target_days REAL NOT NULL,
|
|
85
|
+
target_hours REAL NOT NULL,
|
|
86
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
87
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
88
|
+
UNIQUE(month, project_id, activity)
|
|
89
89
|
)`);
|
|
90
|
-
this.db.run(`CREATE TABLE IF NOT EXISTS report_cache (
|
|
91
|
-
month TEXT PRIMARY KEY,
|
|
92
|
-
data TEXT NOT NULL,
|
|
93
|
-
fetched_at TEXT NOT NULL
|
|
90
|
+
this.db.run(`CREATE TABLE IF NOT EXISTS report_cache (
|
|
91
|
+
month TEXT PRIMARY KEY,
|
|
92
|
+
data TEXT NOT NULL,
|
|
93
|
+
fetched_at TEXT NOT NULL
|
|
94
94
|
)`);
|
|
95
|
-
this.db.run(`CREATE TABLE IF NOT EXISTS projects_cache (
|
|
96
|
-
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
97
|
-
data TEXT NOT NULL,
|
|
98
|
-
fetched_at TEXT NOT NULL
|
|
95
|
+
this.db.run(`CREATE TABLE IF NOT EXISTS projects_cache (
|
|
96
|
+
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
97
|
+
data TEXT NOT NULL,
|
|
98
|
+
fetched_at TEXT NOT NULL
|
|
99
99
|
)`);
|
|
100
|
-
this.db.run(`CREATE TABLE IF NOT EXISTS security_news_cache (
|
|
101
|
-
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
102
|
-
data TEXT NOT NULL,
|
|
103
|
-
fetched_at TEXT NOT NULL
|
|
100
|
+
this.db.run(`CREATE TABLE IF NOT EXISTS security_news_cache (
|
|
101
|
+
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
102
|
+
data TEXT NOT NULL,
|
|
103
|
+
fetched_at TEXT NOT NULL
|
|
104
104
|
)`);
|
|
105
|
-
this.db.run(`CREATE TABLE IF NOT EXISTS trusted_peers_cache (
|
|
106
|
-
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
107
|
-
data TEXT NOT NULL,
|
|
108
|
-
fetched_at TEXT NOT NULL
|
|
105
|
+
this.db.run(`CREATE TABLE IF NOT EXISTS trusted_peers_cache (
|
|
106
|
+
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
107
|
+
data TEXT NOT NULL,
|
|
108
|
+
fetched_at TEXT NOT NULL
|
|
109
109
|
)`);
|
|
110
|
-
this.db.run(`CREATE TABLE IF NOT EXISTS room_availability_cache (
|
|
111
|
-
date TEXT PRIMARY KEY,
|
|
112
|
-
data TEXT NOT NULL,
|
|
113
|
-
fetched_at TEXT NOT NULL
|
|
110
|
+
this.db.run(`CREATE TABLE IF NOT EXISTS room_availability_cache (
|
|
111
|
+
date TEXT PRIMARY KEY,
|
|
112
|
+
data TEXT NOT NULL,
|
|
113
|
+
fetched_at TEXT NOT NULL
|
|
114
114
|
)`);
|
|
115
115
|
// Legacy schema keyed by cache_key (weekStart + sources signature) —
|
|
116
116
|
// migrate to a plain weekStart-keyed table (one cached result per week,
|
|
@@ -121,10 +121,10 @@ export class AppStore {
|
|
|
121
121
|
this.db.run(`DROP TABLE journal_cache`);
|
|
122
122
|
}
|
|
123
123
|
catch { /* already on new schema, or table doesn't exist yet */ }
|
|
124
|
-
this.db.run(`CREATE TABLE IF NOT EXISTS journal_cache (
|
|
125
|
-
week_start TEXT PRIMARY KEY,
|
|
126
|
-
data TEXT NOT NULL,
|
|
127
|
-
fetched_at TEXT NOT NULL
|
|
124
|
+
this.db.run(`CREATE TABLE IF NOT EXISTS journal_cache (
|
|
125
|
+
week_start TEXT PRIMARY KEY,
|
|
126
|
+
data TEXT NOT NULL,
|
|
127
|
+
fetched_at TEXT NOT NULL
|
|
128
128
|
)`);
|
|
129
129
|
// saba_data holds the last Saba "pending training" connector result for
|
|
130
130
|
// the week, saved separately from `data` since it's fetched client-side
|
|
@@ -137,14 +137,14 @@ export class AppStore {
|
|
|
137
137
|
// Remembers the last-selected source toggles (Timetracking/Meetings/Jira/
|
|
138
138
|
// Git/Saba) so the UI can restore the user's picks on next load, instead
|
|
139
139
|
// of always resetting to "all enabled".
|
|
140
|
-
this.db.run(`CREATE TABLE IF NOT EXISTS journal_source_prefs (
|
|
141
|
-
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
142
|
-
timetracking INTEGER NOT NULL DEFAULT 1,
|
|
143
|
-
meetings INTEGER NOT NULL DEFAULT 1,
|
|
144
|
-
jira INTEGER NOT NULL DEFAULT 1,
|
|
145
|
-
git INTEGER NOT NULL DEFAULT 1,
|
|
146
|
-
saba INTEGER NOT NULL DEFAULT 1,
|
|
147
|
-
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
140
|
+
this.db.run(`CREATE TABLE IF NOT EXISTS journal_source_prefs (
|
|
141
|
+
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
142
|
+
timetracking INTEGER NOT NULL DEFAULT 1,
|
|
143
|
+
meetings INTEGER NOT NULL DEFAULT 1,
|
|
144
|
+
jira INTEGER NOT NULL DEFAULT 1,
|
|
145
|
+
git INTEGER NOT NULL DEFAULT 1,
|
|
146
|
+
saba INTEGER NOT NULL DEFAULT 1,
|
|
147
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
148
148
|
)`);
|
|
149
149
|
}
|
|
150
150
|
getPreference(key) {
|
|
@@ -291,7 +291,7 @@ export class AppStore {
|
|
|
291
291
|
// ON CONFLICT (rather than INSERT OR REPLACE) so an existing saba_data
|
|
292
292
|
// value for this week isn't wiped out when the main journal result is
|
|
293
293
|
// (re)generated — the two are cached independently.
|
|
294
|
-
this.db.run(`INSERT INTO journal_cache (week_start, data, fetched_at) VALUES (?, ?, ?)
|
|
294
|
+
this.db.run(`INSERT INTO journal_cache (week_start, data, fetched_at) VALUES (?, ?, ?)
|
|
295
295
|
ON CONFLICT(week_start) DO UPDATE SET data = excluded.data, fetched_at = excluded.fetched_at`, [weekStart, JSON.stringify(data), now]);
|
|
296
296
|
this.persist();
|
|
297
297
|
return now;
|
|
@@ -332,7 +332,7 @@ export class AppStore {
|
|
|
332
332
|
}
|
|
333
333
|
saveJournalSourcePrefs(sources) {
|
|
334
334
|
const now = new Date().toISOString();
|
|
335
|
-
this.db.run(`INSERT OR REPLACE INTO journal_source_prefs (id, timetracking, meetings, jira, git, saba, updated_at)
|
|
335
|
+
this.db.run(`INSERT OR REPLACE INTO journal_source_prefs (id, timetracking, meetings, jira, git, saba, updated_at)
|
|
336
336
|
VALUES (1, ?, ?, ?, ?, ?, ?)`, [
|
|
337
337
|
sources.timetracking ? 1 : 0,
|
|
338
338
|
sources.meetings ? 1 : 0,
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { AppStore } from './store.js';
|
|
3
|
+
describe('AppStore', () => {
|
|
4
|
+
let store;
|
|
5
|
+
beforeEach(() => {
|
|
6
|
+
store = new AppStore(':memory:');
|
|
7
|
+
});
|
|
8
|
+
afterEach(() => {
|
|
9
|
+
store.close();
|
|
10
|
+
});
|
|
11
|
+
describe('preferences', () => {
|
|
12
|
+
it('returns undefined for missing key', () => {
|
|
13
|
+
expect(store.getPreference('missing')).toBeUndefined();
|
|
14
|
+
});
|
|
15
|
+
it('sets and gets a preference', () => {
|
|
16
|
+
store.setPreference('theme', 'light');
|
|
17
|
+
expect(store.getPreference('theme')).toBe('light');
|
|
18
|
+
});
|
|
19
|
+
it('overwrites existing preference', () => {
|
|
20
|
+
store.setPreference('theme', 'light');
|
|
21
|
+
store.setPreference('theme', 'dark');
|
|
22
|
+
expect(store.getPreference('theme')).toBe('dark');
|
|
23
|
+
});
|
|
24
|
+
it('returns all preferences', () => {
|
|
25
|
+
store.setPreference('a', '1');
|
|
26
|
+
store.setPreference('b', '2');
|
|
27
|
+
expect(store.getAllPreferences()).toEqual({ a: '1', b: '2' });
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
describe('saved views', () => {
|
|
31
|
+
it('returns empty array when no views', () => {
|
|
32
|
+
expect(store.listViews('timetracking')).toEqual([]);
|
|
33
|
+
});
|
|
34
|
+
it('creates and lists a view', () => {
|
|
35
|
+
const view = store.createView('timetracking', 'June report', '{"month":"2026-06"}');
|
|
36
|
+
expect(view.id).toBeDefined();
|
|
37
|
+
expect(view.app).toBe('timetracking');
|
|
38
|
+
expect(view.name).toBe('June report');
|
|
39
|
+
expect(view.config).toBe('{"month":"2026-06"}');
|
|
40
|
+
const views = store.listViews('timetracking');
|
|
41
|
+
expect(views).toHaveLength(1);
|
|
42
|
+
expect(views[0].name).toBe('June report');
|
|
43
|
+
});
|
|
44
|
+
it('filters views by app', () => {
|
|
45
|
+
store.createView('timetracking', 'View A', '{}');
|
|
46
|
+
store.createView('rooms', 'View B', '{}');
|
|
47
|
+
expect(store.listViews('timetracking')).toHaveLength(1);
|
|
48
|
+
expect(store.listViews('rooms')).toHaveLength(1);
|
|
49
|
+
});
|
|
50
|
+
it('deletes a view', () => {
|
|
51
|
+
const view = store.createView('timetracking', 'To delete', '{}');
|
|
52
|
+
expect(store.deleteView(view.id)).toBe(true);
|
|
53
|
+
expect(store.listViews('timetracking')).toHaveLength(0);
|
|
54
|
+
});
|
|
55
|
+
it('returns false when deleting non-existent view', () => {
|
|
56
|
+
expect(store.deleteView('nonexistent')).toBe(false);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
});
|
|
@@ -1,42 +1,42 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@commandgarden/app",
|
|
3
|
-
"version": "0.1.0",
|
|
4
|
-
"private": true,
|
|
5
|
-
"type": "module",
|
|
6
|
-
"files": [
|
|
7
|
-
"dist"
|
|
8
|
-
],
|
|
9
|
-
"scripts": {
|
|
10
|
-
"build": "vite build && tsc -p tsconfig.server.json",
|
|
11
|
-
"dev": "concurrently \"vite\" \"tsx watch src/server/main.ts\"",
|
|
12
|
-
"start": "node dist/server/main.js",
|
|
13
|
-
"test": "vitest run",
|
|
14
|
-
"test:watch": "vitest"
|
|
15
|
-
},
|
|
16
|
-
"dependencies": {
|
|
17
|
-
"@commandgarden/shared": "*",
|
|
18
|
-
"@fastify/static": "^8.0.0",
|
|
19
|
-
"fastify": "^5.0.0",
|
|
20
|
-
"lucide-react": "^1.23.0",
|
|
21
|
-
"react": "^18.3.0",
|
|
22
|
-
"react-dom": "^18.3.0",
|
|
23
|
-
"react-router-dom": "^6.23.0",
|
|
24
|
-
"sql.js": "^1.11.0",
|
|
25
|
-
"yaml": "^2.4.0"
|
|
26
|
-
},
|
|
27
|
-
"devDependencies": {
|
|
28
|
-
"@types/node": "^22.0.0",
|
|
29
|
-
"@types/react": "^18.3.0",
|
|
30
|
-
"@types/react-dom": "^18.3.0",
|
|
31
|
-
"@vitejs/plugin-react": "^4.3.0",
|
|
32
|
-
"autoprefixer": "^10.4.0",
|
|
33
|
-
"concurrently": "^9.0.0",
|
|
34
|
-
"daisyui": "^4.12.0",
|
|
35
|
-
"postcss": "^8.4.0",
|
|
36
|
-
"tailwindcss": "^3.4.0",
|
|
37
|
-
"tsx": "^4.0.0",
|
|
38
|
-
"typescript": "^5.4.0",
|
|
39
|
-
"vite": "^5.4.0",
|
|
40
|
-
"vitest": "^2.0.0"
|
|
41
|
-
}
|
|
42
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@commandgarden/app",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"files": [
|
|
7
|
+
"dist"
|
|
8
|
+
],
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "vite build && tsc -p tsconfig.server.json",
|
|
11
|
+
"dev": "concurrently \"vite\" \"tsx watch src/server/main.ts\"",
|
|
12
|
+
"start": "node dist/server/main.js",
|
|
13
|
+
"test": "vitest run",
|
|
14
|
+
"test:watch": "vitest"
|
|
15
|
+
},
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"@commandgarden/shared": "*",
|
|
18
|
+
"@fastify/static": "^8.0.0",
|
|
19
|
+
"fastify": "^5.0.0",
|
|
20
|
+
"lucide-react": "^1.23.0",
|
|
21
|
+
"react": "^18.3.0",
|
|
22
|
+
"react-dom": "^18.3.0",
|
|
23
|
+
"react-router-dom": "^6.23.0",
|
|
24
|
+
"sql.js": "^1.11.0",
|
|
25
|
+
"yaml": "^2.4.0"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@types/node": "^22.0.0",
|
|
29
|
+
"@types/react": "^18.3.0",
|
|
30
|
+
"@types/react-dom": "^18.3.0",
|
|
31
|
+
"@vitejs/plugin-react": "^4.3.0",
|
|
32
|
+
"autoprefixer": "^10.4.0",
|
|
33
|
+
"concurrently": "^9.0.0",
|
|
34
|
+
"daisyui": "^4.12.0",
|
|
35
|
+
"postcss": "^8.4.0",
|
|
36
|
+
"tailwindcss": "^3.4.0",
|
|
37
|
+
"tsx": "^4.0.0",
|
|
38
|
+
"typescript": "^5.4.0",
|
|
39
|
+
"vite": "^5.4.0",
|
|
40
|
+
"vitest": "^2.0.0"
|
|
41
|
+
}
|
|
42
|
+
}
|