@rubytech/create-maxy-code 0.1.439 → 0.1.441
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/package.json +1 -1
- package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +41 -12
- package/payload/platform/plugins/cloudflare/skills/calendar-site/SKILL.md +15 -5
- package/payload/platform/plugins/cloudflare/skills/calendar-site/template/functions/api/calendar/free-busy.ts +152 -0
- package/payload/platform/plugins/cloudflare/skills/calendar-site/template/public/index.html +6 -3
- package/payload/platform/plugins/cloudflare/skills/calendar-site/template/schema.sql +11 -0
- package/payload/platform/plugins/docs/references/admin-identity-gate.md +32 -17
- package/payload/platform/plugins/docs/references/admin-ui.md +1 -1
- package/payload/platform/plugins/docs/references/calendar-booking.md +1 -1
- package/payload/platform/plugins/docs/references/graph.md +19 -3
- package/payload/platform/plugins/docs/references/internals.md +2 -2
- package/payload/platform/plugins/docs/references/outlook-guide.md +9 -4
- package/payload/platform/plugins/docs/references/troubleshooting.md +8 -0
- package/payload/platform/plugins/graph/.claude-plugin/plugin.json +19 -0
- package/payload/platform/plugins/graph/PLUGIN.md +10 -4
- package/payload/platform/plugins/outlook/.claude-plugin/plugin.json +1 -1
- package/payload/platform/plugins/outlook/PLUGIN.md +7 -3
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/account-register.test.js +100 -10
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/account-register.test.js.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/mail-list.test.d.ts +2 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/mail-list.test.d.ts.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/mail-list.test.js +55 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/mail-list.test.js.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/mailbox-list.test.d.ts +2 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/mailbox-list.test.d.ts.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/mailbox-list.test.js +64 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/mailbox-list.test.js.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/mailbox-registry.test.d.ts +2 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/mailbox-registry.test.d.ts.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/mailbox-registry.test.js +193 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/mailbox-registry.test.js.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/token-store.test.js +58 -1
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/token-store.test.js.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/auth/mailbox-registry.d.ts +81 -0
- package/payload/platform/plugins/outlook/mcp/dist/auth/mailbox-registry.d.ts.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/auth/mailbox-registry.js +171 -0
- package/payload/platform/plugins/outlook/mcp/dist/auth/mailbox-registry.js.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/auth/token-store.d.ts +16 -1
- package/payload/platform/plugins/outlook/mcp/dist/auth/token-store.d.ts.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/auth/token-store.js +25 -3
- package/payload/platform/plugins/outlook/mcp/dist/auth/token-store.js.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/index.js +119 -50
- package/payload/platform/plugins/outlook/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/lib/graph-client.js +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/lib/graph-client.js.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/tools/account-register.d.ts +8 -4
- package/payload/platform/plugins/outlook/mcp/dist/tools/account-register.d.ts.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/tools/account-register.js +22 -3
- package/payload/platform/plugins/outlook/mcp/dist/tools/account-register.js.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/tools/mail-list.d.ts +17 -0
- package/payload/platform/plugins/outlook/mcp/dist/tools/mail-list.d.ts.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/tools/mail-list.js +84 -21
- package/payload/platform/plugins/outlook/mcp/dist/tools/mail-list.js.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/tools/mailbox-list.d.ts +26 -0
- package/payload/platform/plugins/outlook/mcp/dist/tools/mailbox-list.d.ts.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/tools/mailbox-list.js +28 -0
- package/payload/platform/plugins/outlook/mcp/dist/tools/mailbox-list.js.map +1 -0
- package/payload/platform/plugins/outlook/references/auth.md +36 -8
- package/payload/platform/plugins/outlook/references/graph-surfaces.md +15 -1
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/publish-availability.d.ts +27 -0
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/publish-availability.d.ts.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/publish-availability.js +241 -0
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/publish-availability.js.map +1 -0
- package/payload/platform/scripts/__tests__/account-schema-owned-dirs.test.sh +100 -0
- package/payload/platform/scripts/lib/account-schema-owned-dirs.py +207 -0
- package/payload/platform/scripts/lib/account-schema-owned-dirs.sh +33 -0
- package/payload/platform/scripts/lib/provision-account-dir.sh +7 -0
- package/payload/platform/scripts/setup-account.sh +10 -0
- package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js +1 -0
- package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/http-server.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/http-server.js +6 -4
- package/payload/platform/services/claude-session-manager/dist/http-server.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/tool-registration-drift.d.ts +7 -6
- package/payload/platform/services/claude-session-manager/dist/tool-registration-drift.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/tool-registration-drift.js +56 -7
- package/payload/platform/services/claude-session-manager/dist/tool-registration-drift.js.map +1 -1
- package/payload/platform/templates/specialists/agents/personal-assistant.md +1 -1
- package/payload/server/{chunk-UGPQCVIW.js → chunk-WC4WJR6X.js} +0 -6
- package/payload/server/maxy-edge.js +1 -1
- package/payload/server/public/assets/{AdminLoginScreens-DeR71H29.js → AdminLoginScreens-CukwQufW.js} +1 -1
- package/payload/server/public/assets/AdminShell-BwEBH6tT.js +2 -0
- package/payload/server/public/assets/{admin-C15h-NL0.js → admin-CdFRCUUZ.js} +1 -1
- package/payload/server/public/assets/{browser-B7xQLIRK.js → browser-BWR59IkB.js} +1 -1
- package/payload/server/public/assets/{calendar-A-vGm6pL.js → calendar-kuTcxX1t.js} +1 -1
- package/payload/server/public/assets/chat-ejnq2rck.js +1 -0
- package/payload/server/public/assets/data-dZbOyLdT.js +1 -0
- package/payload/server/public/assets/{graph-D0orIRAh.js → graph-CboWM2_-.js} +2 -2
- package/payload/server/public/assets/{operator-DZzuotDB.js → operator-oSaB1mya.js} +1 -1
- package/payload/server/public/assets/{page-Ck_pLhAk.js → page-DEkI-Gl1.js} +1 -1
- package/payload/server/public/assets/{page-3g0A19Rv.js → page-DMeJHVfA.js} +5 -5
- package/payload/server/public/assets/{public-WFUQw9AD.js → public-C54tl6V4.js} +1 -1
- package/payload/server/public/assets/{useCopyFeedback-CSk9KjhY.js → useCopyFeedback-DHoYHiK9.js} +1 -1
- package/payload/server/public/assets/{useVoiceRecorder-DCyJQkFK.js → useVoiceRecorder-b7HzTFs0.js} +1 -1
- package/payload/server/public/browser.html +2 -2
- package/payload/server/public/calendar.html +2 -2
- package/payload/server/public/chat.html +5 -5
- package/payload/server/public/data.html +3 -3
- package/payload/server/public/graph.html +3 -3
- package/payload/server/public/index.html +5 -5
- package/payload/server/public/operator.html +6 -6
- package/payload/server/public/public.html +5 -5
- package/payload/server/server.js +255 -112
- package/payload/server/public/assets/AdminShell-DlAVQUda.js +0 -2
- package/payload/server/public/assets/chat-DAae6Z51.js +0 -1
- package/payload/server/public/assets/data-BNWpKx1d.js +0 -1
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dispatcher script: publish-availability
|
|
3
|
+
*
|
|
4
|
+
* Self-contained, like reconcile-bookings. Publishes the install account's
|
|
5
|
+
* computed free/busy into its Cloudflare D1 `availability_snapshot` table so the
|
|
6
|
+
* public booking page's edge Function keeps serving open times while the device
|
|
7
|
+
* is offline. Runs while the device is ONLINE; when the device is off it simply
|
|
8
|
+
* does not run and the edge keeps serving the last snapshot until it returns.
|
|
9
|
+
*
|
|
10
|
+
* It reads the device's own free/busy from the always-running local server
|
|
11
|
+
* (127.0.0.1:$MAXY_UI_INTERNAL_PORT/api/calendar/free-busy — a gate-exempt public
|
|
12
|
+
* path), so it needs no Neo4j and no tunnel/DNS. The account whose free/busy the
|
|
13
|
+
* endpoint serves is the one the server resolved and hands in via
|
|
14
|
+
* PUBLISH_ACCOUNT_DIR / PUBLISH_ACCOUNT_ID.
|
|
15
|
+
*
|
|
16
|
+
* If the local endpoint is unreachable it makes NO D1 write (result=skipped-
|
|
17
|
+
* unreachable), leaving the last edge snapshot intact. A PID lock prevents
|
|
18
|
+
* overlapping runs. Each run records the outcome to a local last-publish.json so
|
|
19
|
+
* the server's audit tick can report snapshot age independent of this job.
|
|
20
|
+
*
|
|
21
|
+
* Observability — one [calendar-availability] op=publish line per run.
|
|
22
|
+
*
|
|
23
|
+
* PLATFORM_ROOT=... MAXY_UI_INTERNAL_PORT=... PUBLISH_ACCOUNT_DIR=... \
|
|
24
|
+
* PUBLISH_ACCOUNT_ID=... node dist/scripts/publish-availability.js
|
|
25
|
+
*/
|
|
26
|
+
import { spawnSync } from "node:child_process";
|
|
27
|
+
import { readFileSync, writeFileSync, existsSync, unlinkSync, mkdirSync, mkdtempSync, rmSync } from "node:fs";
|
|
28
|
+
import { resolve, join, basename } from "node:path";
|
|
29
|
+
import { homedir, tmpdir } from "node:os";
|
|
30
|
+
function resolveConfigDir() {
|
|
31
|
+
const platformRoot = process.env.PLATFORM_ROOT;
|
|
32
|
+
if (!platformRoot)
|
|
33
|
+
throw new Error("PLATFORM_ROOT environment variable is required");
|
|
34
|
+
const brandPath = resolve(platformRoot, "config", "brand.json");
|
|
35
|
+
if (!existsSync(brandPath))
|
|
36
|
+
throw new Error(`brand.json not found at ${brandPath}`);
|
|
37
|
+
const brand = JSON.parse(readFileSync(brandPath, "utf-8"));
|
|
38
|
+
if (!brand.configDir)
|
|
39
|
+
throw new Error(`brand.json at ${brandPath} is missing configDir`);
|
|
40
|
+
return brand.configDir;
|
|
41
|
+
}
|
|
42
|
+
const CONFIG_DIR = resolve(homedir(), resolveConfigDir());
|
|
43
|
+
const LOCK_FILE = resolve(CONFIG_DIR, "publish-availability.lock");
|
|
44
|
+
function acquireLock() {
|
|
45
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
46
|
+
if (existsSync(LOCK_FILE)) {
|
|
47
|
+
const storedPid = readFileSync(LOCK_FILE, "utf-8").trim();
|
|
48
|
+
try {
|
|
49
|
+
process.kill(Number(storedPid), 0);
|
|
50
|
+
return false; // a run is in progress
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
unlinkSync(LOCK_FILE); // stale
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
writeFileSync(LOCK_FILE, String(process.pid));
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
function releaseLock() {
|
|
60
|
+
try {
|
|
61
|
+
if (existsSync(LOCK_FILE) && readFileSync(LOCK_FILE, "utf-8").trim() === String(process.pid)) {
|
|
62
|
+
unlinkSync(LOCK_FILE);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
/* best effort */
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/** Read `KEY=value` pairs from a secrets env file without polluting process.env. */
|
|
70
|
+
function readEnvFile(path) {
|
|
71
|
+
const out = {};
|
|
72
|
+
if (!existsSync(path))
|
|
73
|
+
return out;
|
|
74
|
+
for (const line of readFileSync(path, "utf-8").split("\n")) {
|
|
75
|
+
const m = line.match(/^([A-Z0-9_]+)=(.*)$/);
|
|
76
|
+
if (m)
|
|
77
|
+
out[m[1]] = m[2];
|
|
78
|
+
}
|
|
79
|
+
return out;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Record the outcome for the server's audit tick. lastSuccessAt advances only on
|
|
83
|
+
* a successful D1 write, so a run of failures leaves it stale and the server
|
|
84
|
+
* reports a climbing snapshotAgeMs.
|
|
85
|
+
*/
|
|
86
|
+
function writeLastPublish(accountDir, result, slots, lastSuccessAt) {
|
|
87
|
+
const stateDir = join(accountDir, "state", "booking-availability");
|
|
88
|
+
try {
|
|
89
|
+
mkdirSync(stateDir, { recursive: true });
|
|
90
|
+
writeFileSync(join(stateDir, "last-publish.json"), JSON.stringify({ result, slots, lastSuccessAt, at: new Date().toISOString() }));
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
/* best effort — the log line is the primary signal */
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function readPriorSuccessAt(accountDir) {
|
|
97
|
+
const path = join(accountDir, "state", "booking-availability", "last-publish.json");
|
|
98
|
+
if (!existsSync(path))
|
|
99
|
+
return null;
|
|
100
|
+
try {
|
|
101
|
+
const prior = JSON.parse(readFileSync(path, "utf-8"));
|
|
102
|
+
return prior.lastSuccessAt ?? null;
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/** UTC midnight of `now`, and `days` after it, as `YYYY-MM-DDT00:00:00Z`. */
|
|
109
|
+
function utcMidnight(now, addDays) {
|
|
110
|
+
const d = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate() + addDays));
|
|
111
|
+
return d.toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
112
|
+
}
|
|
113
|
+
async function main() {
|
|
114
|
+
if (!acquireLock()) {
|
|
115
|
+
console.error("[calendar-availability] op=publish result=skipped reason=lock-held");
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
try {
|
|
119
|
+
const accountDir = process.env.PUBLISH_ACCOUNT_DIR;
|
|
120
|
+
if (!accountDir) {
|
|
121
|
+
console.error("[calendar-availability] op=publish result=skipped reason=no-account");
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
const accountId = process.env.PUBLISH_ACCOUNT_ID ?? basename(accountDir);
|
|
125
|
+
const availPath = join(accountDir, "calendar-availability.json");
|
|
126
|
+
if (!existsSync(availPath)) {
|
|
127
|
+
// No booking site for this account — nothing to publish. Not an error.
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
let avail;
|
|
131
|
+
try {
|
|
132
|
+
avail = JSON.parse(readFileSync(availPath, "utf-8"));
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
console.error(`[calendar-availability] op=publish accountId=${accountId} result=error reason=bad-availability-json`);
|
|
136
|
+
writeLastPublish(accountDir, "error", null, readPriorSuccessAt(accountDir));
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const dbName = avail.bookingDbName;
|
|
140
|
+
if (!dbName) {
|
|
141
|
+
console.error(`[calendar-availability] op=publish accountId=${accountId} result=skipped reason=no-booking-db`);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
const secrets = readEnvFile(join(accountDir, "secrets", "cloudflare.env"));
|
|
145
|
+
const token = secrets.CF_PAGES_D1_TOKEN;
|
|
146
|
+
if (!token) {
|
|
147
|
+
console.error(`[calendar-availability] op=publish accountId=${accountId} result=skipped reason=no-pages-d1-token`);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
const port = process.env.MAXY_UI_INTERNAL_PORT;
|
|
151
|
+
if (!port) {
|
|
152
|
+
console.error(`[calendar-availability] op=publish accountId=${accountId} result=error reason=no-internal-port`);
|
|
153
|
+
writeLastPublish(accountDir, "error", null, readPriorSuccessAt(accountDir));
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
// Fetch the device's own computed free/busy over a horizon that covers the
|
|
157
|
+
// page's own (operator-set) window, +1 day of margin for the local-midnight
|
|
158
|
+
// boundary the page renders in.
|
|
159
|
+
const daysAhead = typeof avail.daysAhead === "number" && avail.daysAhead > 0 ? Math.floor(avail.daysAhead) : 14;
|
|
160
|
+
const now = new Date();
|
|
161
|
+
const from = utcMidnight(now, 0);
|
|
162
|
+
const to = utcMidnight(now, daysAhead + 1);
|
|
163
|
+
const nowIso = now.toISOString();
|
|
164
|
+
const url = `http://127.0.0.1:${port}/api/calendar/free-busy?from=${encodeURIComponent(from)}&to=${encodeURIComponent(to)}`;
|
|
165
|
+
let payload;
|
|
166
|
+
try {
|
|
167
|
+
// Bound the read so a wedged server cannot hold the PID lock.
|
|
168
|
+
const res = await fetch(url, { signal: AbortSignal.timeout(30_000) });
|
|
169
|
+
if (!res.ok)
|
|
170
|
+
throw new Error(`HTTP ${res.status}`);
|
|
171
|
+
payload = await res.text();
|
|
172
|
+
}
|
|
173
|
+
catch (err) {
|
|
174
|
+
// Device/local endpoint unreachable — leave the last edge snapshot intact.
|
|
175
|
+
console.error(`[calendar-availability] op=publish accountId=${accountId} result=skipped-unreachable err="${err.message}"`);
|
|
176
|
+
writeLastPublish(accountDir, "skipped-unreachable", null, readPriorSuccessAt(accountDir));
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
// Must be JSON carrying a slots array, or we would poison the snapshot.
|
|
180
|
+
let slotCount;
|
|
181
|
+
try {
|
|
182
|
+
const parsed = JSON.parse(payload);
|
|
183
|
+
if (!Array.isArray(parsed.slots))
|
|
184
|
+
throw new Error("no slots array");
|
|
185
|
+
slotCount = parsed.slots.length;
|
|
186
|
+
}
|
|
187
|
+
catch (err) {
|
|
188
|
+
console.error(`[calendar-availability] op=publish accountId=${accountId} result=skipped reason=bad-payload err="${err.message}"`);
|
|
189
|
+
writeLastPublish(accountDir, "skipped", null, readPriorSuccessAt(accountDir));
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
// Upsert via a temp SQL file — robust for a large payload where a --command
|
|
193
|
+
// argv would be fragile. Single quotes are escaped for the SQL literal. The
|
|
194
|
+
// CREATE first makes this self-healing: a booking site provisioned before
|
|
195
|
+
// this feature (its D1 has `bookings` but no `availability_snapshot`) gets
|
|
196
|
+
// the table here on the first publish, without waiting for a skill re-run.
|
|
197
|
+
const esc = payload.replace(/'/g, "''");
|
|
198
|
+
const sql = `CREATE TABLE IF NOT EXISTS availability_snapshot (
|
|
199
|
+
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
200
|
+
payload TEXT,
|
|
201
|
+
updatedAt TEXT
|
|
202
|
+
);
|
|
203
|
+
INSERT INTO availability_snapshot (id, payload, updatedAt)
|
|
204
|
+
VALUES (1, '${esc}', '${nowIso}')
|
|
205
|
+
ON CONFLICT(id) DO UPDATE SET payload=excluded.payload, updatedAt=excluded.updatedAt;`;
|
|
206
|
+
const workDir = mkdtempSync(join(tmpdir(), "publish-availability-"));
|
|
207
|
+
const sqlFile = join(workDir, "upsert.sql");
|
|
208
|
+
try {
|
|
209
|
+
writeFileSync(sqlFile, sql);
|
|
210
|
+
const wranglerEnv = {
|
|
211
|
+
CLOUDFLARE_API_TOKEN: token,
|
|
212
|
+
...(secrets.CLOUDFLARE_ACCOUNT_ID ? { CLOUDFLARE_ACCOUNT_ID: secrets.CLOUDFLARE_ACCOUNT_ID } : {}),
|
|
213
|
+
};
|
|
214
|
+
const proc = spawnSync("npx", ["wrangler", "d1", "execute", dbName, "--remote", "--file", sqlFile], {
|
|
215
|
+
env: { ...process.env, ...wranglerEnv },
|
|
216
|
+
encoding: "utf-8",
|
|
217
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
218
|
+
timeout: 60_000,
|
|
219
|
+
});
|
|
220
|
+
if (proc.status !== 0) {
|
|
221
|
+
console.error(`[calendar-availability] op=publish accountId=${accountId} result=error reason=d1-write-failed status=${proc.status}${proc.signal ? ` signal=${proc.signal}` : ""}`);
|
|
222
|
+
writeLastPublish(accountDir, "error", slotCount, readPriorSuccessAt(accountDir));
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
finally {
|
|
227
|
+
rmSync(workDir, { recursive: true, force: true });
|
|
228
|
+
}
|
|
229
|
+
console.error(`[calendar-availability] op=publish accountId=${accountId} slots=${slotCount} result=success`);
|
|
230
|
+
writeLastPublish(accountDir, "success", slotCount, nowIso);
|
|
231
|
+
}
|
|
232
|
+
finally {
|
|
233
|
+
releaseLock();
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
main().catch((err) => {
|
|
237
|
+
console.error(`[calendar-availability] op=publish result=error reason=fatal err="${err instanceof Error ? err.message : String(err)}"`);
|
|
238
|
+
releaseLock();
|
|
239
|
+
process.exit(1);
|
|
240
|
+
});
|
|
241
|
+
//# sourceMappingURL=publish-availability.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"publish-availability.js","sourceRoot":"","sources":["../../src/scripts/publish-availability.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Tests the plugin-declared account-owned-dir merge/reconcile lib: a brand that
|
|
3
|
+
# ships a plugin declaring an owned top-level dir gets it unioned into the
|
|
4
|
+
# account SCHEMA.md allowed-top-level set; a brand without the plugin does not;
|
|
5
|
+
# the merge is idempotent; the reconcile check reports present before/after.
|
|
6
|
+
set -uo pipefail
|
|
7
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
8
|
+
PLAT_SCRIPTS="$SCRIPT_DIR/.."
|
|
9
|
+
REAL_PROJECT_DIR="$(cd "$PLAT_SCRIPTS/.." && pwd)"
|
|
10
|
+
REAL_TEMPLATE="$REAL_PROJECT_DIR/templates/account-schema/SCHEMA.md"
|
|
11
|
+
REAL_PREMIUM="$(cd "$REAL_PROJECT_DIR/.." && pwd)/premium-plugins"
|
|
12
|
+
PASS=0; FAIL=0; FAILED=()
|
|
13
|
+
assert_grep() { if printf '%s' "$2" | grep -qF "$1"; then PASS=$((PASS+1)); else FAIL=$((FAIL+1)); FAILED+=("$3: expected to find [$1]"); fi; }
|
|
14
|
+
assert_nogrep() { if printf '%s' "$2" | grep -qF "$1"; then FAIL=$((FAIL+1)); FAILED+=("$3: expected NOT to find [$1]"); else PASS=$((PASS+1)); fi; }
|
|
15
|
+
assert_eq() { if [ "$1" = "$2" ]; then PASS=$((PASS+1)); else FAIL=$((FAIL+1)); FAILED+=("$3: expected [$2] got [$1]"); fi; }
|
|
16
|
+
|
|
17
|
+
. "$PLAT_SCRIPTS/lib/account-schema-owned-dirs.sh"
|
|
18
|
+
|
|
19
|
+
# Build a fake platform tree at $T/platform whose premium sibling is a symlink to
|
|
20
|
+
# the real premium-plugins (so the real sitedesk-job PLUGIN.md is exercised).
|
|
21
|
+
make_tree() { # $1=ships (json array literal)
|
|
22
|
+
local T; T="$(mktemp -d)"
|
|
23
|
+
mkdir -p "$T/platform/plugins" "$T/platform/config" "$T/platform/templates/account-schema"
|
|
24
|
+
ln -s "$REAL_PREMIUM" "$T/premium-plugins"
|
|
25
|
+
cp "$REAL_TEMPLATE" "$T/platform/templates/account-schema/SCHEMA.md"
|
|
26
|
+
printf '{"installDir":"test-brand","shipsPremiumBundles":%s}\n' "$1" > "$T/platform/config/brand.json"
|
|
27
|
+
echo "$T"
|
|
28
|
+
}
|
|
29
|
+
make_account() { # $1=tree
|
|
30
|
+
local A; A="$(mktemp -d)"
|
|
31
|
+
cp "$1/platform/templates/account-schema/SCHEMA.md" "$A/SCHEMA.md"
|
|
32
|
+
printf '{"accountId":"x","role":"house"}\n' > "$A/account.json"
|
|
33
|
+
echo "$A"
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
# --- sitedesk-shipping brand: quoting added ---
|
|
37
|
+
T_SD="$(make_tree '["sitedesk"]')"; A_SD="$(make_account "$T_SD")"
|
|
38
|
+
export PROJECT_DIR="$T_SD/platform"
|
|
39
|
+
OUT="$(merge_owned_dirs_into_schema "$A_SD")"
|
|
40
|
+
assert_grep "op=seed-owned-dirs" "$OUT" "sd-seed-log"
|
|
41
|
+
assert_grep "added=quoting" "$OUT" "sd-added-quoting"
|
|
42
|
+
ALLOWED="$(awk '/^```allowed-top-level$/{f=1;next} /^```$/{f=0} f' "$A_SD/SCHEMA.md")"
|
|
43
|
+
assert_grep "quoting" "$ALLOWED" "sd-allowlist-has-quoting"
|
|
44
|
+
assert_grep "Plugin-owned top-level workspaces" "$(cat "$A_SD/SCHEMA.md")" "sd-descriptive-section"
|
|
45
|
+
assert_grep "plugin-owned-dirs:start" "$(cat "$A_SD/SCHEMA.md")" "sd-marker"
|
|
46
|
+
|
|
47
|
+
# --- idempotency: second merge is byte-identical ---
|
|
48
|
+
BEFORE="$(shasum "$A_SD/SCHEMA.md" | awk '{print $1}')"
|
|
49
|
+
OUT2="$(merge_owned_dirs_into_schema "$A_SD")"
|
|
50
|
+
AFTER="$(shasum "$A_SD/SCHEMA.md" | awk '{print $1}')"
|
|
51
|
+
assert_eq "$AFTER" "$BEFORE" "idempotent-second-merge"
|
|
52
|
+
assert_grep "added=none" "$OUT2" "sd-second-added-none"
|
|
53
|
+
|
|
54
|
+
# --- non-sitedesk brand: no quoting ---
|
|
55
|
+
T_MX="$(make_tree '[]')"; A_MX="$(make_account "$T_MX")"
|
|
56
|
+
export PROJECT_DIR="$T_MX/platform"
|
|
57
|
+
merge_owned_dirs_into_schema "$A_MX" >/dev/null
|
|
58
|
+
ALLOWED_MX="$(awk '/^```allowed-top-level$/{f=1;next} /^```$/{f=0} f' "$A_MX/SCHEMA.md")"
|
|
59
|
+
assert_nogrep "quoting" "$ALLOWED_MX" "mx-no-quoting"
|
|
60
|
+
assert_nogrep "Plugin-owned top-level workspaces" "$(cat "$A_MX/SCHEMA.md")" "mx-no-section"
|
|
61
|
+
|
|
62
|
+
# --- reconcile: present=false on bare schema, present=true after merge ---
|
|
63
|
+
A_RC="$(make_account "$T_SD")"
|
|
64
|
+
export PROJECT_DIR="$T_SD/platform"
|
|
65
|
+
RC_BEFORE="$(PROJECT_DIR="$PROJECT_DIR" python3 "$PLAT_SCRIPTS/lib/account-schema-owned-dirs.py" reconcile "$A_RC")"
|
|
66
|
+
assert_grep "ownedDir=quoting present=false" "$RC_BEFORE" "rc-present-false"
|
|
67
|
+
merge_owned_dirs_into_schema "$A_RC" >/dev/null
|
|
68
|
+
RC_AFTER="$(PROJECT_DIR="$PROJECT_DIR" python3 "$PLAT_SCRIPTS/lib/account-schema-owned-dirs.py" reconcile "$A_RC")"
|
|
69
|
+
assert_grep "ownedDir=quoting present=true" "$RC_AFTER" "rc-present-true"
|
|
70
|
+
|
|
71
|
+
# --- sweep: backfills every account with an account.json ---
|
|
72
|
+
ACCTS="$(mktemp -d)"; mkdir -p "$ACCTS/a1"
|
|
73
|
+
cp "$T_SD/platform/templates/account-schema/SCHEMA.md" "$ACCTS/a1/SCHEMA.md"
|
|
74
|
+
printf '{"accountId":"a1","role":"client"}\n' > "$ACCTS/a1/account.json"
|
|
75
|
+
SWEEP="$(reconcile_all_accounts_owned_dirs "$ACCTS" 2>&1)"
|
|
76
|
+
assert_grep "op=reconcile" "$SWEEP" "sweep-reconcile-log"
|
|
77
|
+
ALLOWED_A1="$(awk '/^```allowed-top-level$/{f=1;next} /^```$/{f=0} f' "$ACCTS/a1/SCHEMA.md")"
|
|
78
|
+
assert_grep "quoting" "$ALLOWED_A1" "sweep-backfilled-a1"
|
|
79
|
+
|
|
80
|
+
# --- shape-b: a bundle-root PLUGIN.md declaration is resolved too ---
|
|
81
|
+
# A premium bundle may declare an owned dir in its root PLUGIN.md (depth 1),
|
|
82
|
+
# not only in a sub-plugin manifest (depth 2). Build a fully synthetic tree.
|
|
83
|
+
TB="$(mktemp -d)"
|
|
84
|
+
mkdir -p "$TB/platform/plugins" "$TB/platform/config" "$TB/platform/templates/account-schema"
|
|
85
|
+
mkdir -p "$TB/premium-plugins/acme/plugins/acme-sub"
|
|
86
|
+
cp "$REAL_TEMPLATE" "$TB/platform/templates/account-schema/SCHEMA.md"
|
|
87
|
+
printf '{"installDir":"acme-brand","shipsPremiumBundles":["acme"]}\n' > "$TB/platform/config/brand.json"
|
|
88
|
+
printf -- '---\nname: acme\naccount-owned-dirs: [{"dir": "ledger", "description": "Acme ledger."}]\n---\n' > "$TB/premium-plugins/acme/PLUGIN.md"
|
|
89
|
+
printf -- '---\nname: acme-sub\n---\n' > "$TB/premium-plugins/acme/plugins/acme-sub/PLUGIN.md"
|
|
90
|
+
A_TB="$(mktemp -d)"; cp "$TB/platform/templates/account-schema/SCHEMA.md" "$A_TB/SCHEMA.md"
|
|
91
|
+
export PROJECT_DIR="$TB/platform"
|
|
92
|
+
RES_TB="$(PROJECT_DIR="$PROJECT_DIR" python3 "$PLAT_SCRIPTS/lib/account-schema-owned-dirs.py" resolve)"
|
|
93
|
+
assert_grep "ledger" "$RES_TB" "shapeb-resolve-ledger"
|
|
94
|
+
assert_grep "\"plugin\": \"acme\"" "$RES_TB" "shapeb-owner-is-bundle"
|
|
95
|
+
merge_owned_dirs_into_schema "$A_TB" >/dev/null
|
|
96
|
+
ALLOWED_TB="$(awk '/^```allowed-top-level$/{f=1;next} /^```$/{f=0} f' "$A_TB/SCHEMA.md")"
|
|
97
|
+
assert_grep "ledger" "$ALLOWED_TB" "shapeb-allowlist-has-ledger"
|
|
98
|
+
|
|
99
|
+
echo "PASS=$PASS FAIL=$FAIL"
|
|
100
|
+
if [ "$FAIL" -ne 0 ]; then printf '%s\n' "${FAILED[@]}"; exit 1; fi
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Resolve plugin-declared account-owned top-level dirs for the brand and merge
|
|
3
|
+
them into an account's SCHEMA.md.
|
|
4
|
+
|
|
5
|
+
A plugin declares owned top-level account dirs in its PLUGIN.md frontmatter:
|
|
6
|
+
|
|
7
|
+
account-owned-dirs: [{"dir": "quoting", "description": "..."}]
|
|
8
|
+
|
|
9
|
+
The owned-dir set for a brand is the union declared by every platform plugin
|
|
10
|
+
($PROJECT_DIR/plugins/*/PLUGIN.md) and every premium sub-plugin of a bundle the
|
|
11
|
+
brand ships ($PREMIUM_ROOT/<bundle>/plugins/*/PLUGIN.md, gated by brand.json
|
|
12
|
+
shipsPremiumBundles). Reads PROJECT_DIR from the environment.
|
|
13
|
+
"""
|
|
14
|
+
import json, os, re, sys
|
|
15
|
+
|
|
16
|
+
MARK_START = "<!-- plugin-owned-dirs:start -->"
|
|
17
|
+
MARK_END = "<!-- plugin-owned-dirs:end -->"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _project_dir():
|
|
21
|
+
pd = os.environ.get("PROJECT_DIR", "")
|
|
22
|
+
if not pd:
|
|
23
|
+
sys.stderr.write("account-schema-owned-dirs: PROJECT_DIR unset\n")
|
|
24
|
+
sys.exit(1)
|
|
25
|
+
return os.path.normpath(pd)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _read_brand(project_dir):
|
|
29
|
+
path = os.path.join(project_dir, "config", "brand.json")
|
|
30
|
+
if not os.path.isfile(path):
|
|
31
|
+
return "unknown", []
|
|
32
|
+
try:
|
|
33
|
+
with open(path) as f:
|
|
34
|
+
b = json.load(f)
|
|
35
|
+
return (b.get("installDir") or "unknown"), list(b.get("shipsPremiumBundles") or [])
|
|
36
|
+
except Exception:
|
|
37
|
+
return "unknown", []
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _parse_plugin_md(path):
|
|
41
|
+
"""Return (name, [ {dir, description} ]) from a PLUGIN.md, or (None, [])."""
|
|
42
|
+
try:
|
|
43
|
+
with open(path) as f:
|
|
44
|
+
text = f.read()
|
|
45
|
+
except Exception:
|
|
46
|
+
return None, []
|
|
47
|
+
m = re.match(r"^---\n(.*?)\n---", text, re.DOTALL)
|
|
48
|
+
if not m:
|
|
49
|
+
return None, []
|
|
50
|
+
fm = m.group(1)
|
|
51
|
+
name = None
|
|
52
|
+
nm = re.search(r"^name:\s*(.+)$", fm, re.MULTILINE)
|
|
53
|
+
if nm:
|
|
54
|
+
name = nm.group(1).strip().strip('"').strip("'")
|
|
55
|
+
owned = []
|
|
56
|
+
om = re.search(r"^account-owned-dirs:\s*(.+)$", fm, re.MULTILINE)
|
|
57
|
+
if om:
|
|
58
|
+
try:
|
|
59
|
+
raw = json.loads(om.group(1).strip())
|
|
60
|
+
for e in raw:
|
|
61
|
+
d = (e.get("dir") or "").strip()
|
|
62
|
+
if d:
|
|
63
|
+
owned.append({"dir": d, "description": (e.get("description") or "").strip()})
|
|
64
|
+
except Exception:
|
|
65
|
+
pass
|
|
66
|
+
return name, owned
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _plugin_md_paths(project_dir):
|
|
70
|
+
paths = []
|
|
71
|
+
plugins_root = os.path.join(project_dir, "plugins")
|
|
72
|
+
if os.path.isdir(plugins_root):
|
|
73
|
+
for entry in sorted(os.listdir(plugins_root)):
|
|
74
|
+
p = os.path.join(plugins_root, entry, "PLUGIN.md")
|
|
75
|
+
if os.path.isfile(p):
|
|
76
|
+
paths.append(p)
|
|
77
|
+
_brand, ships = _read_brand(project_dir)
|
|
78
|
+
premium_root = os.path.join(os.path.dirname(project_dir), "premium-plugins")
|
|
79
|
+
for bundle in ships:
|
|
80
|
+
bundle_dir = os.path.join(premium_root, bundle)
|
|
81
|
+
# Bundle-root manifest (shape b): premium-plugins/<bundle>/PLUGIN.md.
|
|
82
|
+
root_md = os.path.join(bundle_dir, "PLUGIN.md")
|
|
83
|
+
if os.path.isfile(root_md):
|
|
84
|
+
paths.append(root_md)
|
|
85
|
+
# Sub-plugin manifests (shape a): premium-plugins/<bundle>/plugins/*/PLUGIN.md.
|
|
86
|
+
subs = os.path.join(bundle_dir, "plugins")
|
|
87
|
+
if not os.path.isdir(subs):
|
|
88
|
+
continue
|
|
89
|
+
for entry in sorted(os.listdir(subs)):
|
|
90
|
+
p = os.path.join(subs, entry, "PLUGIN.md")
|
|
91
|
+
if os.path.isfile(p):
|
|
92
|
+
paths.append(p)
|
|
93
|
+
return paths
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def resolve(project_dir):
|
|
97
|
+
"""Union of owned-dir declarations. First declarer of a dir wins its owner
|
|
98
|
+
label + description."""
|
|
99
|
+
seen = {}
|
|
100
|
+
order = []
|
|
101
|
+
for path in _plugin_md_paths(project_dir):
|
|
102
|
+
name, owned = _parse_plugin_md(path)
|
|
103
|
+
for e in owned:
|
|
104
|
+
if e["dir"] in seen:
|
|
105
|
+
continue
|
|
106
|
+
seen[e["dir"]] = {"dir": e["dir"], "description": e["description"], "plugin": name or "unknown"}
|
|
107
|
+
order.append(e["dir"])
|
|
108
|
+
return [seen[d] for d in order]
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _allowed_block(text):
|
|
112
|
+
"""Return (list_of_entries, start_idx, end_idx) for the fenced
|
|
113
|
+
allowed-top-level block. end_idx is the closing-fence line index."""
|
|
114
|
+
lines = text.split("\n")
|
|
115
|
+
start = None
|
|
116
|
+
for i, ln in enumerate(lines):
|
|
117
|
+
if ln.strip() == "```allowed-top-level":
|
|
118
|
+
start = i
|
|
119
|
+
break
|
|
120
|
+
if start is None:
|
|
121
|
+
return None, None, None
|
|
122
|
+
for j in range(start + 1, len(lines)):
|
|
123
|
+
if lines[j].strip() == "```":
|
|
124
|
+
return [l for l in lines[start + 1:j] if l.strip()], start, j
|
|
125
|
+
return None, None, None
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _strip_region(text):
|
|
129
|
+
if MARK_START not in text:
|
|
130
|
+
return text
|
|
131
|
+
pre = text.split(MARK_START, 1)[0]
|
|
132
|
+
post = ""
|
|
133
|
+
if MARK_END in text:
|
|
134
|
+
post = text.split(MARK_END, 1)[1]
|
|
135
|
+
return (pre.rstrip("\n") + "\n" + post.lstrip("\n")).rstrip("\n") + "\n"
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def merge(project_dir, account_dir):
|
|
139
|
+
brand, _ = _read_brand(project_dir)
|
|
140
|
+
owned = resolve(project_dir)
|
|
141
|
+
schema_path = os.path.join(account_dir, "SCHEMA.md")
|
|
142
|
+
if not os.path.isfile(schema_path):
|
|
143
|
+
print(f"brand={brand} added=none")
|
|
144
|
+
return
|
|
145
|
+
with open(schema_path) as f:
|
|
146
|
+
text = f.read()
|
|
147
|
+
entries, start, end = _allowed_block(text)
|
|
148
|
+
added = []
|
|
149
|
+
if entries is not None:
|
|
150
|
+
present = set(entries)
|
|
151
|
+
lines = text.split("\n")
|
|
152
|
+
insert = []
|
|
153
|
+
for e in owned:
|
|
154
|
+
if e["dir"] not in present:
|
|
155
|
+
insert.append(e["dir"])
|
|
156
|
+
added.append(e["dir"])
|
|
157
|
+
if insert:
|
|
158
|
+
lines[end:end] = insert
|
|
159
|
+
text = "\n".join(lines)
|
|
160
|
+
# Regenerate the descriptive region wholesale (idempotent).
|
|
161
|
+
text = _strip_region(text)
|
|
162
|
+
if owned:
|
|
163
|
+
region = [MARK_START, "## Plugin-owned top-level workspaces", "",
|
|
164
|
+
"A plugin that owns a top-level workspace declares it. The internal",
|
|
165
|
+
"structure is owned by that plugin's tools and recreated on the next",
|
|
166
|
+
"write. Never reorganise it.", ""]
|
|
167
|
+
for e in owned:
|
|
168
|
+
desc = e["description"] or f"Owned by the {e['plugin']} plugin."
|
|
169
|
+
region.append(f"- `{e['dir']}/` — {desc}")
|
|
170
|
+
region.append(MARK_END)
|
|
171
|
+
text = text.rstrip("\n") + "\n\n" + "\n".join(region) + "\n"
|
|
172
|
+
with open(schema_path, "w") as f:
|
|
173
|
+
f.write(text)
|
|
174
|
+
print(f"brand={brand} added={','.join(added) if added else 'none'}")
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def reconcile(project_dir, account_dir):
|
|
178
|
+
brand, _ = _read_brand(project_dir)
|
|
179
|
+
owned = resolve(project_dir)
|
|
180
|
+
schema_path = os.path.join(account_dir, "SCHEMA.md")
|
|
181
|
+
present = set()
|
|
182
|
+
if os.path.isfile(schema_path):
|
|
183
|
+
with open(schema_path) as f:
|
|
184
|
+
entries, _s, _e = _allowed_block(f.read())
|
|
185
|
+
present = set(entries or [])
|
|
186
|
+
for e in owned:
|
|
187
|
+
is_present = "true" if e["dir"] in present else "false"
|
|
188
|
+
print(f"brand={brand} plugin={e['plugin']} ownedDir={e['dir']} present={is_present}")
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def main():
|
|
192
|
+
if len(sys.argv) < 2:
|
|
193
|
+
sys.exit("usage: account-schema-owned-dirs.py {resolve|merge|reconcile} [account_dir]")
|
|
194
|
+
cmd = sys.argv[1]
|
|
195
|
+
pd = _project_dir()
|
|
196
|
+
if cmd == "resolve":
|
|
197
|
+
print(json.dumps(resolve(pd)))
|
|
198
|
+
elif cmd == "merge":
|
|
199
|
+
merge(pd, sys.argv[2])
|
|
200
|
+
elif cmd == "reconcile":
|
|
201
|
+
reconcile(pd, sys.argv[2])
|
|
202
|
+
else:
|
|
203
|
+
sys.exit(f"unknown command: {cmd}")
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
if __name__ == "__main__":
|
|
207
|
+
main()
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# account-schema-owned-dirs.sh — merge plugin-declared owned top-level dirs into
|
|
2
|
+
# an account's SCHEMA.md, and reconcile every account at install time.
|
|
3
|
+
#
|
|
4
|
+
# Requires PROJECT_DIR exported by the caller. Sourced by provision-account-dir.sh
|
|
5
|
+
# and setup-account.sh.
|
|
6
|
+
|
|
7
|
+
_ACCT_SCHEMA_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
8
|
+
_ACCT_SCHEMA_PY="$_ACCT_SCHEMA_LIB_DIR/account-schema-owned-dirs.py"
|
|
9
|
+
|
|
10
|
+
# merge_owned_dirs_into_schema <account_dir>
|
|
11
|
+
# Unions the brand's declared owned dirs into <account_dir>/SCHEMA.md and logs
|
|
12
|
+
# the seam. Idempotent.
|
|
13
|
+
merge_owned_dirs_into_schema() {
|
|
14
|
+
local _acct="$1"
|
|
15
|
+
local _out
|
|
16
|
+
_out="$(PROJECT_DIR="$PROJECT_DIR" python3 "$_ACCT_SCHEMA_PY" merge "$_acct" 2>/dev/null)"
|
|
17
|
+
echo " [acct-schema] op=seed-owned-dirs ${_out}"
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
# reconcile_all_accounts_owned_dirs <accounts_dir>
|
|
21
|
+
# Standing check + backfill: for every account dir with an account.json, log the
|
|
22
|
+
# pre-merge present state of each declared owned dir, then merge (backfill).
|
|
23
|
+
reconcile_all_accounts_owned_dirs() {
|
|
24
|
+
local _accts="$1"
|
|
25
|
+
[ -d "$_accts" ] || return 0
|
|
26
|
+
local _acct
|
|
27
|
+
for _acct in "$_accts"/*/; do
|
|
28
|
+
[ -f "$_acct/account.json" ] || continue
|
|
29
|
+
PROJECT_DIR="$PROJECT_DIR" python3 "$_ACCT_SCHEMA_PY" reconcile "$_acct" 2>/dev/null \
|
|
30
|
+
| while IFS= read -r _line; do echo " [acct-schema] op=reconcile ${_line}"; done
|
|
31
|
+
merge_owned_dirs_into_schema "$_acct"
|
|
32
|
+
done
|
|
33
|
+
}
|
|
@@ -15,6 +15,9 @@
|
|
|
15
15
|
# SCRIPT_DIR — the platform/scripts dir (for lib bootstraps)
|
|
16
16
|
# USERS_FILE — $HOME/<brand.configDir>/users.json (owner stamp; optional)
|
|
17
17
|
|
|
18
|
+
# Owned-dir schema merge (plugin-declared top-level account dirs).
|
|
19
|
+
. "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/account-schema-owned-dirs.sh"
|
|
20
|
+
|
|
18
21
|
provision_account_dir() {
|
|
19
22
|
local ACCOUNT_DIR="$1"
|
|
20
23
|
local ACCOUNT_ID="$2"
|
|
@@ -38,6 +41,10 @@ provision_account_dir() {
|
|
|
38
41
|
fi
|
|
39
42
|
echo " [acct-schema] seeded dirs=$_seeded"
|
|
40
43
|
|
|
44
|
+
# Merge brand-shipped plugin-declared owned top-level dirs into the freshly
|
|
45
|
+
# seeded SCHEMA.md so the guard admits them (e.g. quoting/ on SiteDesk).
|
|
46
|
+
merge_owned_dirs_into_schema "$ACCOUNT_DIR"
|
|
47
|
+
|
|
41
48
|
# Claude Code discovers project-level .claude/ relative to the nearest .git
|
|
42
49
|
# root. Without a .git in the account dir it traverses up to ~/maxy/.git and
|
|
43
50
|
# misses accountDir/.claude/. Initialise a repo so the account dir IS the root.
|
|
@@ -53,3 +53,13 @@ USERS_FILE="$USERS_FILE" resolve_and_sweep_account_dir
|
|
|
53
53
|
# designation (migrating a legacy unlabelled account in the same step).
|
|
54
54
|
export TEMPLATES_DIR PROJECT_DIR SCRIPT_DIR USERS_FILE
|
|
55
55
|
provision_account_dir "$ACCOUNT_DIR" "$ACCOUNT_ID" "house"
|
|
56
|
+
|
|
57
|
+
# ------------------------------------------------------------------
|
|
58
|
+
# 3. Reconcile plugin-declared owned top-level dirs across every account
|
|
59
|
+
# ------------------------------------------------------------------
|
|
60
|
+
# Standing check + durable backfill: every account under data/accounts/ gets the
|
|
61
|
+
# brand's plugin-declared owned dirs unioned into its SCHEMA.md. Runs on every
|
|
62
|
+
# install so a re-provision cannot drop a previously-added owned dir.
|
|
63
|
+
# reconcile_all_accounts_owned_dirs is available because provision-account-dir.sh
|
|
64
|
+
# (sourced above) sources the owned-dir lib.
|
|
65
|
+
reconcile_all_accounts_owned_dirs "$ACCOUNTS_DIR"
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"canonical-tool-names.generated.d.ts","sourceRoot":"","sources":["../src/canonical-tool-names.generated.ts"],"names":[],"mappings":"AAQA,+EAA+E;AAC/E,eAAO,MAAM,gBAAgB,EAAE,SAAS,MAAM,EAqB7C,CAAA;AAED,gEAAgE;AAChE,eAAO,MAAM,yBAAyB,EAAE,SAAS,MAAM,
|
|
1
|
+
{"version":3,"file":"canonical-tool-names.generated.d.ts","sourceRoot":"","sources":["../src/canonical-tool-names.generated.ts"],"names":[],"mappings":"AAQA,+EAA+E;AAC/E,eAAO,MAAM,gBAAgB,EAAE,SAAS,MAAM,EAqB7C,CAAA;AAED,gEAAgE;AAChE,eAAO,MAAM,yBAAyB,EAAE,SAAS,MAAM,EA6QtD,CAAA"}
|
package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js
CHANGED
|
@@ -232,6 +232,7 @@ export const CANONICAL_MAXY_TOOL_NAMES = [
|
|
|
232
232
|
"mcp__plugin_outlook_outlook__outlook-mail-search",
|
|
233
233
|
"mcp__plugin_outlook_outlook__outlook-mail-send",
|
|
234
234
|
"mcp__plugin_outlook_outlook__outlook-mailbox-info",
|
|
235
|
+
"mcp__plugin_outlook_outlook__outlook-mailbox-list",
|
|
235
236
|
"mcp__plugin_quickbooks_quickbooks__quickbooks-authorize-url",
|
|
236
237
|
"mcp__plugin_quickbooks_quickbooks__quickbooks-bill-create",
|
|
237
238
|
"mcp__plugin_quickbooks_quickbooks__quickbooks-connections",
|