dev-cockpit 0.1.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.
Files changed (169) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/LICENSE +21 -0
  3. package/README.md +131 -0
  4. package/bin/dev-cockpit.mjs +20 -0
  5. package/dist/buildCli.d.ts +17 -0
  6. package/dist/buildCli.d.ts.map +1 -0
  7. package/dist/buildCli.js +107 -0
  8. package/dist/cli.d.ts +2 -0
  9. package/dist/cli.d.ts.map +1 -0
  10. package/dist/cli.js +2 -0
  11. package/dist/cockpit/Cockpit.d.ts +33 -0
  12. package/dist/cockpit/Cockpit.d.ts.map +1 -0
  13. package/dist/cockpit/Cockpit.js +73 -0
  14. package/dist/cockpit/Footer.d.ts +22 -0
  15. package/dist/cockpit/Footer.d.ts.map +1 -0
  16. package/dist/cockpit/Footer.js +33 -0
  17. package/dist/cockpit/TabBar.d.ts +3 -0
  18. package/dist/cockpit/TabBar.d.ts.map +1 -0
  19. package/dist/cockpit/TabBar.js +12 -0
  20. package/dist/cockpit/help/content.d.ts +12 -0
  21. package/dist/cockpit/help/content.d.ts.map +1 -0
  22. package/dist/cockpit/help/content.js +22 -0
  23. package/dist/cockpit/help/loader.d.ts +65 -0
  24. package/dist/cockpit/help/loader.d.ts.map +1 -0
  25. package/dist/cockpit/help/loader.js +118 -0
  26. package/dist/cockpit/help/renderer.d.ts +16 -0
  27. package/dist/cockpit/help/renderer.d.ts.map +1 -0
  28. package/dist/cockpit/help/renderer.js +35 -0
  29. package/dist/cockpit/help/types.d.ts +12 -0
  30. package/dist/cockpit/help/types.d.ts.map +1 -0
  31. package/dist/cockpit/help/types.js +1 -0
  32. package/dist/cockpit/hooks/useCockpitStore.d.ts +3 -0
  33. package/dist/cockpit/hooks/useCockpitStore.d.ts.map +1 -0
  34. package/dist/cockpit/hooks/useCockpitStore.js +5 -0
  35. package/dist/cockpit/hooks/useGlobalKeys.d.ts +56 -0
  36. package/dist/cockpit/hooks/useGlobalKeys.d.ts.map +1 -0
  37. package/dist/cockpit/hooks/useGlobalKeys.js +173 -0
  38. package/dist/cockpit/panes/FilterModal.d.ts +3 -0
  39. package/dist/cockpit/panes/FilterModal.d.ts.map +1 -0
  40. package/dist/cockpit/panes/FilterModal.js +22 -0
  41. package/dist/cockpit/panes/Health.d.ts +13 -0
  42. package/dist/cockpit/panes/Health.d.ts.map +1 -0
  43. package/dist/cockpit/panes/Health.js +30 -0
  44. package/dist/cockpit/panes/Help.d.ts +14 -0
  45. package/dist/cockpit/panes/Help.d.ts.map +1 -0
  46. package/dist/cockpit/panes/Help.js +81 -0
  47. package/dist/cockpit/panes/Output.d.ts +14 -0
  48. package/dist/cockpit/panes/Output.d.ts.map +1 -0
  49. package/dist/cockpit/panes/Output.js +108 -0
  50. package/dist/cockpit/panes/Repos.d.ts +3 -0
  51. package/dist/cockpit/panes/Repos.d.ts.map +1 -0
  52. package/dist/cockpit/panes/Repos.js +48 -0
  53. package/dist/cockpit/panes/SearchModal.d.ts +3 -0
  54. package/dist/cockpit/panes/SearchModal.d.ts.map +1 -0
  55. package/dist/cockpit/panes/SearchModal.js +31 -0
  56. package/dist/cockpit/state/store.d.ts +93 -0
  57. package/dist/cockpit/state/store.d.ts.map +1 -0
  58. package/dist/cockpit/state/store.js +111 -0
  59. package/dist/cockpit/tab-state.d.ts +4 -0
  60. package/dist/cockpit/tab-state.d.ts.map +1 -0
  61. package/dist/cockpit/tab-state.js +7 -0
  62. package/dist/commands/dev.d.ts +20 -0
  63. package/dist/commands/dev.d.ts.map +1 -0
  64. package/dist/commands/dev.js +158 -0
  65. package/dist/commands/doctor.d.ts +20 -0
  66. package/dist/commands/doctor.d.ts.map +1 -0
  67. package/dist/commands/doctor.js +66 -0
  68. package/dist/commands/init-config-wizard.d.ts +84 -0
  69. package/dist/commands/init-config-wizard.d.ts.map +1 -0
  70. package/dist/commands/init-config-wizard.js +818 -0
  71. package/dist/commands/init-config.d.ts +35 -0
  72. package/dist/commands/init-config.d.ts.map +1 -0
  73. package/dist/commands/init-config.js +131 -0
  74. package/dist/commands/mount.d.ts +48 -0
  75. package/dist/commands/mount.d.ts.map +1 -0
  76. package/dist/commands/mount.js +150 -0
  77. package/dist/core/config.d.ts +391 -0
  78. package/dist/core/config.d.ts.map +1 -0
  79. package/dist/core/config.js +152 -0
  80. package/dist/core/logger.d.ts +6 -0
  81. package/dist/core/logger.d.ts.map +1 -0
  82. package/dist/core/logger.js +38 -0
  83. package/dist/core/notifier.d.ts +23 -0
  84. package/dist/core/notifier.d.ts.map +1 -0
  85. package/dist/core/notifier.js +100 -0
  86. package/dist/core/paths.d.ts +15 -0
  87. package/dist/core/paths.d.ts.map +1 -0
  88. package/dist/core/paths.js +18 -0
  89. package/dist/core/subprocess.d.ts +20 -0
  90. package/dist/core/subprocess.d.ts.map +1 -0
  91. package/dist/core/subprocess.js +82 -0
  92. package/dist/core/types.d.ts +125 -0
  93. package/dist/core/types.d.ts.map +1 -0
  94. package/dist/core/types.js +1 -0
  95. package/dist/docker/highlights.d.ts +48 -0
  96. package/dist/docker/highlights.d.ts.map +1 -0
  97. package/dist/docker/highlights.js +79 -0
  98. package/dist/docker/logs.d.ts +84 -0
  99. package/dist/docker/logs.d.ts.map +1 -0
  100. package/dist/docker/logs.js +172 -0
  101. package/dist/docker/restart.d.ts +26 -0
  102. package/dist/docker/restart.d.ts.map +1 -0
  103. package/dist/docker/restart.js +45 -0
  104. package/dist/docker/stack-trace.d.ts +25 -0
  105. package/dist/docker/stack-trace.d.ts.map +1 -0
  106. package/dist/docker/stack-trace.js +44 -0
  107. package/dist/health/builtin.d.ts +8 -0
  108. package/dist/health/builtin.d.ts.map +1 -0
  109. package/dist/health/builtin.js +144 -0
  110. package/dist/health/context.d.ts +3 -0
  111. package/dist/health/context.d.ts.map +1 -0
  112. package/dist/health/context.js +31 -0
  113. package/dist/health/notify-resolver.d.ts +18 -0
  114. package/dist/health/notify-resolver.d.ts.map +1 -0
  115. package/dist/health/notify-resolver.js +28 -0
  116. package/dist/health/registry.d.ts +20 -0
  117. package/dist/health/registry.d.ts.map +1 -0
  118. package/dist/health/registry.js +64 -0
  119. package/dist/health/remediations.d.ts +6 -0
  120. package/dist/health/remediations.d.ts.map +1 -0
  121. package/dist/health/remediations.js +41 -0
  122. package/dist/health/runner.d.ts +4 -0
  123. package/dist/health/runner.d.ts.map +1 -0
  124. package/dist/health/runner.js +22 -0
  125. package/dist/health/scheduler.d.ts +41 -0
  126. package/dist/health/scheduler.d.ts.map +1 -0
  127. package/dist/health/scheduler.js +107 -0
  128. package/dist/health/types.d.ts +73 -0
  129. package/dist/health/types.d.ts.map +1 -0
  130. package/dist/health/types.js +1 -0
  131. package/dist/health/useHealth.d.ts +40 -0
  132. package/dist/health/useHealth.d.ts.map +1 -0
  133. package/dist/health/useHealth.js +122 -0
  134. package/dist/index.d.ts +50 -0
  135. package/dist/index.d.ts.map +1 -0
  136. package/dist/index.js +53 -0
  137. package/dist/ink.d.ts +3 -0
  138. package/dist/ink.d.ts.map +1 -0
  139. package/dist/ink.js +1 -0
  140. package/dist/lint/reactive.d.ts +38 -0
  141. package/dist/lint/reactive.d.ts.map +1 -0
  142. package/dist/lint/reactive.js +131 -0
  143. package/dist/react.d.ts +3 -0
  144. package/dist/react.d.ts.map +1 -0
  145. package/dist/react.js +1 -0
  146. package/dist/runCockpit.d.ts +34 -0
  147. package/dist/runCockpit.d.ts.map +1 -0
  148. package/dist/runCockpit.js +75 -0
  149. package/dist/watchers/manager.d.ts +63 -0
  150. package/dist/watchers/manager.d.ts.map +1 -0
  151. package/dist/watchers/manager.js +239 -0
  152. package/dist/watchers/path-mapper.d.ts +23 -0
  153. package/dist/watchers/path-mapper.d.ts.map +1 -0
  154. package/dist/watchers/path-mapper.js +29 -0
  155. package/dist/watchers/types.d.ts +22 -0
  156. package/dist/watchers/types.d.ts.map +1 -0
  157. package/dist/watchers/types.js +9 -0
  158. package/docs/commands.md +71 -0
  159. package/docs/config-reference.md +20 -0
  160. package/docs/getting-started.md +39 -0
  161. package/docs/health.md +120 -0
  162. package/docs/index.md +13 -0
  163. package/docs/init-config.md +46 -0
  164. package/docs/mount.md +55 -0
  165. package/docs/notifications.md +39 -0
  166. package/docs/panes.md +45 -0
  167. package/docs/watchers.md +27 -0
  168. package/examples/cockpit.yaml +116 -0
  169. package/package.json +91 -0
@@ -0,0 +1,35 @@
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 type { Profile } from '../core/types.js';
10
+ export interface InitConfigOptions {
11
+ /** Target directory for cockpit.yaml. Default: process.cwd(). */
12
+ cwd?: string;
13
+ /** Include an uncommented docker block. Default: false. */
14
+ withDocker?: boolean;
15
+ /** Overwrite without prompting. Default: false. */
16
+ force?: boolean;
17
+ /** Override the default app name baked into the template. */
18
+ appName?: string;
19
+ /** Run the interactive wizard instead of writing the static template. */
20
+ interactive?: boolean;
21
+ /** Active profile (passed by buildCli) — forwarded to the post-wizard doctor pass. */
22
+ profile?: Profile;
23
+ }
24
+ export declare function buildTemplate(opts: {
25
+ withDocker: boolean;
26
+ appName: string;
27
+ }): string;
28
+ export interface InitConfigResult {
29
+ /** Absolute path written. */
30
+ path: string;
31
+ /** Whether an existing file was overwritten. */
32
+ overwrote: boolean;
33
+ }
34
+ export declare function initConfigCommand(opts?: InitConfigOptions): Promise<InitConfigResult | null>;
35
+ //# sourceMappingURL=init-config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"init-config.d.ts","sourceRoot":"","sources":["../../src/commands/init-config.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAIhD,MAAM,WAAW,iBAAiB;IAChC,iEAAiE;IACjE,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,2DAA2D;IAC3D,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,mDAAmD;IACnD,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,6DAA6D;IAC7D,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,yEAAyE;IACzE,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,sFAAsF;IACtF,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE;IAAE,UAAU,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,CAoEpF;AAED,MAAM,WAAW,gBAAgB;IAC/B,6BAA6B;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,gDAAgD;IAChD,SAAS,EAAE,OAAO,CAAC;CACpB;AAED,wBAAsB,iBAAiB,CACrC,IAAI,GAAE,iBAAsB,GAC3B,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,CA4DlC"}
@@ -0,0 +1,131 @@
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
+ }
@@ -0,0 +1,48 @@
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 type { Mount, Profile } from '../core/types.js';
20
+ export interface MountCommandOptions {
21
+ config?: string;
22
+ profile?: Profile;
23
+ /** Container service name to attach the bind mounts to. */
24
+ service?: string;
25
+ }
26
+ export interface MountManifest {
27
+ appName: string;
28
+ workspaceRoot: string;
29
+ service: string;
30
+ overlayPath: string;
31
+ mounts: Mount[];
32
+ /** ISO timestamp of last apply. */
33
+ appliedAt: string;
34
+ }
35
+ /**
36
+ * Merge mount sources, keyed on containerPath. Config entries win over
37
+ * provider-discovered ones for the same container path.
38
+ */
39
+ export declare function mergeMounts(configMounts: readonly Mount[], providerMounts: readonly Mount[]): Mount[];
40
+ /**
41
+ * Render a Docker compose override YAML for the given service + mounts.
42
+ * Hand-rolled (no `yaml` writer) so the output stays stable + diff-friendly.
43
+ */
44
+ export declare function renderOverlay(service: string, mounts: readonly Mount[]): string;
45
+ export declare function mountCommand(opts?: MountCommandOptions): Promise<void>;
46
+ export declare function mountStatusCommand(opts?: MountCommandOptions): Promise<void>;
47
+ export declare function mountClearCommand(opts?: MountCommandOptions): Promise<void>;
48
+ //# sourceMappingURL=mount.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mount.d.ts","sourceRoot":"","sources":["../../src/commands/mount.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAIH,OAAO,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAIvD,MAAM,WAAW,mBAAmB;IAClC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,2DAA2D;IAC3D,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,KAAK,EAAE,CAAC;IAChB,mCAAmC;IACnC,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;GAGG;AACH,wBAAgB,WAAW,CACzB,YAAY,EAAE,SAAS,KAAK,EAAE,EAC9B,cAAc,EAAE,SAAS,KAAK,EAAE,GAC/B,KAAK,EAAE,CAKT;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,KAAK,EAAE,GAAG,MAAM,CAe/E;AAkDD,wBAAsB,YAAY,CAAC,IAAI,GAAE,mBAAwB,GAAG,OAAO,CAAC,IAAI,CAAC,CAgChF;AAED,wBAAsB,kBAAkB,CAAC,IAAI,GAAE,mBAAwB,GAAG,OAAO,CAAC,IAAI,CAAC,CAkBtF;AAED,wBAAsB,iBAAiB,CAAC,IAAI,GAAE,mBAAwB,GAAG,OAAO,CAAC,IAAI,CAAC,CAkBrF"}
@@ -0,0 +1,150 @@
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
+ }