@doxbrix/doxloop 0.1.4 → 0.2.0
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/CHANGELOG.md +1153 -0
- package/README.md +422 -226
- package/assets/doxbrix-preview.css +77 -15
- package/contracts/agent-events-v1.schema.json +1 -0
- package/contracts/coverage-v1.schema.json +59 -0
- package/contracts/drift-v1.schema.json +1 -0
- package/contracts/evaluation-v1.schema.json +62 -0
- package/contracts/quality-codes-v1.json +1 -0
- package/contracts/quality-report-v1.schema.json +1 -0
- package/contracts/validation-v1.schema.json +1 -0
- package/dist/agent-failure.d.ts +36 -0
- package/dist/agent-failure.js +98 -0
- package/dist/agent-isolation.d.ts +17 -0
- package/dist/agent-isolation.js +94 -0
- package/dist/agent-log.d.ts +197 -0
- package/dist/agent-log.js +1094 -0
- package/dist/agent-process.d.ts +63 -0
- package/dist/agent-process.js +128 -0
- package/dist/agent-reply.d.ts +47 -0
- package/dist/agent-reply.js +315 -0
- package/dist/agents.d.ts +40 -4
- package/dist/agents.js +115 -11
- package/dist/api-coverage.d.ts +11 -0
- package/dist/api-coverage.js +72 -0
- package/dist/application-probe.d.ts +28 -0
- package/dist/application-probe.js +93 -0
- package/dist/application-url.d.ts +14 -0
- package/dist/application-url.js +26 -0
- package/dist/args.js +15 -0
- package/dist/artifact-deploy.d.ts +9 -0
- package/dist/artifact-deploy.js +28 -8
- package/dist/assets.d.ts +72 -0
- package/dist/assets.js +382 -0
- package/dist/auth.d.ts +10 -0
- package/dist/auth.js +38 -9
- package/dist/author.d.ts +206 -4
- package/dist/author.js +1650 -52
- package/dist/authoring-batches.d.ts +190 -0
- package/dist/authoring-batches.js +489 -0
- package/dist/authoring-postpass.d.ts +79 -0
- package/dist/authoring-postpass.js +1289 -0
- package/dist/authoring-progress.d.ts +95 -0
- package/dist/authoring-progress.js +269 -0
- package/dist/autosync.d.ts +56 -0
- package/dist/autosync.js +442 -0
- package/dist/batch-artifacts.d.ts +81 -0
- package/dist/batch-artifacts.js +412 -0
- package/dist/batch-limits.d.ts +37 -0
- package/dist/batch-limits.js +80 -0
- package/dist/branding.d.ts +41 -0
- package/dist/branding.js +229 -0
- package/dist/bundle-upload.d.ts +26 -0
- package/dist/bundle-upload.js +99 -0
- package/dist/capture-auth.d.ts +95 -0
- package/dist/capture-auth.js +204 -0
- package/dist/capture.d.ts +48 -0
- package/dist/capture.js +20 -1
- package/dist/cli.js +839 -32
- package/dist/content-links.d.ts +10 -0
- package/dist/content-links.js +49 -0
- package/dist/contract-validation.d.ts +4 -0
- package/dist/contract-validation.js +25 -0
- package/dist/coverage-actions.d.ts +8 -0
- package/dist/coverage-actions.js +71 -0
- package/dist/coverage-resolutions.d.ts +16 -0
- package/dist/coverage-resolutions.js +58 -0
- package/dist/db.d.ts +37 -0
- package/dist/db.js +288 -0
- package/dist/demo.d.ts +12 -0
- package/dist/demo.js +122 -0
- package/dist/deploy-credentials.d.ts +7 -0
- package/dist/deploy-credentials.js +76 -0
- package/dist/deploy-targets/github-pages.d.ts +3 -0
- package/dist/deploy-targets/github-pages.js +100 -0
- package/dist/deploy-targets/index.d.ts +8 -0
- package/dist/deploy-targets/index.js +78 -0
- package/dist/deploy-targets/netlify.d.ts +3 -0
- package/dist/deploy-targets/netlify.js +49 -0
- package/dist/deploy-targets/types.d.ts +32 -0
- package/dist/deploy-targets/types.js +2 -0
- package/dist/deploy-targets/vercel.d.ts +3 -0
- package/dist/deploy-targets/vercel.js +95 -0
- package/dist/deploy.d.ts +9 -0
- package/dist/deploy.js +87 -13
- package/dist/deterministic-capture.d.ts +90 -0
- package/dist/deterministic-capture.js +435 -0
- package/dist/direct-edit.d.ts +40 -0
- package/dist/direct-edit.js +159 -0
- package/dist/docs-crawl.d.ts +86 -0
- package/dist/docs-crawl.js +536 -0
- package/dist/docs-site.d.ts +49 -0
- package/dist/docs-site.js +233 -0
- package/dist/doctor.js +8 -0
- package/dist/documentation-collections.d.ts +23 -0
- package/dist/documentation-collections.js +191 -0
- package/dist/documentation-plan.d.ts +253 -0
- package/dist/documentation-plan.js +2563 -0
- package/dist/doxbrix-build.d.ts +19 -0
- package/dist/doxbrix-build.js +167 -0
- package/dist/doxbrix-markdown.d.ts +9 -0
- package/dist/doxbrix-markdown.js +115 -14
- package/dist/drift.d.ts +10 -0
- package/dist/drift.js +164 -0
- package/dist/evaluation.d.ts +46 -0
- package/dist/evaluation.js +113 -0
- package/dist/evidence-pack.d.ts +47 -0
- package/dist/evidence-pack.js +358 -0
- package/dist/evidence.d.ts +25 -0
- package/dist/evidence.js +175 -0
- package/dist/fs.d.ts +8 -2
- package/dist/fs.js +41 -11
- package/dist/generator-api.d.ts +89 -0
- package/dist/generator-preflight.d.ts +27 -0
- package/dist/generator-preflight.js +105 -0
- package/dist/generator-runtime.d.ts +7 -0
- package/dist/generator-runtime.js +17 -1
- package/dist/generators.d.ts +24 -2
- package/dist/generators.js +54 -1
- package/dist/git-delivery.d.ts +17 -0
- package/dist/git-delivery.js +123 -0
- package/dist/globs.d.ts +16 -0
- package/dist/globs.js +65 -0
- package/dist/glossary.d.ts +26 -0
- package/dist/glossary.js +179 -0
- package/dist/history.d.ts +106 -0
- package/dist/history.js +600 -0
- package/dist/html-markdown.d.ts +46 -0
- package/dist/html-markdown.js +423 -0
- package/dist/interactive.js +16 -15
- package/dist/job-events.d.ts +74 -0
- package/dist/job-events.js +377 -0
- package/dist/keep-awake.d.ts +50 -0
- package/dist/keep-awake.js +123 -0
- package/dist/local-source-snapshot.d.ts +20 -0
- package/dist/local-source-snapshot.js +61 -0
- package/dist/mintlify-detect.d.ts +3 -0
- package/dist/mintlify-detect.js +18 -0
- package/dist/mintlify-import.d.ts +75 -0
- package/dist/mintlify-import.js +190 -0
- package/dist/navigation.d.ts +98 -0
- package/dist/navigation.js +310 -0
- package/dist/openapi.d.ts +60 -0
- package/dist/openapi.js +439 -0
- package/dist/page-editor-bridge.d.ts +3 -0
- package/dist/page-editor-bridge.js +109 -0
- package/dist/page-editor-preview.d.ts +10 -0
- package/dist/page-editor-preview.js +55 -0
- package/dist/page-extension.d.ts +9 -0
- package/dist/page-extension.js +15 -0
- package/dist/page-metadata.d.ts +28 -0
- package/dist/page-metadata.js +166 -0
- package/dist/page-operations.d.ts +34 -0
- package/dist/page-operations.js +215 -0
- package/dist/page-routes.d.ts +4 -0
- package/dist/page-routes.js +61 -0
- package/dist/pages.d.ts +20 -0
- package/dist/pages.js +184 -0
- package/dist/plan-generator.d.ts +3 -0
- package/dist/plan-generator.js +21 -0
- package/dist/plan-navigation.d.ts +11 -0
- package/dist/plan-navigation.js +30 -0
- package/dist/planning-captures.d.ts +20 -0
- package/dist/planning-captures.js +143 -0
- package/dist/planning-research.d.ts +135 -0
- package/dist/planning-research.js +472 -0
- package/dist/planning-triage.d.ts +23 -0
- package/dist/planning-triage.js +131 -0
- package/dist/preview.d.ts +24 -0
- package/dist/preview.js +280 -29
- package/dist/project-detect.d.ts +36 -0
- package/dist/project-detect.js +251 -0
- package/dist/project-import.d.ts +54 -0
- package/dist/project-import.js +157 -0
- package/dist/project-lock.d.ts +6 -0
- package/dist/project-lock.js +96 -0
- package/dist/project-registry.d.ts +25 -0
- package/dist/project-registry.js +79 -0
- package/dist/project.d.ts +35 -3
- package/dist/project.js +388 -38
- package/dist/prompts.d.ts +9 -0
- package/dist/prompts.js +33 -4
- package/dist/proposal-replay.d.ts +32 -0
- package/dist/proposal-replay.js +99 -0
- package/dist/quality-claims.d.ts +8 -0
- package/dist/quality-claims.js +168 -0
- package/dist/quality-config.d.ts +5 -0
- package/dist/quality-config.js +84 -0
- package/dist/quality-contract.d.ts +37 -0
- package/dist/quality-contract.js +46 -0
- package/dist/quality-examples.d.ts +4 -0
- package/dist/quality-examples.js +233 -0
- package/dist/quality-gates.d.ts +16 -0
- package/dist/quality-gates.js +192 -0
- package/dist/quality-links.d.ts +7 -0
- package/dist/quality-links.js +149 -0
- package/dist/quality-lint.d.ts +6 -0
- package/dist/quality-lint.js +124 -0
- package/dist/quality-rendered.d.ts +11 -0
- package/dist/quality-rendered.js +222 -0
- package/dist/quality-schema.d.ts +3 -0
- package/dist/quality-schema.js +71 -0
- package/dist/release-notes.d.ts +44 -0
- package/dist/release-notes.js +183 -0
- package/dist/remote-monitor.d.ts +16 -0
- package/dist/remote-monitor.js +74 -0
- package/dist/remote-source.d.ts +34 -0
- package/dist/remote-source.js +426 -0
- package/dist/review-diff.d.ts +82 -0
- package/dist/review-diff.js +400 -0
- package/dist/review-learning.d.ts +11 -0
- package/dist/review-learning.js +60 -0
- package/dist/review-render.d.ts +38 -0
- package/dist/review-render.js +224 -0
- package/dist/review-report.d.ts +9 -0
- package/dist/review-report.js +89 -0
- package/dist/review-ui.d.ts +14 -0
- package/dist/review-ui.js +1248 -0
- package/dist/schedule.d.ts +78 -0
- package/dist/schedule.js +480 -0
- package/dist/screen-capture-provider.d.ts +52 -0
- package/dist/screen-capture-provider.js +218 -0
- package/dist/screenshot-workflow.d.ts +167 -0
- package/dist/screenshot-workflow.js +1237 -0
- package/dist/settings.d.ts +1 -1
- package/dist/settings.js +95 -7
- package/dist/site-export.d.ts +18 -0
- package/dist/site-export.js +87 -0
- package/dist/source-connectors.d.ts +33 -0
- package/dist/source-connectors.js +268 -0
- package/dist/source-discovery.d.ts +132 -0
- package/dist/source-discovery.js +823 -0
- package/dist/source-intelligence.d.ts +9 -0
- package/dist/source-intelligence.js +306 -0
- package/dist/sync-review.d.ts +28 -0
- package/dist/sync-review.js +264 -0
- package/dist/sync-runs.d.ts +192 -0
- package/dist/sync-runs.js +2244 -0
- package/dist/sync.d.ts +35 -0
- package/dist/sync.js +298 -32
- package/dist/text-diff.d.ts +9 -0
- package/dist/text-diff.js +59 -0
- package/dist/types.d.ts +946 -1
- package/dist/ui/assets/doxloop-logo-light-De7Nx7j7.png +0 -0
- package/dist/ui/assets/index-BHBYU2aG.css +1 -0
- package/dist/ui/assets/index-Cq3RPQiC.js +33 -0
- package/dist/ui/index.html +18 -0
- package/dist/ui-server.d.ts +83 -0
- package/dist/ui-server.js +3532 -0
- package/dist/usage-budget.d.ts +28 -0
- package/dist/usage-budget.js +90 -0
- package/dist/validation.d.ts +25 -1
- package/dist/validation.js +312 -26
- package/dist/workspace-tools.d.ts +54 -0
- package/dist/workspace-tools.js +123 -0
- package/docs/agent-compatibility.md +50 -28
- package/docs/ci-and-automation.md +105 -66
- package/docs/doxbrix-http-api.md +8 -1
- package/docs/existing-documentation.md +80 -0
- package/docs/generation-performance.md +108 -0
- package/docs/generator-authoring.md +68 -5
- package/docs/generator-selection.md +50 -13
- package/docs/mintlify-import.md +71 -0
- package/docs/openapi-security.md +25 -0
- package/docs/project-format.md +331 -36
- package/docs/release-quality.md +158 -0
- package/docs/releasing.md +72 -0
- package/docs/review-workflows.md +51 -0
- package/docs/security-model.md +102 -39
- package/docs/troubleshooting.md +226 -93
- package/package.json +46 -15
- package/scripts/test-auto-screenshot.mjs +172 -0
- package/skills/doxloop-authoring/SKILL.md +242 -361
- package/skills/doxloop-authoring/references/existing-documentation.md +94 -0
- package/skills/doxloop-authoring/references/navigation-architecture.md +18 -7
- package/skills/doxloop-authoring/references/page-depth.md +169 -0
- package/skills/doxloop-authoring/references/project-format.md +106 -7
- package/skills/doxloop-authoring/references/quality.md +10 -0
- package/skills/doxloop-authoring/references/screenshot-manifest.md +113 -0
- package/skills/doxloop-authoring/references/screenshots.md +149 -235
- package/skills/doxloop-authoring/references/workflows.md +76 -0
- package/skills/doxloop-doxbrix/SKILL.md +38 -22
- package/skills/doxloop-doxbrix/references/api-endpoints.md +15 -14
- package/skills/doxloop-doxbrix/references/components.md +30 -3
- package/skills/doxloop-doxbrix/references/manifest.md +3 -2
- package/vendor/doxbrix-import/LICENSE +202 -0
- package/vendor/doxbrix-import/README.md +17 -0
- package/vendor/doxbrix-import/UPSTREAM.json +47 -0
- package/vendor/doxbrix-import/dist/docs/frontmatter.d.ts +13 -0
- package/vendor/doxbrix-import/dist/docs/frontmatter.js +83 -0
- package/vendor/doxbrix-import/dist/docs/import.d.ts +21 -0
- package/vendor/doxbrix-import/dist/docs/import.js +147 -0
- package/vendor/doxbrix-import/dist/docs/manifest.d.ts +163 -0
- package/vendor/doxbrix-import/dist/docs/manifest.js +64 -0
- package/vendor/doxbrix-import/dist/docs/project.d.ts +25 -0
- package/vendor/doxbrix-import/dist/docs/project.js +77 -0
- package/vendor/doxbrix-import/dist/docs/starter.d.ts +4 -0
- package/vendor/doxbrix-import/dist/docs/starter.js +11 -0
- package/vendor/doxbrix-import/dist/importer.d.ts +227 -0
- package/vendor/doxbrix-import/dist/importer.js +1567 -0
- package/vendor/doxbrix-import/dist/mintlify-openapi.d.ts +37 -0
- package/vendor/doxbrix-import/dist/mintlify-openapi.js +305 -0
- package/vendor/doxbrix-import/dist/safe-path.d.ts +9 -0
- package/vendor/doxbrix-import/dist/safe-path.js +47 -0
- package/dist/agents.d.ts.map +0 -1
- package/dist/agents.js.map +0 -1
- package/dist/args.d.ts.map +0 -1
- package/dist/args.js.map +0 -1
- package/dist/artifact-deploy.d.ts.map +0 -1
- package/dist/artifact-deploy.js.map +0 -1
- package/dist/auth.d.ts.map +0 -1
- package/dist/auth.js.map +0 -1
- package/dist/author.d.ts.map +0 -1
- package/dist/author.js.map +0 -1
- package/dist/capture.d.ts.map +0 -1
- package/dist/capture.js.map +0 -1
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/deploy.d.ts.map +0 -1
- package/dist/deploy.js.map +0 -1
- package/dist/deployment-visibility.d.ts.map +0 -1
- package/dist/deployment-visibility.js.map +0 -1
- package/dist/doctor.d.ts.map +0 -1
- package/dist/doctor.js.map +0 -1
- package/dist/doxbrix-markdown.d.ts.map +0 -1
- package/dist/doxbrix-markdown.js.map +0 -1
- package/dist/errors.d.ts.map +0 -1
- package/dist/errors.js.map +0 -1
- package/dist/fs.d.ts.map +0 -1
- package/dist/fs.js.map +0 -1
- package/dist/generator-api.d.ts.map +0 -1
- package/dist/generator-api.js.map +0 -1
- package/dist/generator-manager.d.ts.map +0 -1
- package/dist/generator-manager.js.map +0 -1
- package/dist/generator-runtime.d.ts.map +0 -1
- package/dist/generator-runtime.js.map +0 -1
- package/dist/generators.d.ts.map +0 -1
- package/dist/generators.js.map +0 -1
- package/dist/interactive.d.ts.map +0 -1
- package/dist/interactive.js.map +0 -1
- package/dist/preview.d.ts.map +0 -1
- package/dist/preview.js.map +0 -1
- package/dist/progress.d.ts.map +0 -1
- package/dist/progress.js.map +0 -1
- package/dist/project.d.ts.map +0 -1
- package/dist/project.js.map +0 -1
- package/dist/prompts.d.ts.map +0 -1
- package/dist/prompts.js.map +0 -1
- package/dist/settings.d.ts.map +0 -1
- package/dist/settings.js.map +0 -1
- package/dist/sync.d.ts.map +0 -1
- package/dist/sync.js.map +0 -1
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js.map +0 -1
- package/dist/validation.d.ts.map +0 -1
- package/dist/validation.js.map +0 -1
- package/dist/version.d.ts.map +0 -1
- package/dist/version.js.map +0 -1
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { SyncTrigger } from './types.js';
|
|
2
|
+
export declare const SYNC_LOG_FILE: string;
|
|
3
|
+
export interface DailyTime {
|
|
4
|
+
hour: number;
|
|
5
|
+
minute: number;
|
|
6
|
+
}
|
|
7
|
+
export type ScheduleFrequency = {
|
|
8
|
+
kind: 'interval';
|
|
9
|
+
minutes: number;
|
|
10
|
+
} | {
|
|
11
|
+
kind: 'daily';
|
|
12
|
+
time: DailyTime;
|
|
13
|
+
} | {
|
|
14
|
+
kind: 'weekdays';
|
|
15
|
+
time: DailyTime;
|
|
16
|
+
} | {
|
|
17
|
+
kind: 'weekly';
|
|
18
|
+
weekday: number;
|
|
19
|
+
time: DailyTime;
|
|
20
|
+
} | {
|
|
21
|
+
kind: 'monthly';
|
|
22
|
+
day: number;
|
|
23
|
+
time: DailyTime;
|
|
24
|
+
};
|
|
25
|
+
export interface ScheduleState {
|
|
26
|
+
installed: boolean;
|
|
27
|
+
label: string;
|
|
28
|
+
path?: string;
|
|
29
|
+
time?: DailyTime;
|
|
30
|
+
frequency?: ScheduleFrequency;
|
|
31
|
+
registered?: boolean;
|
|
32
|
+
running?: boolean;
|
|
33
|
+
runs?: number;
|
|
34
|
+
lastExitCode?: number;
|
|
35
|
+
logPath?: string;
|
|
36
|
+
}
|
|
37
|
+
/** The first `daily@HH:MM` trigger, or undefined when none is configured. */
|
|
38
|
+
export declare function dailyTime(triggers: readonly SyncTrigger[]): DailyTime | undefined;
|
|
39
|
+
export declare function scheduleFrequency(triggers: readonly SyncTrigger[]): ScheduleFrequency | undefined;
|
|
40
|
+
export declare function formatScheduleFrequency(frequency: ScheduleFrequency): string;
|
|
41
|
+
export declare function formatDailyTime(time: DailyTime): string;
|
|
42
|
+
/**
|
|
43
|
+
* A stable per-project identifier. The directory name keeps it recognizable in
|
|
44
|
+
* `launchctl list` or `systemctl --user`, and the path hash keeps two projects
|
|
45
|
+
* with the same directory name apart.
|
|
46
|
+
*/
|
|
47
|
+
export declare function scheduleLabel(root: string): string;
|
|
48
|
+
export declare function renderLaunchAgent(options: {
|
|
49
|
+
label: string;
|
|
50
|
+
root: string;
|
|
51
|
+
frequency: ScheduleFrequency;
|
|
52
|
+
}): string;
|
|
53
|
+
export declare function renderSystemdService(options: {
|
|
54
|
+
root: string;
|
|
55
|
+
}): string;
|
|
56
|
+
export declare function renderSystemdTimer(options: {
|
|
57
|
+
root: string;
|
|
58
|
+
frequency: ScheduleFrequency;
|
|
59
|
+
}): string;
|
|
60
|
+
/** `Persistent=true` and launchd both run a missed job once the machine wakes. */
|
|
61
|
+
export declare function cronLine(options: {
|
|
62
|
+
label: string;
|
|
63
|
+
root: string;
|
|
64
|
+
frequency: ScheduleFrequency;
|
|
65
|
+
}): string;
|
|
66
|
+
export declare function installSchedule(root: string, frequency: ScheduleFrequency, platform?: NodeJS.Platform): Promise<ScheduleState>;
|
|
67
|
+
export declare function removeSchedule(root: string, platform?: NodeJS.Platform): Promise<ScheduleState>;
|
|
68
|
+
export declare function scheduleState(root: string, platform?: NodeJS.Platform): Promise<ScheduleState>;
|
|
69
|
+
export declare function parseLaunchctlState(output: string): {
|
|
70
|
+
running: boolean;
|
|
71
|
+
runs?: number;
|
|
72
|
+
lastExitCode?: number;
|
|
73
|
+
};
|
|
74
|
+
/** Rewrite the user's crontab, preserving every line that is not ours. */
|
|
75
|
+
export declare function withoutLabel(crontab: string, label: string, add?: string): string;
|
|
76
|
+
export declare function readSyncLog(root: string, lines?: number): Promise<string[]>;
|
|
77
|
+
export declare function appendSyncLog(root: string, message: string): Promise<void>;
|
|
78
|
+
//# sourceMappingURL=schedule.d.ts.map
|
package/dist/schedule.js
ADDED
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
import { withProjectLock } from './project-lock.js';
|
|
2
|
+
import { execFile } from 'node:child_process';
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
import { appendFile, mkdir, readFile, rename, rm, stat, writeFile } from 'node:fs/promises';
|
|
5
|
+
import { homedir } from 'node:os';
|
|
6
|
+
import { basename, join } from 'node:path';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
import { promisify } from 'node:util';
|
|
9
|
+
import { DoxloopError } from './errors.js';
|
|
10
|
+
import { pathExists } from './fs.js';
|
|
11
|
+
const run = promisify(execFile);
|
|
12
|
+
export const SYNC_LOG_FILE = join('.doxloop', 'sync.log');
|
|
13
|
+
/** The first `daily@HH:MM` trigger, or undefined when none is configured. */
|
|
14
|
+
export function dailyTime(triggers) {
|
|
15
|
+
for (const trigger of triggers) {
|
|
16
|
+
const match = /^daily@(\d{2}):(\d{2})$/.exec(trigger);
|
|
17
|
+
if (match)
|
|
18
|
+
return { hour: Number(match[1]), minute: Number(match[2]) };
|
|
19
|
+
}
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
export function scheduleFrequency(triggers) {
|
|
23
|
+
if (triggers.length > 1)
|
|
24
|
+
throw new DoxloopError('Choose one schedule per project.');
|
|
25
|
+
for (const trigger of triggers) {
|
|
26
|
+
const interval = /^every@(\d+)([mh])$/.exec(trigger);
|
|
27
|
+
if (interval) {
|
|
28
|
+
const amount = Number(interval[1]);
|
|
29
|
+
return { kind: 'interval', minutes: interval[2] === 'h' ? amount * 60 : amount };
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
for (const trigger of triggers) {
|
|
33
|
+
const weekdays = /^weekdays@(\d{2}):(\d{2})$/.exec(trigger);
|
|
34
|
+
if (weekdays)
|
|
35
|
+
return { kind: 'weekdays', time: { hour: Number(weekdays[1]), minute: Number(weekdays[2]) } };
|
|
36
|
+
const weekly = /^weekly@(sun|mon|tue|wed|thu|fri|sat)@(\d{2}):(\d{2})$/.exec(trigger);
|
|
37
|
+
if (weekly)
|
|
38
|
+
return {
|
|
39
|
+
kind: 'weekly',
|
|
40
|
+
weekday: WEEKDAYS.findIndex((day) => day === weekly[1]),
|
|
41
|
+
time: { hour: Number(weekly[2]), minute: Number(weekly[3]) },
|
|
42
|
+
};
|
|
43
|
+
const monthly = /^monthly@(\d{1,2})@(\d{2}):(\d{2})$/.exec(trigger);
|
|
44
|
+
if (monthly)
|
|
45
|
+
return {
|
|
46
|
+
kind: 'monthly',
|
|
47
|
+
day: Number(monthly[1]),
|
|
48
|
+
time: { hour: Number(monthly[2]), minute: Number(monthly[3]) },
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
const time = dailyTime(triggers);
|
|
52
|
+
return time ? { kind: 'daily', time } : undefined;
|
|
53
|
+
}
|
|
54
|
+
export function formatScheduleFrequency(frequency) {
|
|
55
|
+
if (frequency.kind === 'daily')
|
|
56
|
+
return `daily ${formatDailyTime(frequency.time)}`;
|
|
57
|
+
if (frequency.kind === 'weekdays')
|
|
58
|
+
return `weekdays ${formatDailyTime(frequency.time)}`;
|
|
59
|
+
if (frequency.kind === 'weekly')
|
|
60
|
+
return `weekly on ${WEEKDAY_LABELS[frequency.weekday]} at ${formatDailyTime(frequency.time)}`;
|
|
61
|
+
if (frequency.kind === 'monthly')
|
|
62
|
+
return `monthly on day ${frequency.day} at ${formatDailyTime(frequency.time)}`;
|
|
63
|
+
if (frequency.minutes % 60 === 0) {
|
|
64
|
+
const hours = frequency.minutes / 60;
|
|
65
|
+
return `every ${hours} hour${hours === 1 ? '' : 's'}`;
|
|
66
|
+
}
|
|
67
|
+
return `every ${frequency.minutes} minute${frequency.minutes === 1 ? '' : 's'}`;
|
|
68
|
+
}
|
|
69
|
+
export function formatDailyTime(time) {
|
|
70
|
+
return `${String(time.hour).padStart(2, '0')}:${String(time.minute).padStart(2, '0')}`;
|
|
71
|
+
}
|
|
72
|
+
const WEEKDAYS = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'];
|
|
73
|
+
const WEEKDAY_LABELS = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
|
|
74
|
+
function launchCalendar(frequency) {
|
|
75
|
+
const item = (extra = '') => `<dict>
|
|
76
|
+
${extra}<key>Hour</key>
|
|
77
|
+
<integer>${frequency.time.hour}</integer>
|
|
78
|
+
<key>Minute</key>
|
|
79
|
+
<integer>${frequency.time.minute}</integer>
|
|
80
|
+
</dict>`;
|
|
81
|
+
if (frequency.kind === 'weekdays') {
|
|
82
|
+
return `<array>\n ${[1, 2, 3, 4, 5].map((day) => item(`<key>Weekday</key>\n <integer>${day}</integer>\n `)).join('\n ')}\n </array>`;
|
|
83
|
+
}
|
|
84
|
+
if (frequency.kind === 'weekly')
|
|
85
|
+
return item(`<key>Weekday</key>\n <integer>${frequency.weekday}</integer>\n `);
|
|
86
|
+
if (frequency.kind === 'monthly')
|
|
87
|
+
return item(`<key>Day</key>\n <integer>${frequency.day}</integer>\n `);
|
|
88
|
+
return item();
|
|
89
|
+
}
|
|
90
|
+
function cronExpression(frequency) {
|
|
91
|
+
if (frequency.kind === 'interval') {
|
|
92
|
+
return '* * * * *'; // The locked elapsed-time gate in runSyncNow handles intervals.
|
|
93
|
+
}
|
|
94
|
+
const time = `${frequency.time.minute} ${frequency.time.hour}`;
|
|
95
|
+
if (frequency.kind === 'weekdays')
|
|
96
|
+
return `${time} * * 1-5`;
|
|
97
|
+
if (frequency.kind === 'weekly')
|
|
98
|
+
return `${time} * * ${frequency.weekday}`;
|
|
99
|
+
if (frequency.kind === 'monthly')
|
|
100
|
+
return `${time} ${frequency.day} * *`;
|
|
101
|
+
return `${time} * * *`;
|
|
102
|
+
}
|
|
103
|
+
function systemdCalendar(frequency) {
|
|
104
|
+
const time = `${formatDailyTime(frequency.time)}:00`;
|
|
105
|
+
if (frequency.kind === 'weekdays')
|
|
106
|
+
return `Mon..Fri *-*-* ${time}`;
|
|
107
|
+
if (frequency.kind === 'weekly')
|
|
108
|
+
return `${WEEKDAY_LABELS[frequency.weekday].slice(0, 3)} *-*-* ${time}`;
|
|
109
|
+
if (frequency.kind === 'monthly')
|
|
110
|
+
return `*-*-${String(frequency.day).padStart(2, '0')} ${time}`;
|
|
111
|
+
return `*-*-* ${time}`;
|
|
112
|
+
}
|
|
113
|
+
function windowsSchedule(frequency) {
|
|
114
|
+
if (frequency.kind === 'interval')
|
|
115
|
+
return { type: 'minute', arguments: ['/mo', String(frequency.minutes)] };
|
|
116
|
+
const start = ['/st', formatDailyTime(frequency.time)];
|
|
117
|
+
if (frequency.kind === 'weekdays')
|
|
118
|
+
return { type: 'weekly', arguments: ['/d', 'MON,TUE,WED,THU,FRI', ...start] };
|
|
119
|
+
if (frequency.kind === 'weekly')
|
|
120
|
+
return { type: 'weekly', arguments: ['/d', WEEKDAYS[frequency.weekday].toUpperCase(), ...start] };
|
|
121
|
+
if (frequency.kind === 'monthly')
|
|
122
|
+
return { type: 'monthly', arguments: ['/d', String(frequency.day), ...start] };
|
|
123
|
+
return { type: 'daily', arguments: start };
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* A stable per-project identifier. The directory name keeps it recognizable in
|
|
127
|
+
* `launchctl list` or `systemctl --user`, and the path hash keeps two projects
|
|
128
|
+
* with the same directory name apart.
|
|
129
|
+
*/
|
|
130
|
+
export function scheduleLabel(root) {
|
|
131
|
+
const slug = basename(root)
|
|
132
|
+
.toLowerCase()
|
|
133
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
134
|
+
.replace(/^-|-$/g, '') || 'docs';
|
|
135
|
+
const digest = createHash('sha256').update(root).digest('hex').slice(0, 8);
|
|
136
|
+
return `doxloop-${slug}-${digest}`;
|
|
137
|
+
}
|
|
138
|
+
function cliPath() {
|
|
139
|
+
return fileURLToPath(new URL('cli.js', import.meta.url));
|
|
140
|
+
}
|
|
141
|
+
export function renderLaunchAgent(options) {
|
|
142
|
+
const escape = (value) => value.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
143
|
+
// launchd opens stdout/stderr before spawning the process. macOS can reject
|
|
144
|
+
// that spawn when the destination is inside a protected Documents folder,
|
|
145
|
+
// so native scheduler diagnostics live in the user's Library logs instead.
|
|
146
|
+
const log = nativeScheduleLog(options.label);
|
|
147
|
+
// LaunchServices may reject a user-managed Node binary as the top-level
|
|
148
|
+
// executable. Start with Apple's signed shell, then exec the exact Node and
|
|
149
|
+
// CLI paths selected during setup.
|
|
150
|
+
const command = [
|
|
151
|
+
'exec',
|
|
152
|
+
shellQuote(process.execPath),
|
|
153
|
+
shellQuote(cliPath()),
|
|
154
|
+
'sync',
|
|
155
|
+
'now',
|
|
156
|
+
'--trigger',
|
|
157
|
+
'schedule',
|
|
158
|
+
'--cwd',
|
|
159
|
+
shellQuote(options.root),
|
|
160
|
+
].join(' ');
|
|
161
|
+
const scheduledPath = process.env.PATH ?? '/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin';
|
|
162
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
163
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
164
|
+
<plist version="1.0">
|
|
165
|
+
<dict>
|
|
166
|
+
<key>Label</key>
|
|
167
|
+
<string>com.doxbrix.${escape(options.label)}</string>
|
|
168
|
+
<key>ProgramArguments</key>
|
|
169
|
+
<array>
|
|
170
|
+
<string>/bin/sh</string>
|
|
171
|
+
<string>-c</string>
|
|
172
|
+
<string>${escape(command)}</string>
|
|
173
|
+
</array>
|
|
174
|
+
<key>EnvironmentVariables</key>
|
|
175
|
+
<dict>
|
|
176
|
+
<key>PATH</key>
|
|
177
|
+
<string>${escape(scheduledPath)}</string>
|
|
178
|
+
<key>HOME</key>
|
|
179
|
+
<string>${escape(homedir())}</string>
|
|
180
|
+
</dict>
|
|
181
|
+
${options.frequency.kind !== 'interval' ? `<key>StartCalendarInterval</key>
|
|
182
|
+
${launchCalendar(options.frequency)}` : `<key>StartInterval</key>
|
|
183
|
+
<integer>${options.frequency.minutes * 60}</integer>`}
|
|
184
|
+
<key>StandardOutPath</key>
|
|
185
|
+
<string>${escape(log)}</string>
|
|
186
|
+
<key>StandardErrorPath</key>
|
|
187
|
+
<string>${escape(log)}</string>
|
|
188
|
+
<key>ProcessType</key>
|
|
189
|
+
<string>Background</string>
|
|
190
|
+
</dict>
|
|
191
|
+
</plist>
|
|
192
|
+
`;
|
|
193
|
+
}
|
|
194
|
+
function shellQuote(value) {
|
|
195
|
+
return `'${value.replace(/'/g, `'"'"'`)}'`;
|
|
196
|
+
}
|
|
197
|
+
export function renderSystemdService(options) {
|
|
198
|
+
return `[Unit]
|
|
199
|
+
Description=Doxloop documentation sync for ${options.root}
|
|
200
|
+
|
|
201
|
+
[Service]
|
|
202
|
+
Type=oneshot
|
|
203
|
+
ExecStart=${process.execPath} ${cliPath()} sync now --trigger schedule --cwd ${options.root}
|
|
204
|
+
StandardOutput=append:${join(options.root, SYNC_LOG_FILE)}
|
|
205
|
+
StandardError=append:${join(options.root, SYNC_LOG_FILE)}
|
|
206
|
+
`;
|
|
207
|
+
}
|
|
208
|
+
export function renderSystemdTimer(options) {
|
|
209
|
+
return `[Unit]
|
|
210
|
+
Description=Doxloop documentation sync for ${options.root}
|
|
211
|
+
|
|
212
|
+
[Timer]
|
|
213
|
+
${options.frequency.kind === 'interval' ? `OnUnitActiveSec=${options.frequency.minutes}m` : `OnCalendar=${systemdCalendar(options.frequency)}`}
|
|
214
|
+
Persistent=true
|
|
215
|
+
|
|
216
|
+
[Install]
|
|
217
|
+
WantedBy=timers.target
|
|
218
|
+
`;
|
|
219
|
+
}
|
|
220
|
+
/** `Persistent=true` and launchd both run a missed job once the machine wakes. */
|
|
221
|
+
export function cronLine(options) {
|
|
222
|
+
if (/[\r\n]/.test(options.root))
|
|
223
|
+
throw new DoxloopError('Scheduled project paths cannot contain line breaks.');
|
|
224
|
+
const log = join(options.root, SYNC_LOG_FILE);
|
|
225
|
+
const expression = cronExpression(options.frequency);
|
|
226
|
+
const quote = (value) => `'${value.replace(/'/g, `\'\"\'\"\'`).replace(/%/g, '\\%')}'`;
|
|
227
|
+
return `${expression} ${quote(process.execPath)} ${quote(cliPath())} sync now --trigger schedule --cwd ${quote(options.root)} >> ${quote(log)} 2>&1 # ${options.label}`;
|
|
228
|
+
}
|
|
229
|
+
export async function installSchedule(root, frequency, platform = process.platform) {
|
|
230
|
+
const label = scheduleLabel(root);
|
|
231
|
+
if (platform === 'darwin') {
|
|
232
|
+
const path = join(homedir(), 'Library', 'LaunchAgents', `com.doxbrix.${label}.plist`);
|
|
233
|
+
await mkdir(join(homedir(), 'Library', 'LaunchAgents'), { recursive: true });
|
|
234
|
+
await mkdir(join(homedir(), 'Library', 'Logs', 'Doxloop'), { recursive: true });
|
|
235
|
+
await writeFile(path, renderLaunchAgent({ label, root, frequency }), 'utf8');
|
|
236
|
+
await launchctl(['bootout', `gui/${process.getuid?.() ?? 0}`, path]);
|
|
237
|
+
await launchctl(['bootstrap', `gui/${process.getuid?.() ?? 0}`, path], true);
|
|
238
|
+
try {
|
|
239
|
+
const health = await smokeLaunchAgent(label);
|
|
240
|
+
return {
|
|
241
|
+
installed: true,
|
|
242
|
+
label,
|
|
243
|
+
path,
|
|
244
|
+
...(frequency.kind !== 'interval' ? { time: frequency.time } : {}),
|
|
245
|
+
frequency,
|
|
246
|
+
registered: true,
|
|
247
|
+
logPath: nativeScheduleLog(label),
|
|
248
|
+
...health,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
catch (error) {
|
|
252
|
+
await launchctl(['bootout', `gui/${process.getuid?.() ?? 0}`, path]);
|
|
253
|
+
await rm(path, { force: true });
|
|
254
|
+
await rm(nativeScheduleLog(label), { force: true });
|
|
255
|
+
throw error;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
if (platform === 'linux') {
|
|
259
|
+
if (await hasSystemdUser()) {
|
|
260
|
+
const directory = join(homedir(), '.config', 'systemd', 'user');
|
|
261
|
+
await mkdir(directory, { recursive: true });
|
|
262
|
+
await writeFile(join(directory, `${label}.service`), renderSystemdService({ root }), 'utf8');
|
|
263
|
+
await writeFile(join(directory, `${label}.timer`), renderSystemdTimer({ root, frequency }), 'utf8');
|
|
264
|
+
await systemctl(['daemon-reload']);
|
|
265
|
+
await systemctl(['enable', '--now', `${label}.timer`], true);
|
|
266
|
+
return { installed: true, label, path: join(directory, `${label}.timer`), frequency };
|
|
267
|
+
}
|
|
268
|
+
await writeCrontab(await crontabWithout(label, cronLine({ label, root, frequency })));
|
|
269
|
+
return { installed: true, label, frequency };
|
|
270
|
+
}
|
|
271
|
+
if (platform === 'win32') {
|
|
272
|
+
await run('schtasks', [
|
|
273
|
+
'/create',
|
|
274
|
+
'/f',
|
|
275
|
+
'/tn',
|
|
276
|
+
label,
|
|
277
|
+
'/sc',
|
|
278
|
+
windowsSchedule(frequency).type,
|
|
279
|
+
...windowsSchedule(frequency).arguments,
|
|
280
|
+
'/tr',
|
|
281
|
+
`"${process.execPath}" "${cliPath()}" sync now --trigger schedule --cwd "${root}"`,
|
|
282
|
+
]);
|
|
283
|
+
return { installed: true, label, frequency };
|
|
284
|
+
}
|
|
285
|
+
throw new DoxloopError(`Scheduled documentation sync is not supported on ${platform}. Run \`doxloop sync now\` from your own scheduler instead.`);
|
|
286
|
+
}
|
|
287
|
+
export async function removeSchedule(root, platform = process.platform) {
|
|
288
|
+
const label = scheduleLabel(root);
|
|
289
|
+
if (platform === 'darwin') {
|
|
290
|
+
const path = join(homedir(), 'Library', 'LaunchAgents', `com.doxbrix.${label}.plist`);
|
|
291
|
+
await launchctl(['bootout', `gui/${process.getuid?.() ?? 0}`, path]);
|
|
292
|
+
await rm(path, { force: true });
|
|
293
|
+
await rm(nativeScheduleLog(label), { force: true });
|
|
294
|
+
return { installed: false, label, path, logPath: nativeScheduleLog(label) };
|
|
295
|
+
}
|
|
296
|
+
if (platform === 'linux') {
|
|
297
|
+
const directory = join(homedir(), '.config', 'systemd', 'user');
|
|
298
|
+
if (await pathExists(join(directory, `${label}.timer`))) {
|
|
299
|
+
await systemctl(['disable', '--now', `${label}.timer`]);
|
|
300
|
+
await rm(join(directory, `${label}.timer`), { force: true });
|
|
301
|
+
await rm(join(directory, `${label}.service`), { force: true });
|
|
302
|
+
await systemctl(['daemon-reload']);
|
|
303
|
+
return { installed: false, label };
|
|
304
|
+
}
|
|
305
|
+
await writeCrontab(await crontabWithout(label));
|
|
306
|
+
return { installed: false, label };
|
|
307
|
+
}
|
|
308
|
+
if (platform === 'win32') {
|
|
309
|
+
await run('schtasks', ['/delete', '/f', '/tn', label]).catch(() => undefined);
|
|
310
|
+
return { installed: false, label };
|
|
311
|
+
}
|
|
312
|
+
return { installed: false, label };
|
|
313
|
+
}
|
|
314
|
+
export async function scheduleState(root, platform = process.platform) {
|
|
315
|
+
const label = scheduleLabel(root);
|
|
316
|
+
if (platform === 'darwin') {
|
|
317
|
+
const path = join(homedir(), 'Library', 'LaunchAgents', `com.doxbrix.${label}.plist`);
|
|
318
|
+
const installed = await pathExists(path);
|
|
319
|
+
if (!installed) {
|
|
320
|
+
return {
|
|
321
|
+
installed: false,
|
|
322
|
+
label,
|
|
323
|
+
path,
|
|
324
|
+
registered: false,
|
|
325
|
+
logPath: nativeScheduleLog(label),
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
const native = await launchAgentState(label);
|
|
329
|
+
return native
|
|
330
|
+
? {
|
|
331
|
+
installed: true,
|
|
332
|
+
label,
|
|
333
|
+
path,
|
|
334
|
+
registered: true,
|
|
335
|
+
logPath: nativeScheduleLog(label),
|
|
336
|
+
...native,
|
|
337
|
+
}
|
|
338
|
+
: {
|
|
339
|
+
installed: true,
|
|
340
|
+
label,
|
|
341
|
+
path,
|
|
342
|
+
registered: false,
|
|
343
|
+
logPath: nativeScheduleLog(label),
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
if (platform === 'linux') {
|
|
347
|
+
const path = join(homedir(), '.config', 'systemd', 'user', `${label}.timer`);
|
|
348
|
+
if (await pathExists(path))
|
|
349
|
+
return { installed: true, label, path };
|
|
350
|
+
return { installed: (await currentCrontab()).includes(`# ${label}`), label };
|
|
351
|
+
}
|
|
352
|
+
if (platform === 'win32') {
|
|
353
|
+
const found = await run('schtasks', ['/query', '/tn', label])
|
|
354
|
+
.then(() => true)
|
|
355
|
+
.catch(() => false);
|
|
356
|
+
return { installed: found, label };
|
|
357
|
+
}
|
|
358
|
+
return { installed: false, label };
|
|
359
|
+
}
|
|
360
|
+
function nativeScheduleLog(label) {
|
|
361
|
+
return join(homedir(), 'Library', 'Logs', 'Doxloop', `${label}.log`);
|
|
362
|
+
}
|
|
363
|
+
export function parseLaunchctlState(output) {
|
|
364
|
+
const runs = /\bruns = (\d+)/.exec(output);
|
|
365
|
+
const lastExit = /\blast exit code = (\d+)/.exec(output);
|
|
366
|
+
return {
|
|
367
|
+
running: /\bstate = running\b/.test(output),
|
|
368
|
+
...(runs ? { runs: Number(runs[1]) } : {}),
|
|
369
|
+
...(lastExit ? { lastExitCode: Number(lastExit[1]) } : {}),
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
async function launchAgentState(label) {
|
|
373
|
+
try {
|
|
374
|
+
const service = `gui/${process.getuid?.() ?? 0}/com.doxbrix.${label}`;
|
|
375
|
+
return parseLaunchctlState((await run('launchctl', ['print', service])).stdout);
|
|
376
|
+
}
|
|
377
|
+
catch {
|
|
378
|
+
return undefined;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
async function smokeLaunchAgent(label) {
|
|
382
|
+
const before = await launchAgentState(label);
|
|
383
|
+
const service = `gui/${process.getuid?.() ?? 0}/com.doxbrix.${label}`;
|
|
384
|
+
await launchctl(['kickstart', '-k', service], true);
|
|
385
|
+
const deadline = Date.now() + 8_000;
|
|
386
|
+
while (Date.now() < deadline) {
|
|
387
|
+
const state = await launchAgentState(label);
|
|
388
|
+
if (state?.running)
|
|
389
|
+
return state;
|
|
390
|
+
if (state && (state.runs ?? 0) > (before?.runs ?? 0)) {
|
|
391
|
+
// Exit 1 is the documented check-mode signal for detected drift. It
|
|
392
|
+
// still proves that launchd started Node and reached the Doxloop CLI.
|
|
393
|
+
if (state.lastExitCode === 0 || state.lastExitCode === 1)
|
|
394
|
+
return state;
|
|
395
|
+
if (state.lastExitCode !== undefined) {
|
|
396
|
+
throw new DoxloopError(`The scheduled sync started but failed its setup smoke test with exit ${state.lastExitCode}.`);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
await new Promise((resolveWait) => setTimeout(resolveWait, 200));
|
|
400
|
+
}
|
|
401
|
+
throw new DoxloopError('The scheduled sync did not start during its setup smoke test.');
|
|
402
|
+
}
|
|
403
|
+
async function launchctl(args, required = false) {
|
|
404
|
+
try {
|
|
405
|
+
await run('launchctl', args);
|
|
406
|
+
}
|
|
407
|
+
catch (error) {
|
|
408
|
+
// `bootout` fails when nothing is loaded, which is the normal install path.
|
|
409
|
+
if (required) {
|
|
410
|
+
throw new DoxloopError(`Could not register the scheduled sync with launchd: ${error instanceof Error ? error.message : String(error)}`);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
async function systemctl(args, required = false) {
|
|
415
|
+
try {
|
|
416
|
+
await run('systemctl', ['--user', ...args]);
|
|
417
|
+
}
|
|
418
|
+
catch (error) {
|
|
419
|
+
if (required) {
|
|
420
|
+
throw new DoxloopError(`Could not register the scheduled sync with systemd: ${error instanceof Error ? error.message : String(error)}`);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
async function hasSystemdUser() {
|
|
425
|
+
try {
|
|
426
|
+
await run('systemctl', ['--user', 'show-environment']);
|
|
427
|
+
return true;
|
|
428
|
+
}
|
|
429
|
+
catch {
|
|
430
|
+
return false;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
async function currentCrontab() {
|
|
434
|
+
try {
|
|
435
|
+
return (await run('crontab', ['-l'])).stdout;
|
|
436
|
+
}
|
|
437
|
+
catch {
|
|
438
|
+
return '';
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
/** Rewrite the user's crontab, preserving every line that is not ours. */
|
|
442
|
+
export function withoutLabel(crontab, label, add) {
|
|
443
|
+
const lines = crontab
|
|
444
|
+
.split('\n')
|
|
445
|
+
.filter((line) => line.trim() !== '' && !line.includes(`# ${label}`));
|
|
446
|
+
if (add)
|
|
447
|
+
lines.push(add);
|
|
448
|
+
return lines.length === 0 ? '' : `${lines.join('\n')}\n`;
|
|
449
|
+
}
|
|
450
|
+
async function crontabWithout(label, add) {
|
|
451
|
+
return withoutLabel(await currentCrontab(), label, add);
|
|
452
|
+
}
|
|
453
|
+
async function writeCrontab(content) {
|
|
454
|
+
const path = join(homedir(), `.doxloop-crontab-${process.pid}`);
|
|
455
|
+
await writeFile(path, content, 'utf8');
|
|
456
|
+
try {
|
|
457
|
+
await run('crontab', [path]);
|
|
458
|
+
}
|
|
459
|
+
finally {
|
|
460
|
+
await rm(path, { force: true });
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
export async function readSyncLog(root, lines = 20) {
|
|
464
|
+
const path = join(root, SYNC_LOG_FILE);
|
|
465
|
+
if (!(await pathExists(path)))
|
|
466
|
+
return [];
|
|
467
|
+
const content = await readFile(path, 'utf8');
|
|
468
|
+
return content.split('\n').filter(Boolean).slice(-lines);
|
|
469
|
+
}
|
|
470
|
+
export async function appendSyncLog(root, message) {
|
|
471
|
+
await withProjectLock(root, 'monitor-log', async () => {
|
|
472
|
+
const path = join(root, SYNC_LOG_FILE);
|
|
473
|
+
const size = await stat(path).then((info) => info.size).catch((error) => { if (error.code === 'ENOENT')
|
|
474
|
+
return 0; throw error; });
|
|
475
|
+
if (size >= 1_048_576)
|
|
476
|
+
await rename(path, `${path}.1`);
|
|
477
|
+
await appendFile(path, `${new Date().toISOString()} ${message.replace(/[\r\n]+/g, ' ').slice(0, 8000)}\n`, { mode: 0o600 });
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
//# sourceMappingURL=schedule.js.map
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { CaptureAuthMaterial, CaptureStorageState } from './capture-auth.js';
|
|
2
|
+
import type { ApplicationConfig } from './types.js';
|
|
3
|
+
export declare const SCREEN_CAPTURE_SERVER = "doxloop_capture";
|
|
4
|
+
export interface ScreenCaptureProvider {
|
|
5
|
+
name: typeof SCREEN_CAPTURE_SERVER;
|
|
6
|
+
command: string;
|
|
7
|
+
args: string[];
|
|
8
|
+
}
|
|
9
|
+
export interface ScreenCaptureBrowserReadiness {
|
|
10
|
+
available: boolean;
|
|
11
|
+
message: string;
|
|
12
|
+
}
|
|
13
|
+
/** Launch the same Chrome channel used by the MCP server, then close it. */
|
|
14
|
+
export declare function checkScreenCaptureBrowser(): Promise<ScreenCaptureBrowserReadiness>;
|
|
15
|
+
export interface CaptureSignInSession {
|
|
16
|
+
/** The page the window opened on. */
|
|
17
|
+
url: string;
|
|
18
|
+
/** Where the user currently is, or the last known page once the window closed. */
|
|
19
|
+
currentUrl(): string;
|
|
20
|
+
/** Whether the Chrome window is still open. */
|
|
21
|
+
open(): boolean;
|
|
22
|
+
/** Snapshot cookies and local storage now, or return the last snapshot if the window has closed. */
|
|
23
|
+
state(): Promise<CaptureStorageState | undefined>;
|
|
24
|
+
/** Snapshot the session and close the window. */
|
|
25
|
+
finish(): Promise<CaptureStorageState>;
|
|
26
|
+
/** Close the window without keeping anything. */
|
|
27
|
+
cancel(): Promise<void>;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Open a visible Chrome window on the application's sign-in page so the user
|
|
31
|
+
* can authenticate by hand, including MFA, SSO, and passkeys. The session is
|
|
32
|
+
* snapshotted every couple of seconds while the window is open, so closing
|
|
33
|
+
* the window early still leaves the last signed-in state to save.
|
|
34
|
+
*/
|
|
35
|
+
export declare function startCaptureSignIn(application: ApplicationConfig): Promise<CaptureSignInSession>;
|
|
36
|
+
/**
|
|
37
|
+
* Build a private Playwright MCP server for one authoring run. The server is
|
|
38
|
+
* passed directly to the selected agent, so users do not have to install or
|
|
39
|
+
* configure a browser plugin in Codex or Claude Code themselves.
|
|
40
|
+
*/
|
|
41
|
+
export declare function screenCaptureProvider(workspace: string, application: ApplicationConfig, auth?: Pick<CaptureAuthMaterial, 'storageStatePath' | 'secretsPath'>, outputDirectory?: string): ScreenCaptureProvider;
|
|
42
|
+
export declare function codexCaptureArguments(provider: ScreenCaptureProvider, required: boolean): string[];
|
|
43
|
+
export declare function claudeCaptureArguments(provider: ScreenCaptureProvider): string[];
|
|
44
|
+
/**
|
|
45
|
+
* Gemini CLI has no flag for a one-off MCP server: it reads `mcpServers` from
|
|
46
|
+
* the project's `.gemini/settings.json`. Merge the run's capture server into
|
|
47
|
+
* that file, keeping every other setting the user has there. `trust` lets the
|
|
48
|
+
* capture tools run without a confirmation Gemini cannot get unattended.
|
|
49
|
+
*/
|
|
50
|
+
export declare function writeGeminiCaptureSettings(root: string, provider?: ScreenCaptureProvider): Promise<string>;
|
|
51
|
+
export declare function geminiCaptureServer(provider: ScreenCaptureProvider): Record<string, unknown>;
|
|
52
|
+
//# sourceMappingURL=screen-capture-provider.d.ts.map
|