dev-cockpit 0.1.0 → 0.2.2

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 (153) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/README.md +86 -29
  3. package/bin/dev-cockpit.mjs +26 -4
  4. package/dist/actions/builtin.d.ts +25 -0
  5. package/dist/actions/builtin.d.ts.map +1 -0
  6. package/dist/actions/dispatch.d.ts +21 -0
  7. package/dist/actions/dispatch.d.ts.map +1 -0
  8. package/dist/actions/registry.d.ts +11 -0
  9. package/dist/actions/registry.d.ts.map +1 -0
  10. package/dist/actions/types.d.ts +76 -0
  11. package/dist/actions/types.d.ts.map +1 -0
  12. package/dist/buildCli.d.ts.map +1 -1
  13. package/dist/chunk-6XGHLLYT.js +46 -0
  14. package/dist/chunk-6XGHLLYT.js.map +7 -0
  15. package/dist/chunk-C4GFJDMG.js +79 -0
  16. package/dist/chunk-C4GFJDMG.js.map +7 -0
  17. package/dist/chunk-Q6677JQF.js +32609 -0
  18. package/dist/chunk-Q6677JQF.js.map +7 -0
  19. package/dist/chunk-VN6UILQW.js +1460 -0
  20. package/dist/chunk-VN6UILQW.js.map +7 -0
  21. package/dist/cockpit/Cockpit.d.ts +6 -0
  22. package/dist/cockpit/Cockpit.d.ts.map +1 -1
  23. package/dist/cockpit/Footer.d.ts +6 -4
  24. package/dist/cockpit/Footer.d.ts.map +1 -1
  25. package/dist/cockpit/TabBar.d.ts.map +1 -1
  26. package/dist/cockpit/hooks/useGlobalKeys.d.ts +15 -15
  27. package/dist/cockpit/hooks/useGlobalKeys.d.ts.map +1 -1
  28. package/dist/cockpit/hooks/useTerminalWidth.d.ts +12 -0
  29. package/dist/cockpit/hooks/useTerminalWidth.d.ts.map +1 -0
  30. package/dist/cockpit/panes/CommandModal.d.ts +18 -0
  31. package/dist/cockpit/panes/CommandModal.d.ts.map +1 -0
  32. package/dist/cockpit/panes/Help.d.ts.map +1 -1
  33. package/dist/cockpit/panes/Output.d.ts +7 -0
  34. package/dist/cockpit/panes/Output.d.ts.map +1 -1
  35. package/dist/cockpit/panes/Repos.d.ts.map +1 -1
  36. package/dist/cockpit/state/store.d.ts +14 -11
  37. package/dist/cockpit/state/store.d.ts.map +1 -1
  38. package/dist/cockpit/tab-state.d.ts +12 -0
  39. package/dist/cockpit/tab-state.d.ts.map +1 -1
  40. package/dist/commands/dev.d.ts.map +1 -1
  41. package/dist/commands/doctor.d.ts.map +1 -1
  42. package/dist/commands/init-config-wizard.d.ts +103 -2
  43. package/dist/commands/init-config-wizard.d.ts.map +1 -1
  44. package/dist/commands/init-config.d.ts +2 -0
  45. package/dist/commands/init-config.d.ts.map +1 -1
  46. package/dist/commands/link.d.ts +20 -0
  47. package/dist/commands/link.d.ts.map +1 -0
  48. package/dist/commands/migrate-config.d.ts +18 -0
  49. package/dist/commands/migrate-config.d.ts.map +1 -0
  50. package/dist/commands/mount.d.ts +17 -32
  51. package/dist/commands/mount.d.ts.map +1 -1
  52. package/dist/core/config-discovery.d.ts +39 -0
  53. package/dist/core/config-discovery.d.ts.map +1 -0
  54. package/dist/core/config.d.ts +73 -5
  55. package/dist/core/config.d.ts.map +1 -1
  56. package/dist/core/manifest.d.ts +47 -0
  57. package/dist/core/manifest.d.ts.map +1 -0
  58. package/dist/core/migrations.d.ts +33 -0
  59. package/dist/core/migrations.d.ts.map +1 -0
  60. package/dist/core/subprocess.d.ts +20 -0
  61. package/dist/core/subprocess.d.ts.map +1 -1
  62. package/dist/core/types.d.ts +36 -12
  63. package/dist/core/types.d.ts.map +1 -1
  64. package/dist/devtools-YXMW6JJ6.js +3720 -0
  65. package/dist/devtools-YXMW6JJ6.js.map +7 -0
  66. package/dist/docker/highlights.d.ts +14 -4
  67. package/dist/docker/highlights.d.ts.map +1 -1
  68. package/dist/docker/logs.d.ts +3 -2
  69. package/dist/docker/logs.d.ts.map +1 -1
  70. package/dist/health/builtin.d.ts.map +1 -1
  71. package/dist/index.d.ts +14 -3
  72. package/dist/index.d.ts.map +1 -1
  73. package/dist/index.js +92944 -53
  74. package/dist/index.js.map +7 -0
  75. package/dist/ink.js +38 -1
  76. package/dist/ink.js.map +7 -0
  77. package/dist/link-HXNII7EU.js +65 -0
  78. package/dist/link-HXNII7EU.js.map +7 -0
  79. package/dist/mount/compose.d.ts +21 -0
  80. package/dist/mount/compose.d.ts.map +1 -0
  81. package/dist/mount/discovery.d.ts +35 -0
  82. package/dist/mount/discovery.d.ts.map +1 -0
  83. package/dist/mount/git-status.d.ts +12 -0
  84. package/dist/mount/git-status.d.ts.map +1 -0
  85. package/dist/mount/manifest.d.ts +16 -0
  86. package/dist/mount/manifest.d.ts.map +1 -0
  87. package/dist/mount/symlinks.d.ts +30 -0
  88. package/dist/mount/symlinks.d.ts.map +1 -0
  89. package/dist/mount/types.d.ts +60 -0
  90. package/dist/mount/types.d.ts.map +1 -0
  91. package/dist/react.js +35 -1
  92. package/dist/react.js.map +7 -0
  93. package/dist/runCockpit.d.ts +3 -0
  94. package/dist/runCockpit.d.ts.map +1 -1
  95. package/docs/commands.md +29 -16
  96. package/docs/config-reference.md +115 -11
  97. package/docs/getting-started.md +9 -6
  98. package/docs/index.md +5 -1
  99. package/docs/init-config.md +34 -8
  100. package/docs/mount.md +198 -25
  101. package/docs/notifications.md +14 -13
  102. package/docs/panes.md +36 -15
  103. package/docs/processes.md +42 -0
  104. package/package.json +93 -90
  105. package/dist/buildCli.js +0 -107
  106. package/dist/cli.js +0 -2
  107. package/dist/cockpit/Cockpit.js +0 -73
  108. package/dist/cockpit/Footer.js +0 -33
  109. package/dist/cockpit/TabBar.js +0 -12
  110. package/dist/cockpit/help/content.js +0 -22
  111. package/dist/cockpit/help/loader.js +0 -118
  112. package/dist/cockpit/help/renderer.js +0 -35
  113. package/dist/cockpit/help/types.js +0 -1
  114. package/dist/cockpit/hooks/useCockpitStore.js +0 -5
  115. package/dist/cockpit/hooks/useGlobalKeys.js +0 -173
  116. package/dist/cockpit/panes/FilterModal.js +0 -22
  117. package/dist/cockpit/panes/Health.js +0 -30
  118. package/dist/cockpit/panes/Help.js +0 -81
  119. package/dist/cockpit/panes/Output.js +0 -108
  120. package/dist/cockpit/panes/Repos.js +0 -48
  121. package/dist/cockpit/panes/SearchModal.js +0 -31
  122. package/dist/cockpit/state/store.js +0 -111
  123. package/dist/cockpit/tab-state.js +0 -7
  124. package/dist/commands/dev.js +0 -158
  125. package/dist/commands/doctor.js +0 -66
  126. package/dist/commands/init-config-wizard.js +0 -818
  127. package/dist/commands/init-config.js +0 -131
  128. package/dist/commands/mount.js +0 -150
  129. package/dist/core/config.js +0 -152
  130. package/dist/core/logger.js +0 -38
  131. package/dist/core/notifier.js +0 -100
  132. package/dist/core/paths.js +0 -18
  133. package/dist/core/subprocess.js +0 -82
  134. package/dist/core/types.js +0 -1
  135. package/dist/docker/highlights.js +0 -79
  136. package/dist/docker/logs.js +0 -172
  137. package/dist/docker/restart.js +0 -45
  138. package/dist/docker/stack-trace.js +0 -44
  139. package/dist/health/builtin.js +0 -144
  140. package/dist/health/context.js +0 -31
  141. package/dist/health/notify-resolver.js +0 -28
  142. package/dist/health/registry.js +0 -64
  143. package/dist/health/remediations.js +0 -41
  144. package/dist/health/runner.js +0 -22
  145. package/dist/health/scheduler.js +0 -107
  146. package/dist/health/types.js +0 -1
  147. package/dist/health/useHealth.js +0 -122
  148. package/dist/lint/reactive.js +0 -131
  149. package/dist/runCockpit.js +0 -75
  150. package/dist/watchers/manager.js +0 -239
  151. package/dist/watchers/path-mapper.js +0 -29
  152. package/dist/watchers/types.js +0 -9
  153. package/docs/watchers.md +0 -27
@@ -1,131 +0,0 @@
1
- /**
2
- * `dev-cockpit init-config` — write a starter `cockpit.yaml`.
3
- *
4
- * The template is intentionally minimal and domain-neutral. Every section is
5
- * commented out except `version` and `appName`; users uncomment the blocks
6
- * they need. `--with-docker` includes the docker stanza uncommented as a
7
- * jumping-off point for containerized workflows.
8
- */
9
- import fs from 'node:fs';
10
- import path from 'node:path';
11
- import { renderWizardYaml, runInitWizard } from './init-config-wizard.js';
12
- import { doctorCommand } from './doctor.js';
13
- export function buildTemplate(opts) {
14
- const dockerBlock = opts.withDocker
15
- ? `
16
- # Docker services to tail in the Output pane. Remove this block on plain-Node
17
- # projects.
18
- docker:
19
- composeFile: docker-compose.yml
20
- services:
21
- - name: app
22
- tail: true
23
- `
24
- : `
25
- # Docker services to tail in the Output pane. Uncomment for containerized projects.
26
- # docker:
27
- # composeFile: docker-compose.yml
28
- # services:
29
- # - name: app
30
- # tail: true
31
- `;
32
- return `# dev-cockpit configuration. All keys except \`version\` and \`appName\` are
33
- # optional; defaults from src/core/config.ts apply when missing.
34
-
35
- version: 1
36
- appName: ${opts.appName}
37
-
38
- # Long-running watcher processes streamed into the Output pane.
39
- # watchers:
40
- # - id: build
41
- # command: npm run watch
42
- # color: cyan
43
-
44
- # Repos surfaced in the Repos pane (linkable to watchers / lint).
45
- # repos:
46
- # - id: app
47
- # path: .
48
- # label: app
49
- ${dockerBlock}
50
- # Output highlight patterns (regex) — matched lines render with elevated severity.
51
- # highlights:
52
- # - pattern: 'ERROR'
53
- # severity: error
54
- # - pattern: 'WARN'
55
- # severity: warn
56
-
57
- # Health checks. Built-in types: container-running, port-open, http-ok,
58
- # file-exists, exec-zero. See docs/health.md for the full schema.
59
- # health:
60
- # - id: app-up
61
- # label: app responsive
62
- # type: http-ok
63
- # url: http://localhost:3000/health
64
-
65
- # Help-pane sources (markdown trees) and the landing page slug.
66
- # help:
67
- # sources: []
68
- # defaultPage: getting-started
69
-
70
- # Native OS notifications. Set enabled: false to mute entirely.
71
- # notifications:
72
- # enabled: true
73
- # exclude: []
74
-
75
- # Bind-mount overlays applied by \`dev-cockpit mount\`.
76
- # mounts:
77
- # - hostPath: ./packages/api
78
- # containerPath: /srv/api
79
- `;
80
- }
81
- export async function initConfigCommand(opts = {}) {
82
- const cwd = opts.cwd ?? process.cwd();
83
- const target = path.join(cwd, 'cockpit.yaml');
84
- const exists = fs.existsSync(target);
85
- if (exists && !opts.force) {
86
- process.stdout.write(`dev-cockpit init-config: ${target} already exists.\n` +
87
- ` Pass --force to overwrite.\n`);
88
- return null;
89
- }
90
- const content = opts.interactive
91
- ? renderWizardYaml(await runInitWizard({
92
- defaultAppName: opts.appName ?? (path.basename(cwd) || 'my-cockpit'),
93
- cwd,
94
- }))
95
- : buildTemplate({
96
- withDocker: opts.withDocker ?? false,
97
- appName: opts.appName ?? 'my-cockpit',
98
- });
99
- fs.writeFileSync(target, content, 'utf8');
100
- process.stdout.write(`\ndev-cockpit init-config: wrote ${target}\n`);
101
- if (opts.withDocker) {
102
- process.stdout.write(' Docker block uncommented; adjust composeFile/services as needed.\n');
103
- }
104
- if (opts.interactive) {
105
- // Offer to run doctor immediately so the user gets instant feedback that
106
- // the config parses + every health check has a baseline state. Note for
107
- // edits: re-run `dev-cockpit doctor` any time after hand-editing.
108
- const { confirm } = (await import('@inquirer/prompts'));
109
- const runDoctorNow = await confirm({
110
- message: 'Run `dev-cockpit doctor` now to validate the file + show initial health?',
111
- default: true,
112
- });
113
- if (runDoctorNow) {
114
- process.stdout.write('\n');
115
- try {
116
- await doctorCommand({ config: target, profile: opts.profile });
117
- }
118
- catch (err) {
119
- process.stderr.write(`\ndoctor failed: ${err instanceof Error ? err.message : String(err)}\n`);
120
- }
121
- }
122
- process.stdout.write('\nNext:\n');
123
- process.stdout.write(' • Edit cockpit.yaml any time, then re-run `dev-cockpit doctor`.\n');
124
- process.stdout.write(' • Run `dev-cockpit dev` to launch the cockpit (Tab/arrows cycle panes, q quits).\n');
125
- process.stdout.write(' • The Help tab inside the cockpit has the full docs.\n');
126
- }
127
- else {
128
- process.stdout.write(' Run `dev-cockpit doctor` to verify the config loads cleanly.\n');
129
- }
130
- return { path: target, overwrote: exists };
131
- }
@@ -1,150 +0,0 @@
1
- /**
2
- * `dev-cockpit mount` — generic bind-mount overlay manager.
3
- *
4
- * Collects mount candidates from two sources and writes a Docker compose
5
- * override file plus a manifest:
6
- * 1. `config.mounts[]` — explicit, declared in cockpit.yaml
7
- * 2. `profile.mountCandidatesProvider?.()` — discovered programmatically
8
- *
9
- * The merged set is keyed by `containerPath`; if both sources name the same
10
- * container path, the config entry wins (explicit > discovered). The
11
- * compose override targets a single configurable service (default: first
12
- * service in `config.docker.services`).
13
- *
14
- * Subcommands:
15
- * mount — write/refresh the overlay and manifest
16
- * mount status — list active mounts from the manifest
17
- * mount clear — remove overlay and manifest
18
- */
19
- import path from 'node:path';
20
- import fs from 'node:fs';
21
- import { loadConfig } from '../core/config.js';
22
- import { getStatePaths } from '../core/paths.js';
23
- /**
24
- * Merge mount sources, keyed on containerPath. Config entries win over
25
- * provider-discovered ones for the same container path.
26
- */
27
- export function mergeMounts(configMounts, providerMounts) {
28
- const seen = new Map();
29
- for (const m of providerMounts)
30
- seen.set(m.containerPath, m);
31
- for (const m of configMounts)
32
- seen.set(m.containerPath, m);
33
- return Array.from(seen.values());
34
- }
35
- /**
36
- * Render a Docker compose override YAML for the given service + mounts.
37
- * Hand-rolled (no `yaml` writer) so the output stays stable + diff-friendly.
38
- */
39
- export function renderOverlay(service, mounts) {
40
- if (mounts.length === 0) {
41
- return `# dev-cockpit: no mounts to apply\nservices: {}\n`;
42
- }
43
- const lines = [
44
- '# Generated by `dev-cockpit mount`. Do not edit by hand — re-run the command.',
45
- 'services:',
46
- ` ${service}:`,
47
- ' volumes:',
48
- ];
49
- for (const m of mounts) {
50
- lines.push(` - ${m.hostPath}:${m.containerPath}`);
51
- }
52
- lines.push('');
53
- return lines.join('\n');
54
- }
55
- function resolveOverlayPath(workspaceRoot) {
56
- return path.join(workspaceRoot, 'docker-compose.dev-cockpit.yml');
57
- }
58
- function manifestPath(workspaceRoot, appName) {
59
- const paths = getStatePaths(workspaceRoot, { appName });
60
- return path.join(paths.stateDir, 'mount.manifest.json');
61
- }
62
- function loadCommandConfig(opts) {
63
- const profile = opts.profile;
64
- const configPath = path.resolve(opts.config ?? 'cockpit.yaml');
65
- if (!fs.existsSync(configPath)) {
66
- process.stderr.write(`dev-cockpit mount: no cockpit.yaml found at ${configPath}.\n`);
67
- process.exit(1);
68
- }
69
- const config = loadConfig(configPath, {
70
- configSchemaExt: profile?.configSchemaExt,
71
- profileKey: profile?.appName,
72
- });
73
- const workspaceRoot = profile?.discoverer?.()?.root ?? path.dirname(configPath);
74
- const explicitService = opts.service ?? config.docker?.services?.[0]?.name;
75
- if (!explicitService) {
76
- process.stderr.write('dev-cockpit mount: no target service. Pass --service <name> or set docker.services in cockpit.yaml.\n');
77
- process.exit(1);
78
- }
79
- const providerMounts = profile?.mountCandidatesProvider?.() ?? [];
80
- return {
81
- configPath,
82
- workspaceRoot,
83
- appName: config.appName,
84
- service: explicitService,
85
- configMounts: config.mounts,
86
- providerMounts,
87
- };
88
- }
89
- export async function mountCommand(opts = {}) {
90
- const { workspaceRoot, appName, service, configMounts, providerMounts } = loadCommandConfig(opts);
91
- const merged = mergeMounts(configMounts, providerMounts);
92
- if (merged.length === 0) {
93
- process.stdout.write('dev-cockpit mount: no mount candidates from config.mounts[] or profile.mountCandidatesProvider.\n');
94
- return;
95
- }
96
- const overlayPath = resolveOverlayPath(workspaceRoot);
97
- fs.writeFileSync(overlayPath, renderOverlay(service, merged), 'utf8');
98
- const manifest = {
99
- appName,
100
- workspaceRoot,
101
- service,
102
- overlayPath,
103
- mounts: merged,
104
- appliedAt: new Date().toISOString(),
105
- };
106
- fs.writeFileSync(manifestPath(workspaceRoot, appName), JSON.stringify(manifest, null, 2), 'utf8');
107
- process.stdout.write(`dev-cockpit mount: wrote ${overlayPath}\n`);
108
- for (const m of merged) {
109
- process.stdout.write(` ${m.hostPath} → ${service}:${m.containerPath}\n`);
110
- }
111
- process.stdout.write(` Apply with: docker compose -f <your-compose>.yml -f ${path.basename(overlayPath)} up -d\n`);
112
- }
113
- export async function mountStatusCommand(opts = {}) {
114
- const { workspaceRoot, appName } = loadCommandConfig(opts);
115
- const target = manifestPath(workspaceRoot, appName);
116
- if (!fs.existsSync(target)) {
117
- process.stdout.write('dev-cockpit mount: no active overlay (no manifest found).\n');
118
- return;
119
- }
120
- const manifest = JSON.parse(fs.readFileSync(target, 'utf8'));
121
- process.stdout.write(`dev-cockpit mount status — service: ${manifest.service}\n`);
122
- process.stdout.write(` overlay: ${manifest.overlayPath}\n`);
123
- process.stdout.write(` applied: ${manifest.appliedAt}\n`);
124
- if (manifest.mounts.length === 0) {
125
- process.stdout.write(' (no mounts)\n');
126
- return;
127
- }
128
- for (const m of manifest.mounts) {
129
- process.stdout.write(` ${m.hostPath} → ${m.containerPath}\n`);
130
- }
131
- }
132
- export async function mountClearCommand(opts = {}) {
133
- const { workspaceRoot, appName } = loadCommandConfig(opts);
134
- const overlayPath = resolveOverlayPath(workspaceRoot);
135
- const target = manifestPath(workspaceRoot, appName);
136
- let removed = 0;
137
- if (fs.existsSync(overlayPath)) {
138
- fs.rmSync(overlayPath);
139
- removed += 1;
140
- process.stdout.write(`dev-cockpit mount: removed ${overlayPath}\n`);
141
- }
142
- if (fs.existsSync(target)) {
143
- fs.rmSync(target);
144
- removed += 1;
145
- process.stdout.write(`dev-cockpit mount: removed ${target}\n`);
146
- }
147
- if (removed === 0) {
148
- process.stdout.write('dev-cockpit mount: nothing to clear.\n');
149
- }
150
- }
@@ -1,152 +0,0 @@
1
- import fs from 'node:fs';
2
- import { parse as parseYaml } from 'yaml';
3
- import { z } from 'zod';
4
- /** Schema version constant. Bumps reserved for breaking changes (see ADR 0004). */
5
- export const CONFIG_VERSION = 1;
6
- // ─── Sub-schemas ────────────────────────────────────────────────────────────
7
- const NotifyOverrideSchema = z.union([
8
- z.literal(false),
9
- z.object({
10
- onTransitionTo: z.array(z.string()).optional(),
11
- }),
12
- ]);
13
- const WatcherSchema = z.object({
14
- id: z.string(),
15
- label: z.string().optional(),
16
- command: z.string(),
17
- cwd: z.string().optional(),
18
- env: z.record(z.string()).optional(),
19
- color: z.string().optional(),
20
- restartOn: z.array(z.string()).optional(),
21
- notify: NotifyOverrideSchema.optional(),
22
- });
23
- const RepoSchema = z.object({
24
- id: z.string(),
25
- path: z.string(),
26
- label: z.string().optional(),
27
- });
28
- const ServiceSchema = z.object({
29
- name: z.string(),
30
- tail: z.boolean().optional().default(true),
31
- });
32
- const DockerSchema = z.object({
33
- composeFile: z.string().optional(),
34
- services: z.array(ServiceSchema).optional().default([]),
35
- });
36
- const HighlightSchema = z.object({
37
- pattern: z.string(),
38
- severity: z.enum(['info', 'warn', 'error']).optional().default('info'),
39
- });
40
- const RemediationSchema = z.object({
41
- key: z.string(),
42
- label: z.string(),
43
- command: z.string(),
44
- cwd: z.string().optional(),
45
- });
46
- const TriggerKindSchema = z.enum(['startup', 'fsevent', 'lockfile', 'docker']);
47
- const HealthCheckSchema = z.object({
48
- id: z.string(),
49
- label: z.string(),
50
- type: z.string(),
51
- severity: z.enum(['ok', 'warn', 'error']).optional(),
52
- triggers: z.array(TriggerKindSchema).optional(),
53
- url: z.string().optional(),
54
- expectStatus: z.number().int().optional(),
55
- container: z.string().optional(),
56
- port: z.number().int().optional(),
57
- host: z.string().optional(),
58
- path: z.string().optional(),
59
- command: z.string().optional(),
60
- args: z.array(z.string()).optional(),
61
- cwd: z.string().optional(),
62
- notify: NotifyOverrideSchema.optional(),
63
- remediation: RemediationSchema,
64
- });
65
- const HelpSchema = z.object({
66
- sources: z.array(z.string()).optional().default([]),
67
- defaultPage: z.string().optional(),
68
- });
69
- const NotificationsSchema = z.object({
70
- enabled: z.boolean().optional().default(true),
71
- onTransitionTo: z.array(z.string()).optional().default([]),
72
- exclude: z.array(z.string()).optional().default([]),
73
- });
74
- const MountSchema = z.object({
75
- hostPath: z.string(),
76
- containerPath: z.string(),
77
- });
78
- // ─── Base schema ────────────────────────────────────────────────────────────
79
- export const BaseCockpitConfigSchema = z.object({
80
- version: z.number().int(),
81
- appName: z.string(),
82
- watchers: z.array(WatcherSchema).optional().default([]),
83
- repos: z.array(RepoSchema).optional().default([]),
84
- docker: DockerSchema.optional(),
85
- highlights: z.array(HighlightSchema).optional().default([]),
86
- health: z.array(HealthCheckSchema).optional().default([]),
87
- help: HelpSchema.optional().default({ sources: [] }),
88
- notifications: NotificationsSchema.optional().default({
89
- enabled: true,
90
- onTransitionTo: [],
91
- exclude: [],
92
- }),
93
- mounts: z.array(MountSchema).optional().default([]),
94
- /**
95
- * Profile-specific config namespace. Each profile owns one key under here
96
- * (e.g. `profile.myapp = { ... }`) and validates it with its own zod schema
97
- * passed to the loader as `configSchemaExt`.
98
- */
99
- profile: z.record(z.unknown()).optional().default({}),
100
- });
101
- // ─── Errors ─────────────────────────────────────────────────────────────────
102
- export class ConfigVersionError extends Error {
103
- filePath;
104
- found;
105
- supported;
106
- constructor(filePath, found, supported) {
107
- super(`cockpit config at "${filePath}" declares version ${String(found)}, but this build supports version ${supported}`);
108
- this.filePath = filePath;
109
- this.found = found;
110
- this.supported = supported;
111
- this.name = 'ConfigVersionError';
112
- }
113
- }
114
- export class ConfigValidationError extends Error {
115
- filePath;
116
- cause;
117
- constructor(filePath, cause) {
118
- super(`invalid cockpit config at "${filePath}": ${String(cause)}`);
119
- this.filePath = filePath;
120
- this.cause = cause;
121
- this.name = 'ConfigValidationError';
122
- }
123
- }
124
- /**
125
- * Loads, parses, and validates a `cockpit.yaml` (or any path the caller chooses).
126
- * Throws `ConfigVersionError` if the file's `version` does not match `CONFIG_VERSION`.
127
- * Throws `ConfigValidationError` if the schema fails.
128
- */
129
- export function loadConfig(filePath, opts = {}) {
130
- if (!fs.existsSync(filePath)) {
131
- throw new ConfigValidationError(filePath, 'file does not exist');
132
- }
133
- const content = fs.readFileSync(filePath, 'utf8');
134
- const raw = (parseYaml(content) ?? {});
135
- if (typeof raw['version'] !== 'number' || raw['version'] !== CONFIG_VERSION) {
136
- throw new ConfigVersionError(filePath, raw['version'], CONFIG_VERSION);
137
- }
138
- const baseResult = BaseCockpitConfigSchema.safeParse(raw);
139
- if (!baseResult.success) {
140
- throw new ConfigValidationError(filePath, baseResult.error);
141
- }
142
- const config = baseResult.data;
143
- if (opts.configSchemaExt && opts.profileKey) {
144
- const profileBlock = config.profile?.[opts.profileKey] ?? {};
145
- const profileResult = opts.configSchemaExt.safeParse(profileBlock);
146
- if (!profileResult.success) {
147
- throw new ConfigValidationError(filePath, profileResult.error);
148
- }
149
- config.profile[opts.profileKey] = profileResult.data;
150
- }
151
- return config;
152
- }
@@ -1,38 +0,0 @@
1
- import fs from 'node:fs';
2
- import pino from 'pino';
3
- const LOG_SIZE_CAP = 10 * 1024 * 1024;
4
- function rotateIfNeeded(logFile) {
5
- try {
6
- const stat = fs.statSync(logFile);
7
- if (stat.size > LOG_SIZE_CAP) {
8
- fs.renameSync(logFile, `${logFile}.1`);
9
- }
10
- }
11
- catch {
12
- // File doesn't exist yet — no rotation needed.
13
- }
14
- }
15
- export function createLogger(logFile) {
16
- rotateIfNeeded(logFile);
17
- const level = process.env['LOG_LEVEL'] ?? 'info';
18
- return pino({ level }, pino.transport({
19
- targets: [
20
- {
21
- target: 'pino/file',
22
- level,
23
- options: { destination: logFile, mkdir: true },
24
- },
25
- ],
26
- }));
27
- }
28
- let _logger = null;
29
- export function initLogger(logFile) {
30
- _logger = createLogger(logFile);
31
- return _logger;
32
- }
33
- export function getLogger() {
34
- if (!_logger) {
35
- throw new Error('logger not initialized — call initLogger(logFile) first');
36
- }
37
- return _logger;
38
- }
@@ -1,100 +0,0 @@
1
- export function detectTransitions(prev, next) {
2
- const prevMap = new Map();
3
- for (const s of prev) {
4
- prevMap.set(s.id, s);
5
- }
6
- const transitions = [];
7
- for (const nextStatus of next) {
8
- const prevStatus = prevMap.get(nextStatus.id);
9
- if (!prevStatus) {
10
- if (nextStatus.severity === 'error') {
11
- transitions.push({
12
- event: 'health-failed',
13
- id: nextStatus.id,
14
- label: nextStatus.label,
15
- detail: nextStatus.detail,
16
- });
17
- }
18
- continue;
19
- }
20
- const wasOk = prevStatus.severity !== 'error';
21
- const isOk = nextStatus.severity !== 'error';
22
- if (wasOk && !isOk) {
23
- transitions.push({
24
- event: 'health-failed',
25
- id: nextStatus.id,
26
- label: nextStatus.label,
27
- detail: nextStatus.detail,
28
- });
29
- }
30
- else if (!wasOk && isOk) {
31
- transitions.push({
32
- event: 'health-recovered',
33
- id: nextStatus.id,
34
- label: nextStatus.label,
35
- detail: nextStatus.detail,
36
- });
37
- }
38
- }
39
- return transitions;
40
- }
41
- export function notify(transition, opts) {
42
- const { config, sessionEnabled, sender, appName = 'cockpit' } = opts;
43
- if (!sessionEnabled)
44
- return;
45
- if (!config.enabled)
46
- return;
47
- if (config.exclude.includes(transition.event))
48
- return;
49
- const title = `${appName} — ${transition.label}`;
50
- const message = transition.detail;
51
- if (sender) {
52
- sender(title, message);
53
- return;
54
- }
55
- defaultPlatformNotify(title, message);
56
- }
57
- /**
58
- * OS notification via a detached child process. The child runs independently
59
- * (`detached: true`, `stdio: 'ignore'`, `unref()`) so it never enters the
60
- * parent's event loop — the cockpit can exit while the notification helper
61
- * is still flushing without leaving an unreaped ChildProcess handle.
62
- *
63
- * Platforms:
64
- * - darwin: `osascript -e 'display notification …'`
65
- * - linux: `notify-send`
66
- * - other: silently skipped
67
- */
68
- function defaultPlatformNotify(title, message) {
69
- // Lazy require so test environments don't pull child_process at import time.
70
- void import('node:child_process').then(({ spawn }) => {
71
- try {
72
- let child = null;
73
- if (process.platform === 'darwin') {
74
- const safeTitle = title.replace(/(["\\])/g, '\\$1');
75
- const safeMsg = message.replace(/(["\\])/g, '\\$1');
76
- child = spawn('osascript', ['-e', `display notification "${safeMsg}" with title "${safeTitle}"`], { detached: true, stdio: 'ignore' });
77
- }
78
- else if (process.platform === 'linux') {
79
- child = spawn('notify-send', [title, message], {
80
- detached: true,
81
- stdio: 'ignore',
82
- });
83
- }
84
- child?.unref();
85
- }
86
- catch {
87
- // Notification is best-effort. Don't crash the cockpit if the helper
88
- // binary is missing (e.g. headless Linux without notify-send).
89
- }
90
- });
91
- }
92
- export function notifyTransitions(prev, next, opts) {
93
- const transitions = detectTransitions(prev, next);
94
- for (const t of transitions) {
95
- notify(t, opts);
96
- }
97
- }
98
- export function emitEvent(event, label, detail, opts) {
99
- notify({ event, id: String(event), label, detail }, opts);
100
- }
@@ -1,18 +0,0 @@
1
- import crypto from 'node:crypto';
2
- import fs from 'node:fs';
3
- import os from 'node:os';
4
- import path from 'node:path';
5
- export function hashWorkspacePath(workspaceRoot) {
6
- return crypto.createHash('sha256').update(workspaceRoot).digest('hex').slice(0, 8);
7
- }
8
- export function getStatePaths(workspaceRoot, opts) {
9
- const xdgStateHome = process.env['XDG_STATE_HOME'] ?? path.join(os.homedir(), '.local', 'state');
10
- const hash = hashWorkspacePath(workspaceRoot);
11
- const stateDir = path.join(xdgStateHome, opts.appName, hash);
12
- fs.mkdirSync(stateDir, { recursive: true });
13
- return {
14
- stateDir,
15
- lockFile: path.join(stateDir, 'cockpit.lock'),
16
- logFile: path.join(stateDir, 'cockpit.log'),
17
- };
18
- }
@@ -1,82 +0,0 @@
1
- import { execa } from 'execa';
2
- /**
3
- * Module-level registry of every currently-running spawnStream child. Each
4
- * spawn auto-registers; auto-deregisters when the child exits. Callers that
5
- * own a long-running session (typically the `dev` command) sweep this on
6
- * teardown via {@link killAllSpawned} so no rogue subprocess keeps the Node
7
- * event loop alive after the cockpit unmounts.
8
- */
9
- const ACTIVE = new Set();
10
- export function spawnStream(cmd, args, opts = {}) {
11
- const child = execa(cmd, args, {
12
- cwd: opts.cwd,
13
- env: opts.env ? { ...process.env, ...opts.env } : process.env,
14
- all: false,
15
- lines: true,
16
- reject: false,
17
- stdout: 'pipe',
18
- stderr: 'pipe',
19
- });
20
- if (opts.onStdout && child.stdout) {
21
- child.stdout.on('data', (chunk) => {
22
- const text = chunk.toString();
23
- for (const line of text.split('\n')) {
24
- if (line)
25
- opts.onStdout(line);
26
- }
27
- });
28
- }
29
- if (opts.onStderr && child.stderr) {
30
- child.stderr.on('data', (chunk) => {
31
- const text = chunk.toString();
32
- for (const line of text.split('\n')) {
33
- if (line)
34
- opts.onStderr(line);
35
- }
36
- });
37
- }
38
- const exitCode = child.then((result) => result.exitCode ?? 0);
39
- const handle = {
40
- exitCode,
41
- kill: (signal = 'SIGTERM') => {
42
- child.kill(signal);
43
- },
44
- child,
45
- };
46
- ACTIVE.add(handle);
47
- void exitCode.finally(() => ACTIVE.delete(handle));
48
- return handle;
49
- }
50
- /**
51
- * Send SIGTERM to every still-running spawnStream child and await each one's
52
- * reap. After {@link sigkillAfterMs} the holdouts are SIGKILLed so the sweep
53
- * cannot hang indefinitely on an unresponsive subprocess.
54
- */
55
- export async function killAllSpawned(sigkillAfterMs = 1500) {
56
- if (ACTIVE.size === 0)
57
- return;
58
- const handles = Array.from(ACTIVE);
59
- for (const h of handles)
60
- h.kill('SIGTERM');
61
- const reaps = handles.map((h) => h.exitCode.catch(() => undefined));
62
- let timer = null;
63
- const guard = new Promise((resolve) => {
64
- timer = setTimeout(() => {
65
- for (const h of handles) {
66
- try {
67
- h.kill('SIGKILL');
68
- }
69
- catch {
70
- // already dead
71
- }
72
- }
73
- resolve();
74
- }, sigkillAfterMs);
75
- });
76
- await Promise.race([Promise.all(reaps), guard]);
77
- if (timer)
78
- clearTimeout(timer);
79
- // After the SIGKILL fallback, drain the original exitCodes so handles are
80
- // fully released from the event loop.
81
- await Promise.all(reaps);
82
- }
@@ -1 +0,0 @@
1
- export {};