dev-cockpit 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +64 -29
- package/bin/dev-cockpit.mjs +26 -4
- package/dist/actions/builtin.d.ts +25 -0
- package/dist/actions/builtin.d.ts.map +1 -0
- package/dist/actions/dispatch.d.ts +21 -0
- package/dist/actions/dispatch.d.ts.map +1 -0
- package/dist/actions/registry.d.ts +11 -0
- package/dist/actions/registry.d.ts.map +1 -0
- package/dist/actions/types.d.ts +76 -0
- package/dist/actions/types.d.ts.map +1 -0
- package/dist/buildCli.d.ts.map +1 -1
- package/dist/chunk-6XGHLLYT.js +46 -0
- package/dist/chunk-6XGHLLYT.js.map +7 -0
- package/dist/chunk-Q6677JQF.js +32609 -0
- package/dist/chunk-Q6677JQF.js.map +7 -0
- package/dist/chunk-VN6UILQW.js +1460 -0
- package/dist/chunk-VN6UILQW.js.map +7 -0
- package/dist/cockpit/Cockpit.d.ts +6 -0
- package/dist/cockpit/Cockpit.d.ts.map +1 -1
- package/dist/cockpit/Footer.d.ts +6 -4
- package/dist/cockpit/Footer.d.ts.map +1 -1
- package/dist/cockpit/TabBar.d.ts.map +1 -1
- package/dist/cockpit/hooks/useGlobalKeys.d.ts +15 -15
- package/dist/cockpit/hooks/useGlobalKeys.d.ts.map +1 -1
- package/dist/cockpit/hooks/useTerminalWidth.d.ts +12 -0
- package/dist/cockpit/hooks/useTerminalWidth.d.ts.map +1 -0
- package/dist/cockpit/panes/CommandModal.d.ts +18 -0
- package/dist/cockpit/panes/CommandModal.d.ts.map +1 -0
- package/dist/cockpit/panes/Help.d.ts.map +1 -1
- package/dist/cockpit/panes/Output.d.ts +7 -0
- package/dist/cockpit/panes/Output.d.ts.map +1 -1
- package/dist/cockpit/panes/Repos.d.ts.map +1 -1
- package/dist/cockpit/state/store.d.ts +14 -11
- package/dist/cockpit/state/store.d.ts.map +1 -1
- package/dist/cockpit/tab-state.d.ts +12 -0
- package/dist/cockpit/tab-state.d.ts.map +1 -1
- package/dist/commands/dev.d.ts.map +1 -1
- package/dist/commands/init-config-wizard.d.ts +103 -2
- package/dist/commands/init-config-wizard.d.ts.map +1 -1
- package/dist/commands/init-config.d.ts.map +1 -1
- package/dist/commands/migrate-config.d.ts +18 -0
- package/dist/commands/migrate-config.d.ts.map +1 -0
- package/dist/commands/mount.d.ts +17 -32
- package/dist/commands/mount.d.ts.map +1 -1
- package/dist/core/config.d.ts +73 -5
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/migrations.d.ts +33 -0
- package/dist/core/migrations.d.ts.map +1 -0
- package/dist/core/subprocess.d.ts +20 -0
- package/dist/core/subprocess.d.ts.map +1 -1
- package/dist/core/types.d.ts +36 -12
- package/dist/core/types.d.ts.map +1 -1
- package/dist/devtools-YXMW6JJ6.js +3720 -0
- package/dist/devtools-YXMW6JJ6.js.map +7 -0
- package/dist/docker/highlights.d.ts +14 -4
- package/dist/docker/highlights.d.ts.map +1 -1
- package/dist/docker/logs.d.ts +3 -2
- package/dist/docker/logs.d.ts.map +1 -1
- package/dist/health/builtin.d.ts.map +1 -1
- package/dist/index.d.ts +14 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +92837 -53
- package/dist/index.js.map +7 -0
- package/dist/ink.js +38 -1
- package/dist/ink.js.map +7 -0
- package/dist/mount/compose.d.ts +21 -0
- package/dist/mount/compose.d.ts.map +1 -0
- package/dist/mount/discovery.d.ts +35 -0
- package/dist/mount/discovery.d.ts.map +1 -0
- package/dist/mount/git-status.d.ts +12 -0
- package/dist/mount/git-status.d.ts.map +1 -0
- package/dist/mount/manifest.d.ts +16 -0
- package/dist/mount/manifest.d.ts.map +1 -0
- package/dist/mount/symlinks.d.ts +30 -0
- package/dist/mount/symlinks.d.ts.map +1 -0
- package/dist/mount/types.d.ts +60 -0
- package/dist/mount/types.d.ts.map +1 -0
- package/dist/react.js +35 -1
- package/dist/react.js.map +7 -0
- package/dist/runCockpit.d.ts +3 -0
- package/dist/runCockpit.d.ts.map +1 -1
- package/docs/commands.md +29 -16
- package/docs/config-reference.md +115 -11
- package/docs/getting-started.md +9 -6
- package/docs/index.md +5 -1
- package/docs/init-config.md +34 -8
- package/docs/mount.md +198 -25
- package/docs/notifications.md +14 -13
- package/docs/panes.md +36 -15
- package/docs/processes.md +42 -0
- package/package.json +93 -90
- package/dist/buildCli.js +0 -107
- package/dist/cli.js +0 -2
- package/dist/cockpit/Cockpit.js +0 -73
- package/dist/cockpit/Footer.js +0 -33
- package/dist/cockpit/TabBar.js +0 -12
- package/dist/cockpit/help/content.js +0 -22
- package/dist/cockpit/help/loader.js +0 -118
- package/dist/cockpit/help/renderer.js +0 -35
- package/dist/cockpit/help/types.js +0 -1
- package/dist/cockpit/hooks/useCockpitStore.js +0 -5
- package/dist/cockpit/hooks/useGlobalKeys.js +0 -173
- package/dist/cockpit/panes/FilterModal.js +0 -22
- package/dist/cockpit/panes/Health.js +0 -30
- package/dist/cockpit/panes/Help.js +0 -81
- package/dist/cockpit/panes/Output.js +0 -108
- package/dist/cockpit/panes/Repos.js +0 -48
- package/dist/cockpit/panes/SearchModal.js +0 -31
- package/dist/cockpit/state/store.js +0 -111
- package/dist/cockpit/tab-state.js +0 -7
- package/dist/commands/dev.js +0 -158
- package/dist/commands/doctor.js +0 -66
- package/dist/commands/init-config-wizard.js +0 -818
- package/dist/commands/init-config.js +0 -131
- package/dist/commands/mount.js +0 -150
- package/dist/core/config.js +0 -152
- package/dist/core/logger.js +0 -38
- package/dist/core/notifier.js +0 -100
- package/dist/core/paths.js +0 -18
- package/dist/core/subprocess.js +0 -82
- package/dist/core/types.js +0 -1
- package/dist/docker/highlights.js +0 -79
- package/dist/docker/logs.js +0 -172
- package/dist/docker/restart.js +0 -45
- package/dist/docker/stack-trace.js +0 -44
- package/dist/health/builtin.js +0 -144
- package/dist/health/context.js +0 -31
- package/dist/health/notify-resolver.js +0 -28
- package/dist/health/registry.js +0 -64
- package/dist/health/remediations.js +0 -41
- package/dist/health/runner.js +0 -22
- package/dist/health/scheduler.js +0 -107
- package/dist/health/types.js +0 -1
- package/dist/health/useHealth.js +0 -122
- package/dist/lint/reactive.js +0 -131
- package/dist/runCockpit.js +0 -75
- package/dist/watchers/manager.js +0 -239
- package/dist/watchers/path-mapper.js +0 -29
- package/dist/watchers/types.js +0 -9
- 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
|
-
}
|
package/dist/commands/mount.js
DELETED
|
@@ -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
|
-
}
|
package/dist/core/config.js
DELETED
|
@@ -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
|
-
}
|
package/dist/core/logger.js
DELETED
|
@@ -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
|
-
}
|
package/dist/core/notifier.js
DELETED
|
@@ -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
|
-
}
|
package/dist/core/paths.js
DELETED
|
@@ -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
|
-
}
|
package/dist/core/subprocess.js
DELETED
|
@@ -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
|
-
}
|
package/dist/core/types.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|