thumbgate 1.37.2 → 1.37.3

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.
Files changed (40) hide show
  1. package/.agents/skills/article-syndication-pipeline/SKILL.md +37 -0
  2. package/.agents/skills/ci-gha-buildkite-patterns-not-clone/SKILL.md +72 -0
  3. package/.agents/skills/comet-session-logger/SKILL.md +59 -0
  4. package/.agents/skills/datadog-llm-obs-compare-not-clone/SKILL.md +75 -0
  5. package/.agents/skills/deeppattern-discipline-honesty-not-clone/SKILL.md +73 -0
  6. package/.agents/skills/thumbgate-board-loop/SKILL.md +72 -0
  7. package/.agents/skills/thumbgate-daily-discoveries/SKILL.md +57 -0
  8. package/.agents/skills/typesafe-typed-questions-not-clone/SKILL.md +7 -3
  9. package/.claude-plugin/plugin.json +1 -1
  10. package/.well-known/mcp/server-card.json +1 -1
  11. package/adapters/claude/.mcp.json +2 -2
  12. package/adapters/forge/forge.yaml +3 -3
  13. package/adapters/future-agi/.mcp.json +1 -1
  14. package/adapters/future-agi/config.toml +1 -1
  15. package/adapters/future-agi/opencode.json +1 -1
  16. package/adapters/herdr/herdr-plugin.toml +2 -2
  17. package/adapters/mcp/server-stdio.js +1 -1
  18. package/adapters/opencode/opencode.json +1 -1
  19. package/bin/cli.js +132 -4
  20. package/config/gate-templates.json +48 -0
  21. package/package.json +36 -9
  22. package/public/blog/2026-09-23-datadog-llm-obs-four-practices.html +42 -0
  23. package/public/index.html +2 -2
  24. package/public/install.html +7 -7
  25. package/public/numbers.html +2 -2
  26. package/scripts/ci-gha-buildkite-patterns.js +393 -0
  27. package/scripts/cli-schema.js +51 -1
  28. package/scripts/deeppattern-discipline-honesty.js +431 -0
  29. package/scripts/install-thumbgate-daily-publish-launchd.sh +83 -0
  30. package/scripts/llm-obs-honesty.js +478 -0
  31. package/scripts/thumbgate-board-loop.js +432 -0
  32. package/scripts/thumbgate-daily-discoveries-publish.js +413 -0
  33. package/scripts/thumbgate-daily-discoveries-publish.sh +18 -0
  34. package/scripts/typesafe-typed-questions.js +332 -7
  35. package/server.json +2 -2
  36. package/skills/article-syndication-pipeline/SKILL.md +37 -0
  37. package/skills/comet-session-logger/SKILL.md +59 -0
  38. package/skills/thumbgate-daily-discoveries/SKILL.md +57 -0
  39. package/src/index.js +10 -0
  40. package/src/integrations/ideabrowser-connector.js +190 -0
@@ -0,0 +1,431 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * DeepPattern (AQG + Decision Engine) FORMAT steal — not a clone.
6
+ *
7
+ * Sources (public, MIT, 2026-09):
8
+ * https://github.com/deeppatternai/agent-quality-gates
9
+ * https://github.com/deeppatternai/decision-engine
10
+ *
11
+ * Transfers:
12
+ * 1. layer-check — refuse treating products at different stack layers as substitutes
13
+ * (DE /layer-check). Primary failure: L4 transport/API gateway as "competitor" to
14
+ * L7 PreToolUse governance; or multi-model panel SaaS as ThumbGate substitute.
15
+ * 2. evidence-closeout — six required evidence items before claiming done
16
+ * (AQG check_evidence_closeout). Maps onto ThumbGate Completion Claim Contract.
17
+ *
18
+ * Does NOT install AQG, Decision Engine, dp-install, or a cross-vendor audit SKU.
19
+ * Does NOT unpark LLM adjudicator (#3690/#3687). Hosted multi-model panels stay external.
20
+ */
21
+
22
+ const fs = require('node:fs');
23
+ const path = require('node:path');
24
+
25
+ const SOURCE_AQG = 'https://github.com/deeppatternai/agent-quality-gates';
26
+ const SOURCE_DE = 'https://github.com/deeppatternai/decision-engine';
27
+
28
+ const LAYERS = Object.freeze({
29
+ L1: 'Metal / hardware',
30
+ L2: 'Foundation model',
31
+ L3: 'Training / eval infrastructure',
32
+ L4: 'Transport / API gateway',
33
+ L5: 'Orchestration framework',
34
+ L6: 'Developer tooling platform',
35
+ L7: 'Application / workflow SaaS',
36
+ L8: 'Brand / distribution surface',
37
+ });
38
+
39
+ const THUMBGATE_LAYER = 'L7';
40
+
41
+ const REQUIRED_CLOSEOUT_ITEMS = Object.freeze([
42
+ 'scope completed',
43
+ 'verification run',
44
+ 'audit adjudicated',
45
+ 'durable state updated',
46
+ 'production boundary',
47
+ 'remaining blockers',
48
+ ]);
49
+
50
+ const EMPTY_CLOSEOUT_VALUES = new Set([
51
+ '', '-', '--', 'n/a', 'na', 'todo', 'tbd', 'pending',
52
+ ]);
53
+
54
+ const CLONE_RE = /\b(dp-install|agent-quality-gates|decision-engine|aqg_doctor|deeppattern|install_aqg|\/audit-forecast|\/discussion-board)\b/i;
55
+ const SUBSTITUTE_RE = /\b(replace[sd]?|substitut\w*|commoditiz\w*|competes? with|instead of|drop thumbgate for|thumbgate is just|same as thumbgate)\b/i;
56
+ const CROSS_LAYER_SUBJECTS = [
57
+ { re: /\b(api gateway|token aggregat\w*|openrouter|litellm|multi-?vendor (api|token)|raw (model )?api)\b/i, layer: 'L4', label: 'transport / API gateway' },
58
+ { re: /\b(langchain|langgraph|crewai|autogen|agent framework|orchestration framework)\b/i, layer: 'L5', label: 'orchestration framework' },
59
+ { re: /\b(foundation model|gpt-5|claude-4|raw llm|base model family)\b/i, layer: 'L2', label: 'foundation model' },
60
+ { re: /\b(decision engine|cross-vendor panel|multi-?model (panel|audit)|hosted audit engine)\b/i, layer: 'L7_adjacent', label: 'hosted multi-model panel (complement, not PreToolUse gate)' },
61
+ ];
62
+
63
+ const RAIL_MAP = Object.freeze([
64
+ {
65
+ deeppattern: 'layer-check: same job-to-be-done before calling something a substitute',
66
+ thumbgate: 'PreToolUse evaluate→block→evidence is L7 governance; L4 gateways are inputs',
67
+ },
68
+ {
69
+ deeppattern: 'AQG evidence-closeout: six required items before "done"',
70
+ thumbgate: 'Completion Claim Contract — merge SHA / /health.buildSha / terminal CI',
71
+ },
72
+ {
73
+ deeppattern: 'AQG local discipline + DE hosted panel as a pair',
74
+ thumbgate: 'Deterministic gates stay local; hosted multi-model audit is optional external',
75
+ },
76
+ {
77
+ deeppattern: 'Never invent forecast numbers; pool existing sources',
78
+ thumbgate: 'Never invent ROI; never claim LIVE without same-session evidence',
79
+ },
80
+ ]);
81
+
82
+ function normalizeBoolean(value) {
83
+ if (value === true || value === 1) return true;
84
+ if (value === false || value === 0 || value == null) return false;
85
+ return /^(1|true|yes|on)$/i.test(String(value).trim());
86
+ }
87
+
88
+ function normalizeLabel(value) {
89
+ return String(value || '')
90
+ .trim()
91
+ .replace(/[*_`|]/g, '')
92
+ .replace(/[^a-z0-9 \-/]/gi, ' ')
93
+ .replace(/[/-]/g, ' ')
94
+ .replace(/\s+/g, ' ')
95
+ .trim()
96
+ .toLowerCase();
97
+ }
98
+
99
+ function isPlaceholder(value) {
100
+ const stripped = String(value || '').trim();
101
+ const lowered = stripped.toLowerCase();
102
+ const normalized = normalizeLabel(stripped);
103
+ if (EMPTY_CLOSEOUT_VALUES.has(normalized)) return true;
104
+ if (/\b(todo|tbd)\b/.test(lowered)) return true;
105
+ if (/__fill/.test(lowered)) return true;
106
+ if (/<[^>]+>/.test(stripped)) return true;
107
+ const compact = lowered.replace(/[^a-z0-9]/g, '');
108
+ return compact.length > 0 && compact.length < 3;
109
+ }
110
+
111
+ function headingSpans(text) {
112
+ const pattern = /^(#{1,6})\s+(.+?)\s*$/gm;
113
+ const matches = [...String(text || '').matchAll(pattern)];
114
+ const spans = [];
115
+ for (let i = 0; i < matches.length; i += 1) {
116
+ const match = matches[i];
117
+ const start = match.index + match[0].length;
118
+ const end = i + 1 < matches.length ? matches[i + 1].index : text.length;
119
+ spans.push({ title: match[2].trim(), body: text.slice(start, end) });
120
+ }
121
+ return spans;
122
+ }
123
+
124
+ function isEvidenceHeading(title) {
125
+ const normalized = normalizeLabel(title);
126
+ return normalized.includes('evidence') || normalized.includes('closeout') || title.includes('证据');
127
+ }
128
+
129
+ function extractCloseoutItems(text) {
130
+ const blocks = headingSpans(text)
131
+ .filter((span) => isEvidenceHeading(span.title))
132
+ .map((span) => span.body);
133
+ if (!blocks.length && REQUIRED_CLOSEOUT_ITEMS.some((item) => normalizeLabel(text).includes(item))) {
134
+ blocks.push(text);
135
+ }
136
+
137
+ const found = {};
138
+ for (const block of blocks) {
139
+ for (const line of block.split(/\r?\n/)) {
140
+ const trimmed = line.trim();
141
+ if (!trimmed) continue;
142
+ if (trimmed.startsWith('|') && trimmed.endsWith('|')) {
143
+ const cells = trimmed.slice(1, -1).split('|').map((c) => c.trim());
144
+ if (cells.length < 2) continue;
145
+ if (cells.every((c) => /^[-:\s]+$/.test(c))) continue;
146
+ const label = normalizeLabel(cells[0]);
147
+ if (REQUIRED_CLOSEOUT_ITEMS.includes(label)) {
148
+ found[label] = cells.slice(1).join(' | ').trim();
149
+ }
150
+ continue;
151
+ }
152
+ const colon = /^(?:\*\*)?([^:*|]{3,80}?)(?:\*\*)?\s*:\s*(.+)$/.exec(
153
+ trimmed.replace(/^[-*]\s+/, '')
154
+ );
155
+ if (!colon) continue;
156
+ const label = normalizeLabel(colon[1]);
157
+ if (REQUIRED_CLOSEOUT_ITEMS.includes(label)) {
158
+ found[label] = colon[2].trim();
159
+ }
160
+ }
161
+ }
162
+ return {
163
+ hasHeading: headingSpans(text).some((span) => isEvidenceHeading(span.title)),
164
+ found,
165
+ };
166
+ }
167
+
168
+ function normalizeOptions(raw = {}) {
169
+ return {
170
+ claim: String(raw.claim || ''),
171
+ subjectLayer: String(raw['subject-layer'] || raw.subjectLayer || THUMBGATE_LAYER).toUpperCase(),
172
+ closeoutPath: raw.closeout || raw['closeout-path'] || raw.closeoutPath || null,
173
+ closeoutText: raw['closeout-text'] || raw.closeoutText || null,
174
+ mapOnly: normalizeBoolean(raw['map-only'] || raw.mapOnly),
175
+ claimReady: normalizeBoolean(raw['claim-ready'] || raw.claimReady),
176
+ cloneDeeppattern: normalizeBoolean(raw['clone-deeppattern'] || raw.cloneDeeppattern || raw['clone-aqg'] || raw.cloneAqg),
177
+ json: normalizeBoolean(raw.json),
178
+ strict: normalizeBoolean(raw.strict),
179
+ };
180
+ }
181
+
182
+ function buildDeeppatternDisciplineHonestyReport(rawOptions = {}) {
183
+ const options = normalizeOptions(rawOptions);
184
+ const findings = [];
185
+ const claim = options.claim;
186
+
187
+ if (options.cloneDeeppattern || CLONE_RE.test(claim)) {
188
+ findings.push({
189
+ id: 'deeppattern_sku_clone',
190
+ severity: 'fail',
191
+ gateId: 'refuse-deeppattern-sku-clone',
192
+ message: 'Refused cloning Agent Quality Gates / Decision Engine / dp-install as a ThumbGate SKU. Steal FORMAT only.',
193
+ });
194
+ }
195
+
196
+ if (claim && SUBSTITUTE_RE.test(claim)) {
197
+ for (const subject of CROSS_LAYER_SUBJECTS) {
198
+ if (!subject.re.test(claim)) continue;
199
+ const distanceNote = subject.layer === 'L7_adjacent'
200
+ ? 'same-layer complement (hosted panel) ≠ PreToolUse evaluate→block→evidence substitute'
201
+ : `layer ${subject.layer} vs ThumbGate ${THUMBGATE_LAYER}`;
202
+ findings.push({
203
+ id: 'cross_layer_substitute',
204
+ severity: 'fail',
205
+ gateId: 'require-same-layer-comparison',
206
+ message: `Category error: treating ${subject.label} as a ThumbGate substitute (${distanceNote}). Tag layers; compare only same job-to-be-done.`,
207
+ subjectLayer: options.subjectLayer,
208
+ referenceLayer: subject.layer,
209
+ });
210
+ }
211
+ }
212
+
213
+ // Explicit layer distance check when claim names a lower-layer product next to ThumbGate/governance
214
+ if (claim && /\b(thumbgate|pretooluse|prevention rule|evaluate.?block.?evidence)\b/i.test(claim)) {
215
+ for (const subject of CROSS_LAYER_SUBJECTS) {
216
+ if (subject.layer === 'L7_adjacent') continue;
217
+ if (!subject.re.test(claim)) continue;
218
+ if (findings.some((f) => f.id === 'cross_layer_substitute' && f.referenceLayer === subject.layer)) {
219
+ continue;
220
+ }
221
+ if (/\b(input|dependenc|consumes?|routes? through|sits under|below)\b/i.test(claim)) {
222
+ continue; // honest dependency language is fine
223
+ }
224
+ if (/\b(compet\w*|substitut\w*|commoditiz\w*|replace[sd]?|instead of|vs\.?|versus)\b/i.test(claim)) {
225
+ findings.push({
226
+ id: 'cross_layer_substitute',
227
+ severity: 'fail',
228
+ gateId: 'require-same-layer-comparison',
229
+ message: `Cross-layer comparison: ${subject.label} (${subject.layer}) vs ThumbGate (${THUMBGATE_LAYER}). Use as dependency language, not competitor language.`,
230
+ subjectLayer: THUMBGATE_LAYER,
231
+ referenceLayer: subject.layer,
232
+ });
233
+ }
234
+ }
235
+ }
236
+
237
+ let closeout = null;
238
+ const closeoutSource = options.closeoutText
239
+ || (options.closeoutPath ? fs.readFileSync(String(options.closeoutPath), 'utf8') : null);
240
+ if (closeoutSource != null) {
241
+ const extracted = extractCloseoutItems(closeoutSource);
242
+ closeout = {
243
+ hasHeading: extracted.hasHeading,
244
+ items: extracted.found,
245
+ missing: [],
246
+ placeholders: [],
247
+ };
248
+ if (!extracted.hasHeading) {
249
+ findings.push({
250
+ id: 'missing_evidence_heading',
251
+ severity: 'fail',
252
+ gateId: 'require-evidence-closeout-items',
253
+ message: 'Closeout markdown needs an ## Evidence / Closeout heading (AQG evidence-closeout FORMAT).',
254
+ });
255
+ }
256
+ for (const item of REQUIRED_CLOSEOUT_ITEMS) {
257
+ if (!(item in extracted.found)) {
258
+ closeout.missing.push(item);
259
+ findings.push({
260
+ id: 'missing_closeout_item',
261
+ severity: 'fail',
262
+ gateId: 'require-evidence-closeout-items',
263
+ message: `Missing evidence closeout item: ${item}`,
264
+ item,
265
+ });
266
+ } else if (isPlaceholder(extracted.found[item])) {
267
+ closeout.placeholders.push(item);
268
+ findings.push({
269
+ id: 'placeholder_closeout_item',
270
+ severity: 'fail',
271
+ gateId: 'require-evidence-closeout-items',
272
+ message: `Evidence closeout item has placeholder value: ${item}`,
273
+ item,
274
+ });
275
+ }
276
+ }
277
+ }
278
+
279
+ if (options.claimReady && findings.length) {
280
+ findings.push({
281
+ id: 'claim_ready_blocked',
282
+ severity: 'fail',
283
+ gateId: 'require-same-layer-comparison',
284
+ message: '--claim-ready refused: clear layer-check and closeout findings first.',
285
+ });
286
+ }
287
+
288
+ const failCount = findings.filter((f) => f.severity === 'fail').length;
289
+ const status = options.mapOnly && !failCount
290
+ ? 'ready'
291
+ : (failCount ? 'fail' : 'ready');
292
+
293
+ const nextActions = [];
294
+ if (findings.some((f) => f.id === 'deeppattern_sku_clone')) {
295
+ nextActions.push('Keep AQG/DE as neighbors. Steal layer-check + evidence-closeout FORMAT only.');
296
+ }
297
+ if (findings.some((f) => f.id === 'cross_layer_substitute')) {
298
+ nextActions.push('Retag each product L1–L8; drop cross-layer substitutes; cite specific surfaces not company names.');
299
+ }
300
+ if (findings.some((f) => f.gateId === 'require-evidence-closeout-items')) {
301
+ nextActions.push('Add an Evidence/Closeout heading with all six required items filled from fresh checks.');
302
+ }
303
+ if (!nextActions.length) {
304
+ nextActions.push('Use --map-only for the rail map; pass --closeout=path.md before claiming done.');
305
+ }
306
+
307
+ return {
308
+ name: 'thumbgate-deeppattern-discipline-honesty',
309
+ ok: status === 'ready',
310
+ status,
311
+ source: { aqg: SOURCE_AQG, decisionEngine: SOURCE_DE },
312
+ thumbgateLayer: THUMBGATE_LAYER,
313
+ layers: LAYERS,
314
+ requiredCloseoutItems: REQUIRED_CLOSEOUT_ITEMS,
315
+ map: options.mapOnly || !findings.length ? RAIL_MAP : undefined,
316
+ closeout,
317
+ findings,
318
+ summary: {
319
+ findingCount: findings.length,
320
+ failCount,
321
+ },
322
+ nextActions,
323
+ exampleCommand: 'npx thumbgate deeppattern-discipline-honesty --json --map-only',
324
+ disclaimer: 'FORMAT steal from DeepPattern Agent Quality Gates + Decision Engine. Not affiliated. Does not install AQG/DE or ship a cross-vendor audit SKU.',
325
+ };
326
+ }
327
+
328
+ function formatDeeppatternDisciplineHonestyReport(report) {
329
+ const lines = [
330
+ 'DeepPattern Discipline Honesty Doctor',
331
+ `Status : ${report.status}`,
332
+ `Layer : ThumbGate ${report.thumbgateLayer} (application / workflow governance)`,
333
+ `Sources : ${report.source.aqg}`,
334
+ ` ${report.source.decisionEngine}`,
335
+ `Findings: ${report.summary.findingCount} (fail=${report.summary.failCount})`,
336
+ ];
337
+ if (report.map) {
338
+ lines.push('', 'Rail map:');
339
+ for (const row of report.map) {
340
+ lines.push(` - ${row.deeppattern}`);
341
+ lines.push(` → ${row.thumbgate}`);
342
+ }
343
+ }
344
+ if (report.closeout) {
345
+ lines.push('', 'Closeout:');
346
+ lines.push(` heading: ${report.closeout.hasHeading ? 'yes' : 'no'}`);
347
+ lines.push(` missing: ${report.closeout.missing.join(', ') || 'none'}`);
348
+ lines.push(` placeholders: ${report.closeout.placeholders.join(', ') || 'none'}`);
349
+ }
350
+ if (report.findings.length) {
351
+ lines.push('', 'Findings:');
352
+ for (const f of report.findings) {
353
+ lines.push(` - [${f.severity}] ${f.id}${f.gateId ? ` [${f.gateId}]` : ''}`);
354
+ lines.push(` ${f.message}`);
355
+ }
356
+ }
357
+ lines.push('', 'Next actions:');
358
+ for (const a of report.nextActions) lines.push(` - ${a}`);
359
+ lines.push('', `Example: ${report.exampleCommand}`);
360
+ lines.push(`Note: ${report.disclaimer}`, '');
361
+ return `${lines.join('\n')}\n`;
362
+ }
363
+
364
+ function parseCliArgs(argv) {
365
+ const options = {};
366
+ for (const arg of argv) {
367
+ if (arg === '--json') { options.json = true; continue; }
368
+ if (arg === '--strict') { options.strict = true; continue; }
369
+ if (arg === '--map-only') { options['map-only'] = true; continue; }
370
+ if (arg === '--claim-ready') { options['claim-ready'] = true; continue; }
371
+ if (arg === '--clone-deeppattern' || arg === '--clone-aqg') {
372
+ options['clone-deeppattern'] = true;
373
+ continue;
374
+ }
375
+ if (arg === '--help' || arg === '-h') { options.help = true; continue; }
376
+ const m = /^--([^=]+)(?:=(.*))?$/.exec(arg);
377
+ if (!m) continue;
378
+ options[m[1]] = m[2] === undefined ? true : m[2];
379
+ }
380
+ return options;
381
+ }
382
+
383
+ function printHelp() {
384
+ process.stdout.write(`Usage: node scripts/deeppattern-discipline-honesty.js [flags]
385
+
386
+ Flags:
387
+ --claim=TEXT Comparative / substitute claim to layer-check
388
+ --subject-layer=L7 Subject product layer (default L7)
389
+ --closeout=PATH Markdown closeout / PR body to validate
390
+ --closeout-text=TEXT Inline closeout markdown
391
+ --map-only
392
+ --claim-ready
393
+ --clone-deeppattern Always fail (refuse AQG/DE SKU clone)
394
+ --json --strict
395
+
396
+ Sources:
397
+ ${SOURCE_AQG}
398
+ ${SOURCE_DE}
399
+ `);
400
+ }
401
+
402
+ function runCli(argv = process.argv.slice(2)) {
403
+ const args = parseCliArgs(argv);
404
+ if (args.help) {
405
+ printHelp();
406
+ return 0;
407
+ }
408
+ const report = buildDeeppatternDisciplineHonestyReport(args);
409
+ if (args.json) process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
410
+ else process.stdout.write(formatDeeppatternDisciplineHonestyReport(report));
411
+ if (args.strict && report.status !== 'ready') return 1;
412
+ if (report.status === 'fail') return 1;
413
+ return 0;
414
+ }
415
+
416
+ module.exports = {
417
+ SOURCE_AQG,
418
+ SOURCE_DE,
419
+ LAYERS,
420
+ THUMBGATE_LAYER,
421
+ REQUIRED_CLOSEOUT_ITEMS,
422
+ RAIL_MAP,
423
+ buildDeeppatternDisciplineHonestyReport,
424
+ formatDeeppatternDisciplineHonestyReport,
425
+ extractCloseoutItems,
426
+ runCli,
427
+ };
428
+
429
+ if (path.resolve(process.argv[1] || '') === path.resolve(__filename)) {
430
+ process.exitCode = runCli();
431
+ }
@@ -0,0 +1,83 @@
1
+ #!/usr/bin/env bash
2
+ # install-thumbgate-daily-publish-launchd.sh — Daily ThumbGate News & Technical Discoveries LaunchAgent.
3
+ #
4
+ # Default: every day at 09:00 AM EST (local).
5
+ #
6
+ # Usage:
7
+ # bash scripts/install-thumbgate-daily-publish-launchd.sh
8
+ # bash scripts/install-thumbgate-daily-publish-launchd.sh --uninstall
9
+ #
10
+ # Logs: ~/Library/Logs/thumbgate-daily-publish.log
11
+ set -euo pipefail
12
+
13
+ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
14
+ SCRIPT="$ROOT/scripts/thumbgate-daily-discoveries-publish.sh"
15
+ LABEL="com.thumbgate.daily-publish"
16
+ PLIST="$HOME/Library/LaunchAgents/${LABEL}.plist"
17
+ LOG_DIR="$HOME/Library/Logs"
18
+ HOUR="${THUMBGATE_PUBLISH_HOUR:-9}"
19
+ MINUTE="${THUMBGATE_PUBLISH_MINUTE:-0}"
20
+
21
+ UNINSTALL=0
22
+ while [[ $# -gt 0 ]]; do
23
+ case "$1" in
24
+ --uninstall) UNINSTALL=1 ;;
25
+ -h|--help)
26
+ echo "Usage: $0 [--uninstall]"
27
+ exit 0
28
+ ;;
29
+ *)
30
+ echo "unknown arg: $1" >&2
31
+ exit 2
32
+ ;;
33
+ esac
34
+ shift
35
+ done
36
+
37
+ if [[ "$(uname -s)" != "Darwin" ]]; then
38
+ echo "error: macOS only" >&2
39
+ exit 1
40
+ fi
41
+
42
+ if [[ "$UNINSTALL" -eq 1 ]]; then
43
+ launchctl bootout "gui/$(id -u)/${LABEL}" 2>/dev/null || true
44
+ rm -f "$PLIST"
45
+ echo "removed $PLIST"
46
+ exit 0
47
+ fi
48
+
49
+ mkdir -p "$LOG_DIR" "$HOME/Library/LaunchAgents"
50
+
51
+ cat > "$PLIST" <<PLIST_EOF
52
+ <?xml version="1.0" encoding="UTF-8"?>
53
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
54
+ <plist version="1.0">
55
+ <dict>
56
+ <key>Label</key>
57
+ <string>${LABEL}</string>
58
+ <key>ProgramArguments</key>
59
+ <array>
60
+ <string>/bin/bash</string>
61
+ <string>${SCRIPT}</string>
62
+ </array>
63
+ <key>StartCalendarInterval</key>
64
+ <dict>
65
+ <key>Hour</key>
66
+ <integer>${HOUR}</integer>
67
+ <key>Minute</key>
68
+ <integer>${MINUTE}</integer>
69
+ </dict>
70
+ <key>StandardOutPath</key>
71
+ <string>${LOG_DIR}/thumbgate-daily-publish.log</string>
72
+ <key>StandardErrorPath</key>
73
+ <string>${LOG_DIR}/thumbgate-daily-publish.log</string>
74
+ <key>WorkingDirectory</key>
75
+ <string>${ROOT}</string>
76
+ </dict>
77
+ </plist>
78
+ PLIST_EOF
79
+
80
+ launchctl bootout "gui/$(id -u)/${LABEL}" 2>/dev/null || true
81
+ launchctl bootstrap "gui/$(id -u)" "$PLIST"
82
+
83
+ echo "installed and loaded ${PLIST} (runs daily at ${HOUR}:${MINUTE})"