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.
- package/CHANGELOG.md +36 -0
- package/LICENSE +21 -0
- package/README.md +131 -0
- package/bin/dev-cockpit.mjs +20 -0
- package/dist/buildCli.d.ts +17 -0
- package/dist/buildCli.d.ts.map +1 -0
- package/dist/buildCli.js +107 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +2 -0
- package/dist/cockpit/Cockpit.d.ts +33 -0
- package/dist/cockpit/Cockpit.d.ts.map +1 -0
- package/dist/cockpit/Cockpit.js +73 -0
- package/dist/cockpit/Footer.d.ts +22 -0
- package/dist/cockpit/Footer.d.ts.map +1 -0
- package/dist/cockpit/Footer.js +33 -0
- package/dist/cockpit/TabBar.d.ts +3 -0
- package/dist/cockpit/TabBar.d.ts.map +1 -0
- package/dist/cockpit/TabBar.js +12 -0
- package/dist/cockpit/help/content.d.ts +12 -0
- package/dist/cockpit/help/content.d.ts.map +1 -0
- package/dist/cockpit/help/content.js +22 -0
- package/dist/cockpit/help/loader.d.ts +65 -0
- package/dist/cockpit/help/loader.d.ts.map +1 -0
- package/dist/cockpit/help/loader.js +118 -0
- package/dist/cockpit/help/renderer.d.ts +16 -0
- package/dist/cockpit/help/renderer.d.ts.map +1 -0
- package/dist/cockpit/help/renderer.js +35 -0
- package/dist/cockpit/help/types.d.ts +12 -0
- package/dist/cockpit/help/types.d.ts.map +1 -0
- package/dist/cockpit/help/types.js +1 -0
- package/dist/cockpit/hooks/useCockpitStore.d.ts +3 -0
- package/dist/cockpit/hooks/useCockpitStore.d.ts.map +1 -0
- package/dist/cockpit/hooks/useCockpitStore.js +5 -0
- package/dist/cockpit/hooks/useGlobalKeys.d.ts +56 -0
- package/dist/cockpit/hooks/useGlobalKeys.d.ts.map +1 -0
- package/dist/cockpit/hooks/useGlobalKeys.js +173 -0
- package/dist/cockpit/panes/FilterModal.d.ts +3 -0
- package/dist/cockpit/panes/FilterModal.d.ts.map +1 -0
- package/dist/cockpit/panes/FilterModal.js +22 -0
- package/dist/cockpit/panes/Health.d.ts +13 -0
- package/dist/cockpit/panes/Health.d.ts.map +1 -0
- package/dist/cockpit/panes/Health.js +30 -0
- package/dist/cockpit/panes/Help.d.ts +14 -0
- package/dist/cockpit/panes/Help.d.ts.map +1 -0
- package/dist/cockpit/panes/Help.js +81 -0
- package/dist/cockpit/panes/Output.d.ts +14 -0
- package/dist/cockpit/panes/Output.d.ts.map +1 -0
- package/dist/cockpit/panes/Output.js +108 -0
- package/dist/cockpit/panes/Repos.d.ts +3 -0
- package/dist/cockpit/panes/Repos.d.ts.map +1 -0
- package/dist/cockpit/panes/Repos.js +48 -0
- package/dist/cockpit/panes/SearchModal.d.ts +3 -0
- package/dist/cockpit/panes/SearchModal.d.ts.map +1 -0
- package/dist/cockpit/panes/SearchModal.js +31 -0
- package/dist/cockpit/state/store.d.ts +93 -0
- package/dist/cockpit/state/store.d.ts.map +1 -0
- package/dist/cockpit/state/store.js +111 -0
- package/dist/cockpit/tab-state.d.ts +4 -0
- package/dist/cockpit/tab-state.d.ts.map +1 -0
- package/dist/cockpit/tab-state.js +7 -0
- package/dist/commands/dev.d.ts +20 -0
- package/dist/commands/dev.d.ts.map +1 -0
- package/dist/commands/dev.js +158 -0
- package/dist/commands/doctor.d.ts +20 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +66 -0
- package/dist/commands/init-config-wizard.d.ts +84 -0
- package/dist/commands/init-config-wizard.d.ts.map +1 -0
- package/dist/commands/init-config-wizard.js +818 -0
- package/dist/commands/init-config.d.ts +35 -0
- package/dist/commands/init-config.d.ts.map +1 -0
- package/dist/commands/init-config.js +131 -0
- package/dist/commands/mount.d.ts +48 -0
- package/dist/commands/mount.d.ts.map +1 -0
- package/dist/commands/mount.js +150 -0
- package/dist/core/config.d.ts +391 -0
- package/dist/core/config.d.ts.map +1 -0
- package/dist/core/config.js +152 -0
- package/dist/core/logger.d.ts +6 -0
- package/dist/core/logger.d.ts.map +1 -0
- package/dist/core/logger.js +38 -0
- package/dist/core/notifier.d.ts +23 -0
- package/dist/core/notifier.d.ts.map +1 -0
- package/dist/core/notifier.js +100 -0
- package/dist/core/paths.d.ts +15 -0
- package/dist/core/paths.d.ts.map +1 -0
- package/dist/core/paths.js +18 -0
- package/dist/core/subprocess.d.ts +20 -0
- package/dist/core/subprocess.d.ts.map +1 -0
- package/dist/core/subprocess.js +82 -0
- package/dist/core/types.d.ts +125 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +1 -0
- package/dist/docker/highlights.d.ts +48 -0
- package/dist/docker/highlights.d.ts.map +1 -0
- package/dist/docker/highlights.js +79 -0
- package/dist/docker/logs.d.ts +84 -0
- package/dist/docker/logs.d.ts.map +1 -0
- package/dist/docker/logs.js +172 -0
- package/dist/docker/restart.d.ts +26 -0
- package/dist/docker/restart.d.ts.map +1 -0
- package/dist/docker/restart.js +45 -0
- package/dist/docker/stack-trace.d.ts +25 -0
- package/dist/docker/stack-trace.d.ts.map +1 -0
- package/dist/docker/stack-trace.js +44 -0
- package/dist/health/builtin.d.ts +8 -0
- package/dist/health/builtin.d.ts.map +1 -0
- package/dist/health/builtin.js +144 -0
- package/dist/health/context.d.ts +3 -0
- package/dist/health/context.d.ts.map +1 -0
- package/dist/health/context.js +31 -0
- package/dist/health/notify-resolver.d.ts +18 -0
- package/dist/health/notify-resolver.d.ts.map +1 -0
- package/dist/health/notify-resolver.js +28 -0
- package/dist/health/registry.d.ts +20 -0
- package/dist/health/registry.d.ts.map +1 -0
- package/dist/health/registry.js +64 -0
- package/dist/health/remediations.d.ts +6 -0
- package/dist/health/remediations.d.ts.map +1 -0
- package/dist/health/remediations.js +41 -0
- package/dist/health/runner.d.ts +4 -0
- package/dist/health/runner.d.ts.map +1 -0
- package/dist/health/runner.js +22 -0
- package/dist/health/scheduler.d.ts +41 -0
- package/dist/health/scheduler.d.ts.map +1 -0
- package/dist/health/scheduler.js +107 -0
- package/dist/health/types.d.ts +73 -0
- package/dist/health/types.d.ts.map +1 -0
- package/dist/health/types.js +1 -0
- package/dist/health/useHealth.d.ts +40 -0
- package/dist/health/useHealth.d.ts.map +1 -0
- package/dist/health/useHealth.js +122 -0
- package/dist/index.d.ts +50 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +53 -0
- package/dist/ink.d.ts +3 -0
- package/dist/ink.d.ts.map +1 -0
- package/dist/ink.js +1 -0
- package/dist/lint/reactive.d.ts +38 -0
- package/dist/lint/reactive.d.ts.map +1 -0
- package/dist/lint/reactive.js +131 -0
- package/dist/react.d.ts +3 -0
- package/dist/react.d.ts.map +1 -0
- package/dist/react.js +1 -0
- package/dist/runCockpit.d.ts +34 -0
- package/dist/runCockpit.d.ts.map +1 -0
- package/dist/runCockpit.js +75 -0
- package/dist/watchers/manager.d.ts +63 -0
- package/dist/watchers/manager.d.ts.map +1 -0
- package/dist/watchers/manager.js +239 -0
- package/dist/watchers/path-mapper.d.ts +23 -0
- package/dist/watchers/path-mapper.d.ts.map +1 -0
- package/dist/watchers/path-mapper.js +29 -0
- package/dist/watchers/types.d.ts +22 -0
- package/dist/watchers/types.d.ts.map +1 -0
- package/dist/watchers/types.js +9 -0
- package/docs/commands.md +71 -0
- package/docs/config-reference.md +20 -0
- package/docs/getting-started.md +39 -0
- package/docs/health.md +120 -0
- package/docs/index.md +13 -0
- package/docs/init-config.md +46 -0
- package/docs/mount.md +55 -0
- package/docs/notifications.md +39 -0
- package/docs/panes.md +45 -0
- package/docs/watchers.md +27 -0
- package/examples/cockpit.yaml +116 -0
- package/package.json +91 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reactive lint engine.
|
|
3
|
+
*
|
|
4
|
+
* Pure command-selection function: given a file extension and a repo's lint
|
|
5
|
+
* config, returns the lint command (or undefined if none configured).
|
|
6
|
+
*
|
|
7
|
+
* The debounced runner lives here too, backed by spawnStream. Debounce is
|
|
8
|
+
* per-file (keyed by absolute path), 300ms.
|
|
9
|
+
*
|
|
10
|
+
* Lint result is reported via `onLintResult` — this module never mutates
|
|
11
|
+
* watcher status. Watcher lifecycle (idle/running/failing) is exclusively
|
|
12
|
+
* owned by WatcherManager.
|
|
13
|
+
*/
|
|
14
|
+
import type { NormalizedRepoConfig } from '../watchers/types.js';
|
|
15
|
+
import type { OutputLine } from '../cockpit/state/store.js';
|
|
16
|
+
import type { LintStatus } from '../cockpit/state/store.js';
|
|
17
|
+
import { spawnStream } from '../core/subprocess.js';
|
|
18
|
+
/** Returns the lint command for a file, or undefined if none configured. Pure. */
|
|
19
|
+
export declare function selectLintCommand(filePath: string, lintConfig: NormalizedRepoConfig['lint']): string | undefined;
|
|
20
|
+
export interface LintRunnerOptions {
|
|
21
|
+
repoName: string;
|
|
22
|
+
repoRootDir: string;
|
|
23
|
+
lintConfig: NormalizedRepoConfig['lint'];
|
|
24
|
+
appendOutput: (line: OutputLine) => void;
|
|
25
|
+
onLintResult: (result: LintStatus) => void;
|
|
26
|
+
spawn?: typeof spawnStream;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Schedule a lint run for `filePath`, debounced 300ms per file. Files with
|
|
30
|
+
* no matching lint command are silently skipped.
|
|
31
|
+
*/
|
|
32
|
+
export declare function scheduleLint(filePath: string, opts: LintRunnerOptions): void;
|
|
33
|
+
/**
|
|
34
|
+
* Run all configured lint commands for a repo (js, css, php) in parallel.
|
|
35
|
+
* Aggregates pass/fail — any failing kind → overall 'fail', all pass → 'pass'.
|
|
36
|
+
*/
|
|
37
|
+
export declare function runAllLints(opts: LintRunnerOptions): Promise<void>;
|
|
38
|
+
//# sourceMappingURL=reactive.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reactive.d.ts","sourceRoot":"","sources":["../../src/lint/reactive.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAGH,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AACjE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AAC5D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AAC5D,OAAO,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAgBpD,kFAAkF;AAClF,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,oBAAoB,CAAC,MAAM,CAAC,GACvC,MAAM,GAAG,SAAS,CAKpB;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,oBAAoB,CAAC,MAAM,CAAC,CAAC;IACzC,YAAY,EAAE,CAAC,IAAI,EAAE,UAAU,KAAK,IAAI,CAAC;IACzC,YAAY,EAAE,CAAC,MAAM,EAAE,UAAU,KAAK,IAAI,CAAC;IAC3C,KAAK,CAAC,EAAE,OAAO,WAAW,CAAC;CAC5B;AAKD;;;GAGG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,iBAAiB,GAAG,IAAI,CAa5E;AAED;;;GAGG;AACH,wBAAsB,WAAW,CAAC,IAAI,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CAgDxE"}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reactive lint engine.
|
|
3
|
+
*
|
|
4
|
+
* Pure command-selection function: given a file extension and a repo's lint
|
|
5
|
+
* config, returns the lint command (or undefined if none configured).
|
|
6
|
+
*
|
|
7
|
+
* The debounced runner lives here too, backed by spawnStream. Debounce is
|
|
8
|
+
* per-file (keyed by absolute path), 300ms.
|
|
9
|
+
*
|
|
10
|
+
* Lint result is reported via `onLintResult` — this module never mutates
|
|
11
|
+
* watcher status. Watcher lifecycle (idle/running/failing) is exclusively
|
|
12
|
+
* owned by WatcherManager.
|
|
13
|
+
*/
|
|
14
|
+
import path from 'node:path';
|
|
15
|
+
import { spawnStream } from '../core/subprocess.js';
|
|
16
|
+
const EXT_TO_LINT_KEY = {
|
|
17
|
+
'.js': 'js',
|
|
18
|
+
'.jsx': 'js',
|
|
19
|
+
'.ts': 'js',
|
|
20
|
+
'.tsx': 'js',
|
|
21
|
+
'.mjs': 'js',
|
|
22
|
+
'.cjs': 'js',
|
|
23
|
+
'.css': 'css',
|
|
24
|
+
'.scss': 'css',
|
|
25
|
+
'.sass': 'css',
|
|
26
|
+
'.less': 'css',
|
|
27
|
+
'.php': 'php',
|
|
28
|
+
};
|
|
29
|
+
/** Returns the lint command for a file, or undefined if none configured. Pure. */
|
|
30
|
+
export function selectLintCommand(filePath, lintConfig) {
|
|
31
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
32
|
+
const key = EXT_TO_LINT_KEY[ext];
|
|
33
|
+
if (!key)
|
|
34
|
+
return undefined;
|
|
35
|
+
return lintConfig[key];
|
|
36
|
+
}
|
|
37
|
+
const debounceTimers = new Map();
|
|
38
|
+
const DEBOUNCE_MS = 300;
|
|
39
|
+
/**
|
|
40
|
+
* Schedule a lint run for `filePath`, debounced 300ms per file. Files with
|
|
41
|
+
* no matching lint command are silently skipped.
|
|
42
|
+
*/
|
|
43
|
+
export function scheduleLint(filePath, opts) {
|
|
44
|
+
const cmd = selectLintCommand(filePath, opts.lintConfig);
|
|
45
|
+
if (!cmd)
|
|
46
|
+
return;
|
|
47
|
+
const existing = debounceTimers.get(filePath);
|
|
48
|
+
if (existing !== undefined)
|
|
49
|
+
clearTimeout(existing);
|
|
50
|
+
const timer = setTimeout(() => {
|
|
51
|
+
debounceTimers.delete(filePath);
|
|
52
|
+
void runLintCmd(filePath, cmd, opts);
|
|
53
|
+
}, DEBOUNCE_MS);
|
|
54
|
+
debounceTimers.set(filePath, timer);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Run all configured lint commands for a repo (js, css, php) in parallel.
|
|
58
|
+
* Aggregates pass/fail — any failing kind → overall 'fail', all pass → 'pass'.
|
|
59
|
+
*/
|
|
60
|
+
export async function runAllLints(opts) {
|
|
61
|
+
const { repoName, repoRootDir, lintConfig, appendOutput, onLintResult } = opts;
|
|
62
|
+
const spawnFn = opts.spawn ?? spawnStream;
|
|
63
|
+
const kinds = ['js', 'css', 'php'];
|
|
64
|
+
const toRun = [];
|
|
65
|
+
for (const kind of kinds) {
|
|
66
|
+
const cmd = lintConfig[kind];
|
|
67
|
+
if (cmd)
|
|
68
|
+
toRun.push({ kind, cmd });
|
|
69
|
+
}
|
|
70
|
+
if (toRun.length === 0) {
|
|
71
|
+
appendOutput({
|
|
72
|
+
ts: Date.now(),
|
|
73
|
+
source: repoName,
|
|
74
|
+
severity: 'info',
|
|
75
|
+
text: 'lint: no linters configured for this repo',
|
|
76
|
+
});
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const results = await Promise.all(toRun.map(async ({ kind, cmd }) => {
|
|
80
|
+
const [exe, ...args] = cmd.split(/\s+/);
|
|
81
|
+
if (!exe)
|
|
82
|
+
return { kind, passed: false };
|
|
83
|
+
const handle = spawnFn(exe, args, {
|
|
84
|
+
cwd: repoRootDir,
|
|
85
|
+
onStdout: (line) => appendOutput({ ts: Date.now(), source: repoName, severity: 'info', text: line }),
|
|
86
|
+
onStderr: (line) => appendOutput({ ts: Date.now(), source: repoName, severity: 'warn', text: line }),
|
|
87
|
+
});
|
|
88
|
+
const code = await handle.exitCode;
|
|
89
|
+
const passed = code === 0;
|
|
90
|
+
appendOutput({
|
|
91
|
+
ts: Date.now(),
|
|
92
|
+
source: repoName,
|
|
93
|
+
severity: passed ? 'info' : 'error',
|
|
94
|
+
text: `lint [${kind}]: ${passed ? 'passed' : `FAILED (exit ${code})`}`,
|
|
95
|
+
});
|
|
96
|
+
return { kind, passed };
|
|
97
|
+
}));
|
|
98
|
+
const allPassed = results.every((r) => r.passed);
|
|
99
|
+
onLintResult(allPassed ? 'pass' : 'fail');
|
|
100
|
+
}
|
|
101
|
+
async function runLintCmd(filePath, cmd, opts) {
|
|
102
|
+
const { repoName, repoRootDir, appendOutput, onLintResult } = opts;
|
|
103
|
+
const spawnFn = opts.spawn ?? spawnStream;
|
|
104
|
+
const [exe, ...args] = cmd.split(/\s+/);
|
|
105
|
+
if (!exe)
|
|
106
|
+
return;
|
|
107
|
+
const handle = spawnFn(exe, args, {
|
|
108
|
+
cwd: repoRootDir,
|
|
109
|
+
onStdout: (line) => appendOutput({ ts: Date.now(), source: repoName, severity: 'info', text: line }),
|
|
110
|
+
onStderr: (line) => appendOutput({ ts: Date.now(), source: repoName, severity: 'warn', text: line }),
|
|
111
|
+
});
|
|
112
|
+
const code = await handle.exitCode;
|
|
113
|
+
if (code === 0) {
|
|
114
|
+
appendOutput({
|
|
115
|
+
ts: Date.now(),
|
|
116
|
+
source: repoName,
|
|
117
|
+
severity: 'info',
|
|
118
|
+
text: `lint passed for ${path.basename(filePath)}`,
|
|
119
|
+
});
|
|
120
|
+
onLintResult('pass');
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
appendOutput({
|
|
124
|
+
ts: Date.now(),
|
|
125
|
+
source: repoName,
|
|
126
|
+
severity: 'error',
|
|
127
|
+
text: `lint FAILED for ${path.basename(filePath)} (exit ${code})`,
|
|
128
|
+
});
|
|
129
|
+
onLintResult('fail');
|
|
130
|
+
}
|
|
131
|
+
}
|
package/dist/react.d.ts
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { useState, useEffect, useMemo, useCallback, useRef, useReducer, useContext, useLayoutEffect, createContext, Fragment, } from 'react';
|
|
2
|
+
export type { ReactNode, ReactElement, FC, PropsWithChildren, Dispatch, SetStateAction, MutableRefObject, RefObject, } from 'react';
|
|
3
|
+
//# sourceMappingURL=react.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"react.d.ts","sourceRoot":"","sources":["../src/react.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,QAAQ,EACR,SAAS,EACT,OAAO,EACP,WAAW,EACX,MAAM,EACN,UAAU,EACV,UAAU,EACV,eAAe,EACf,aAAa,EACb,QAAQ,GACT,MAAM,OAAO,CAAC;AACf,YAAY,EACV,SAAS,EACT,YAAY,EACZ,EAAE,EACF,iBAAiB,EACjB,QAAQ,EACR,cAAc,EACd,gBAAgB,EAChB,SAAS,GACV,MAAM,OAAO,CAAC"}
|
package/dist/react.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { useState, useEffect, useMemo, useCallback, useRef, useReducer, useContext, useLayoutEffect, createContext, Fragment, } from 'react';
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* runCockpit — boot the cockpit TUI.
|
|
3
|
+
*
|
|
4
|
+
* Responsibilities (Phase 3, shell-only):
|
|
5
|
+
* - Switch the terminal into the alternate-screen buffer (so the cockpit fills
|
|
6
|
+
* the window without scrolling shell history out of view; restored on exit
|
|
7
|
+
* or uncaughtException).
|
|
8
|
+
* - Seed `cockpitStore.helpConfig` from `profile.helpSources` + `profile.defaultHelpPage`.
|
|
9
|
+
* - Render `<Cockpit>` with the caller-supplied handlers via `ink.render`.
|
|
10
|
+
* - Return `waitUntilExit` and `restore` for the caller to await + clean up.
|
|
11
|
+
*
|
|
12
|
+
* Domain-specific boot (loading config, building watchers, scheduling health
|
|
13
|
+
* checks, docker tailers) lives in the consumer's `dev` command; this entry
|
|
14
|
+
* stays narrow and reusable.
|
|
15
|
+
*/
|
|
16
|
+
import { type Instance } from 'ink';
|
|
17
|
+
import { type CockpitProps } from './cockpit/Cockpit.js';
|
|
18
|
+
import type { Profile } from './core/types.js';
|
|
19
|
+
export interface RunCockpitOptions extends CockpitProps {
|
|
20
|
+
/** Optional profile — used to seed helpConfig (sources + defaultPage). */
|
|
21
|
+
profile?: Profile;
|
|
22
|
+
/** Skip the alternate-screen-buffer dance (e.g. for tests). Default: false. */
|
|
23
|
+
noAltScreen?: boolean;
|
|
24
|
+
}
|
|
25
|
+
export interface RunCockpitHandle {
|
|
26
|
+
/** Promise that resolves when the user quits (q or Ctrl-C with exitOnCtrlC). */
|
|
27
|
+
waitUntilExit: () => Promise<void>;
|
|
28
|
+
/** Manually restore the screen buffer. Called automatically on exit. */
|
|
29
|
+
restore: () => void;
|
|
30
|
+
/** Underlying Ink render instance. */
|
|
31
|
+
ink: Instance;
|
|
32
|
+
}
|
|
33
|
+
export declare function runCockpit(opts?: RunCockpitOptions): RunCockpitHandle;
|
|
34
|
+
//# sourceMappingURL=runCockpit.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runCockpit.d.ts","sourceRoot":"","sources":["../src/runCockpit.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAGH,OAAO,EAAU,KAAK,QAAQ,EAAE,MAAM,KAAK,CAAC;AAC5C,OAAO,EAAW,KAAK,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAElE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAG/C,MAAM,WAAW,iBAAkB,SAAQ,YAAY;IACrD,0EAA0E;IAC1E,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,+EAA+E;IAC/E,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAED,MAAM,WAAW,gBAAgB;IAC/B,gFAAgF;IAChF,aAAa,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IACnC,wEAAwE;IACxE,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,sCAAsC;IACtC,GAAG,EAAE,QAAQ,CAAC;CACf;AAKD,wBAAgB,UAAU,CAAC,IAAI,GAAE,iBAAsB,GAAG,gBAAgB,CAyDzE"}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* runCockpit — boot the cockpit TUI.
|
|
3
|
+
*
|
|
4
|
+
* Responsibilities (Phase 3, shell-only):
|
|
5
|
+
* - Switch the terminal into the alternate-screen buffer (so the cockpit fills
|
|
6
|
+
* the window without scrolling shell history out of view; restored on exit
|
|
7
|
+
* or uncaughtException).
|
|
8
|
+
* - Seed `cockpitStore.helpConfig` from `profile.helpSources` + `profile.defaultHelpPage`.
|
|
9
|
+
* - Render `<Cockpit>` with the caller-supplied handlers via `ink.render`.
|
|
10
|
+
* - Return `waitUntilExit` and `restore` for the caller to await + clean up.
|
|
11
|
+
*
|
|
12
|
+
* Domain-specific boot (loading config, building watchers, scheduling health
|
|
13
|
+
* checks, docker tailers) lives in the consumer's `dev` command; this entry
|
|
14
|
+
* stays narrow and reusable.
|
|
15
|
+
*/
|
|
16
|
+
import React from 'react';
|
|
17
|
+
import { render } from 'ink';
|
|
18
|
+
import { Cockpit } from './cockpit/Cockpit.js';
|
|
19
|
+
import { cockpitStore } from './cockpit/state/store.js';
|
|
20
|
+
const ENTER_ALT_SCREEN = '\x1b[?1049h\x1b[H';
|
|
21
|
+
const EXIT_ALT_SCREEN = '\x1b[?1049l';
|
|
22
|
+
export function runCockpit(opts = {}) {
|
|
23
|
+
const { profile, noAltScreen, ...cockpitProps } = opts;
|
|
24
|
+
// Seed helpConfig from profile.
|
|
25
|
+
if (profile) {
|
|
26
|
+
const sources = (profile.helpSources ?? []).map((src) => ({
|
|
27
|
+
page: src.path == null && src.content != null
|
|
28
|
+
? {
|
|
29
|
+
slug: src.id,
|
|
30
|
+
title: src.title ?? src.id,
|
|
31
|
+
path: '',
|
|
32
|
+
body: src.content,
|
|
33
|
+
}
|
|
34
|
+
: undefined,
|
|
35
|
+
path: src.path,
|
|
36
|
+
id: src.id,
|
|
37
|
+
omit: src.omit,
|
|
38
|
+
}));
|
|
39
|
+
cockpitStore.getState().setHelpConfig({
|
|
40
|
+
sources,
|
|
41
|
+
defaultPage: profile.defaultHelpPage,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
// Alternate-screen buffer.
|
|
45
|
+
let restored = false;
|
|
46
|
+
const restore = () => {
|
|
47
|
+
if (restored)
|
|
48
|
+
return;
|
|
49
|
+
restored = true;
|
|
50
|
+
if (!noAltScreen) {
|
|
51
|
+
process.stdout.write(EXIT_ALT_SCREEN);
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
if (!noAltScreen) {
|
|
55
|
+
process.stdout.write(ENTER_ALT_SCREEN);
|
|
56
|
+
process.once('exit', restore);
|
|
57
|
+
process.once('uncaughtException', (err) => {
|
|
58
|
+
restore();
|
|
59
|
+
throw err;
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
const ink = render(React.createElement(Cockpit, cockpitProps), { exitOnCtrlC: true });
|
|
63
|
+
return {
|
|
64
|
+
waitUntilExit: async () => {
|
|
65
|
+
try {
|
|
66
|
+
await ink.waitUntilExit();
|
|
67
|
+
}
|
|
68
|
+
finally {
|
|
69
|
+
restore();
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
restore,
|
|
73
|
+
ink,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WatcherManager — orchestrates chokidar filesystem watchers and per-repo
|
|
3
|
+
* watcher subprocesses.
|
|
4
|
+
*
|
|
5
|
+
* Responsibilities:
|
|
6
|
+
* - Subscribe to chokidar for the union of all repo discover globs.
|
|
7
|
+
* - On file change in an idle repo → spawn the repo's watch command.
|
|
8
|
+
* - On watcher exit (code 0) → mark repo idle; non-zero → failing.
|
|
9
|
+
* - Trigger reactive lint via scheduleLint on each change.
|
|
10
|
+
*
|
|
11
|
+
* Public API: start(), stop(), toggle(repoName), rebuild(repoName), lint(repoName).
|
|
12
|
+
*
|
|
13
|
+
* All subprocess work goes through `core/subprocess.spawnStream` (DRY).
|
|
14
|
+
*/
|
|
15
|
+
import type { NormalizedRepoConfig } from './types.js';
|
|
16
|
+
import type { LintStatus, OutputLine, RepoStatus } from '../cockpit/state/store.js';
|
|
17
|
+
import { spawnStream } from '../core/subprocess.js';
|
|
18
|
+
export interface WatcherManagerDeps {
|
|
19
|
+
appendOutput: (line: OutputLine) => void;
|
|
20
|
+
setRepoStatus: (key: string, status: RepoStatus) => void;
|
|
21
|
+
/** Lint result — never mutates watcher status. */
|
|
22
|
+
setLintStatus: (key: string, lintStatus: LintStatus) => void;
|
|
23
|
+
/** Inject a custom spawner (tests). Defaults to spawnStream. */
|
|
24
|
+
spawn?: typeof spawnStream;
|
|
25
|
+
/**
|
|
26
|
+
* Optional build-outcome notification hook. Called when a watcher exits
|
|
27
|
+
* with a state transition (clean→fail or fail→clean). Pass `emitEvent`
|
|
28
|
+
* from `core/notifier` here to wire OS notifications.
|
|
29
|
+
*/
|
|
30
|
+
onBuildOutcome?: (repoName: string, outcome: 'build-failed' | 'build-recovered') => void;
|
|
31
|
+
}
|
|
32
|
+
export interface ManagedRepo {
|
|
33
|
+
config: NormalizedRepoConfig;
|
|
34
|
+
/** Absolute path to the repo's root directory. */
|
|
35
|
+
rootDir: string;
|
|
36
|
+
}
|
|
37
|
+
type FsEventListener = (filePath: string) => void;
|
|
38
|
+
export declare class WatcherManager {
|
|
39
|
+
private readonly repos;
|
|
40
|
+
private readonly prefixEntries;
|
|
41
|
+
private readonly handles;
|
|
42
|
+
private fsWatcher;
|
|
43
|
+
private readonly deps;
|
|
44
|
+
private readonly buildOutcomes;
|
|
45
|
+
private readonly fsEventListeners;
|
|
46
|
+
constructor(repos: ManagedRepo[], deps: WatcherManagerDeps);
|
|
47
|
+
start(): void;
|
|
48
|
+
stop(): Promise<void>;
|
|
49
|
+
toggle(repoName: string): void;
|
|
50
|
+
rebuild(repoName: string): void;
|
|
51
|
+
lint(repoName: string): void;
|
|
52
|
+
/**
|
|
53
|
+
* Subscribe to filesystem-event notifications. The listener is called for
|
|
54
|
+
* every detected change before the watcher-launch logic. Returns an
|
|
55
|
+
* unsubscribe function.
|
|
56
|
+
*/
|
|
57
|
+
subscribeFsEvents(listener: FsEventListener): () => void;
|
|
58
|
+
private handleChange;
|
|
59
|
+
private spawnWatcher;
|
|
60
|
+
private killHandle;
|
|
61
|
+
}
|
|
62
|
+
export {};
|
|
63
|
+
//# sourceMappingURL=manager.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manager.d.ts","sourceRoot":"","sources":["../../src/watchers/manager.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAKH,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC;AACvD,OAAO,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AACpF,OAAO,EAAE,WAAW,EAAsB,MAAM,uBAAuB,CAAC;AAIxE,MAAM,WAAW,kBAAkB;IACjC,YAAY,EAAE,CAAC,IAAI,EAAE,UAAU,KAAK,IAAI,CAAC;IACzC,aAAa,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,KAAK,IAAI,CAAC;IACzD,kDAAkD;IAClD,aAAa,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,UAAU,EAAE,UAAU,KAAK,IAAI,CAAC;IAC7D,gEAAgE;IAChE,KAAK,CAAC,EAAE,OAAO,WAAW,CAAC;IAC3B;;;;OAIG;IACH,cAAc,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,cAAc,GAAG,iBAAiB,KAAK,IAAI,CAAC;CAC1F;AAED,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,oBAAoB,CAAC;IAC7B,kDAAkD;IAClD,OAAO,EAAE,MAAM,CAAC;CACjB;AAyBD,KAAK,eAAe,GAAG,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;AAElD,qBAAa,cAAc;IACzB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAuC;IAC7D,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAyB;IACvD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAyC;IACjE,OAAO,CAAC,SAAS,CAA0B;IAC3C,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAiB;IACtC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAA8C;IAC5E,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAmC;gBAExD,KAAK,EAAE,WAAW,EAAE,EAAE,IAAI,EAAE,kBAAkB;IAe1D,KAAK,IAAI,IAAI;IAgCP,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAiB3B,MAAM,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI;IAU9B,OAAO,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI;IAK/B,IAAI,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI;IAc5B;;;;OAIG;IACH,iBAAiB,CAAC,QAAQ,EAAE,eAAe,GAAG,MAAM,IAAI;IASxD,OAAO,CAAC,YAAY;YAyBN,YAAY;IA8E1B,OAAO,CAAC,UAAU;CAOnB"}
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WatcherManager — orchestrates chokidar filesystem watchers and per-repo
|
|
3
|
+
* watcher subprocesses.
|
|
4
|
+
*
|
|
5
|
+
* Responsibilities:
|
|
6
|
+
* - Subscribe to chokidar for the union of all repo discover globs.
|
|
7
|
+
* - On file change in an idle repo → spawn the repo's watch command.
|
|
8
|
+
* - On watcher exit (code 0) → mark repo idle; non-zero → failing.
|
|
9
|
+
* - Trigger reactive lint via scheduleLint on each change.
|
|
10
|
+
*
|
|
11
|
+
* Public API: start(), stop(), toggle(repoName), rebuild(repoName), lint(repoName).
|
|
12
|
+
*
|
|
13
|
+
* All subprocess work goes through `core/subprocess.spawnStream` (DRY).
|
|
14
|
+
*/
|
|
15
|
+
import fs from 'node:fs';
|
|
16
|
+
import path from 'node:path';
|
|
17
|
+
import chokidar from 'chokidar';
|
|
18
|
+
import { spawnStream } from '../core/subprocess.js';
|
|
19
|
+
import { mapPathToRepo } from './path-mapper.js';
|
|
20
|
+
import { scheduleLint, runAllLints } from '../lint/reactive.js';
|
|
21
|
+
/**
|
|
22
|
+
* Detect WSL2 by reading /proc/version (Linux-only).
|
|
23
|
+
* Native Linux + macOS don't need polling. Set DEV_COCKPIT_CHOKIDAR_POLLING=1
|
|
24
|
+
* to force polling as an escape hatch.
|
|
25
|
+
*/
|
|
26
|
+
function detectUsePolling() {
|
|
27
|
+
if (process.env['DEV_COCKPIT_CHOKIDAR_POLLING'] === '1')
|
|
28
|
+
return true;
|
|
29
|
+
try {
|
|
30
|
+
const version = fs.readFileSync('/proc/version', 'utf8');
|
|
31
|
+
return /microsoft|wsl/i.test(version);
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export class WatcherManager {
|
|
38
|
+
repos = new Map();
|
|
39
|
+
prefixEntries = [];
|
|
40
|
+
handles = new Map();
|
|
41
|
+
fsWatcher = null;
|
|
42
|
+
deps;
|
|
43
|
+
buildOutcomes = new Map();
|
|
44
|
+
fsEventListeners = new Set();
|
|
45
|
+
constructor(repos, deps) {
|
|
46
|
+
this.deps = {
|
|
47
|
+
appendOutput: deps.appendOutput,
|
|
48
|
+
setRepoStatus: deps.setRepoStatus,
|
|
49
|
+
setLintStatus: deps.setLintStatus,
|
|
50
|
+
spawn: deps.spawn ?? spawnStream,
|
|
51
|
+
onBuildOutcome: deps.onBuildOutcome ?? null,
|
|
52
|
+
};
|
|
53
|
+
for (const repo of repos) {
|
|
54
|
+
this.repos.set(repo.config.name, repo);
|
|
55
|
+
this.prefixEntries.push({ name: repo.config.name, rootDir: repo.rootDir });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
start() {
|
|
59
|
+
if (this.fsWatcher)
|
|
60
|
+
return;
|
|
61
|
+
const globs = [];
|
|
62
|
+
for (const [, managed] of this.repos) {
|
|
63
|
+
for (const pattern of managed.config.discover) {
|
|
64
|
+
globs.push(path.join(managed.rootDir, pattern));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (globs.length === 0)
|
|
68
|
+
return;
|
|
69
|
+
const usePolling = detectUsePolling();
|
|
70
|
+
this.fsWatcher = chokidar.watch(globs, {
|
|
71
|
+
usePolling,
|
|
72
|
+
ignoreInitial: true,
|
|
73
|
+
ignored: /(^|[/\\])\../,
|
|
74
|
+
});
|
|
75
|
+
this.fsWatcher.on('change', (filePath) => this.handleChange(filePath));
|
|
76
|
+
this.fsWatcher.on('add', (filePath) => this.handleChange(filePath));
|
|
77
|
+
this.fsWatcher.on('error', (err) => {
|
|
78
|
+
this.deps.appendOutput({
|
|
79
|
+
ts: Date.now(),
|
|
80
|
+
source: 'watcher',
|
|
81
|
+
severity: 'error',
|
|
82
|
+
text: `chokidar error: ${String(err)}`,
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
async stop() {
|
|
87
|
+
// Snapshot exitCode promises before killing so we can await reap after
|
|
88
|
+
// SIGTERM is delivered. Without awaiting, Node's event loop keeps the
|
|
89
|
+
// ChildProcess + stdio socket handles alive, which blocks process exit
|
|
90
|
+
// on the consumer side.
|
|
91
|
+
const reaps = [];
|
|
92
|
+
for (const [name, handle] of this.handles) {
|
|
93
|
+
reaps.push(handle.exitCode.catch(() => undefined));
|
|
94
|
+
this.killHandle(name);
|
|
95
|
+
}
|
|
96
|
+
await Promise.all(reaps);
|
|
97
|
+
if (this.fsWatcher) {
|
|
98
|
+
await this.fsWatcher.close();
|
|
99
|
+
this.fsWatcher = null;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
toggle(repoName) {
|
|
103
|
+
const handle = this.handles.get(repoName);
|
|
104
|
+
if (handle) {
|
|
105
|
+
this.killHandle(repoName);
|
|
106
|
+
this.deps.setRepoStatus(repoName, 'idle');
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
void this.spawnWatcher(repoName);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
rebuild(repoName) {
|
|
113
|
+
this.killHandle(repoName);
|
|
114
|
+
void this.spawnWatcher(repoName);
|
|
115
|
+
}
|
|
116
|
+
lint(repoName) {
|
|
117
|
+
const managed = this.repos.get(repoName);
|
|
118
|
+
if (!managed)
|
|
119
|
+
return;
|
|
120
|
+
void runAllLints({
|
|
121
|
+
repoName,
|
|
122
|
+
repoRootDir: managed.rootDir,
|
|
123
|
+
lintConfig: managed.config.lint,
|
|
124
|
+
appendOutput: this.deps.appendOutput,
|
|
125
|
+
onLintResult: (result) => this.deps.setLintStatus(repoName, result),
|
|
126
|
+
spawn: this.deps.spawn,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Subscribe to filesystem-event notifications. The listener is called for
|
|
131
|
+
* every detected change before the watcher-launch logic. Returns an
|
|
132
|
+
* unsubscribe function.
|
|
133
|
+
*/
|
|
134
|
+
subscribeFsEvents(listener) {
|
|
135
|
+
this.fsEventListeners.add(listener);
|
|
136
|
+
return () => {
|
|
137
|
+
this.fsEventListeners.delete(listener);
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
// ─── Private ────────────────────────────────────────────────────────────
|
|
141
|
+
handleChange(filePath) {
|
|
142
|
+
const repoName = mapPathToRepo(filePath, this.prefixEntries);
|
|
143
|
+
if (!repoName)
|
|
144
|
+
return;
|
|
145
|
+
const managed = this.repos.get(repoName);
|
|
146
|
+
if (!managed)
|
|
147
|
+
return;
|
|
148
|
+
for (const listener of this.fsEventListeners) {
|
|
149
|
+
listener(filePath);
|
|
150
|
+
}
|
|
151
|
+
scheduleLint(filePath, {
|
|
152
|
+
repoName,
|
|
153
|
+
repoRootDir: managed.rootDir,
|
|
154
|
+
lintConfig: managed.config.lint,
|
|
155
|
+
appendOutput: this.deps.appendOutput,
|
|
156
|
+
onLintResult: (result) => this.deps.setLintStatus(repoName, result),
|
|
157
|
+
spawn: this.deps.spawn,
|
|
158
|
+
});
|
|
159
|
+
if (!this.handles.has(repoName)) {
|
|
160
|
+
void this.spawnWatcher(repoName);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
async spawnWatcher(repoName) {
|
|
164
|
+
const managed = this.repos.get(repoName);
|
|
165
|
+
if (!managed || !managed.config.watch) {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
const cmd = managed.config.watch;
|
|
169
|
+
const [exe, ...args] = cmd.split(/\s+/);
|
|
170
|
+
if (!exe)
|
|
171
|
+
return;
|
|
172
|
+
this.deps.setRepoStatus(repoName, 'running');
|
|
173
|
+
this.deps.appendOutput({
|
|
174
|
+
ts: Date.now(),
|
|
175
|
+
source: repoName,
|
|
176
|
+
severity: 'info',
|
|
177
|
+
text: `starting watcher: ${cmd}`,
|
|
178
|
+
});
|
|
179
|
+
const handle = this.deps.spawn(exe, args, {
|
|
180
|
+
cwd: managed.rootDir,
|
|
181
|
+
onStdout: (line) => this.deps.appendOutput({
|
|
182
|
+
ts: Date.now(),
|
|
183
|
+
source: repoName,
|
|
184
|
+
severity: 'info',
|
|
185
|
+
text: line,
|
|
186
|
+
}),
|
|
187
|
+
onStderr: (line) => this.deps.appendOutput({
|
|
188
|
+
ts: Date.now(),
|
|
189
|
+
source: repoName,
|
|
190
|
+
severity: 'warn',
|
|
191
|
+
text: line,
|
|
192
|
+
}),
|
|
193
|
+
});
|
|
194
|
+
this.handles.set(repoName, handle);
|
|
195
|
+
// Capture this spawn's handle identity. If rebuild() replaces the handle
|
|
196
|
+
// before exitCode resolves, the stale exit handler must not clobber the
|
|
197
|
+
// new watcher's status.
|
|
198
|
+
const thisHandle = handle;
|
|
199
|
+
const code = await handle.exitCode;
|
|
200
|
+
if (this.handles.get(repoName) !== thisHandle)
|
|
201
|
+
return;
|
|
202
|
+
this.handles.delete(repoName);
|
|
203
|
+
if (code === 0) {
|
|
204
|
+
this.deps.setRepoStatus(repoName, 'idle');
|
|
205
|
+
this.deps.appendOutput({
|
|
206
|
+
ts: Date.now(),
|
|
207
|
+
source: repoName,
|
|
208
|
+
severity: 'info',
|
|
209
|
+
text: `watcher exited cleanly`,
|
|
210
|
+
});
|
|
211
|
+
// Transition: fail→clean only (not on first-ever clean exit).
|
|
212
|
+
if (this.deps.onBuildOutcome && this.buildOutcomes.get(repoName) === 'fail') {
|
|
213
|
+
this.deps.onBuildOutcome(repoName, 'build-recovered');
|
|
214
|
+
}
|
|
215
|
+
this.buildOutcomes.set(repoName, 'success');
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
this.deps.setRepoStatus(repoName, 'failing');
|
|
219
|
+
this.deps.appendOutput({
|
|
220
|
+
ts: Date.now(),
|
|
221
|
+
source: repoName,
|
|
222
|
+
severity: 'error',
|
|
223
|
+
text: `watcher exited with code ${code}`,
|
|
224
|
+
});
|
|
225
|
+
// Transition: clean→fail only (not on repeated failures).
|
|
226
|
+
if (this.deps.onBuildOutcome && this.buildOutcomes.get(repoName) !== 'fail') {
|
|
227
|
+
this.deps.onBuildOutcome(repoName, 'build-failed');
|
|
228
|
+
}
|
|
229
|
+
this.buildOutcomes.set(repoName, 'fail');
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
killHandle(repoName) {
|
|
233
|
+
const handle = this.handles.get(repoName);
|
|
234
|
+
if (handle) {
|
|
235
|
+
handle.kill('SIGTERM');
|
|
236
|
+
this.handles.delete(repoName);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure function: maps an absolute file path to the name of the owning repo.
|
|
3
|
+
*
|
|
4
|
+
* Strategy: longest-prefix match against each repo's root directory.
|
|
5
|
+
* Returns null if no repo claims the path.
|
|
6
|
+
*/
|
|
7
|
+
export interface RepoPrefixEntry {
|
|
8
|
+
name: string;
|
|
9
|
+
/** Absolute path to the repo root directory. */
|
|
10
|
+
rootDir: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Finds the repo that owns `filePath` by longest-prefix match on `rootDir`.
|
|
14
|
+
*
|
|
15
|
+
* Rules:
|
|
16
|
+
* - `rootDir` must be a prefix of `filePath` with a path-separator boundary
|
|
17
|
+
* (avoids `/foo-bar` matching `/foo`).
|
|
18
|
+
* - When multiple repos match, the one with the longest `rootDir` wins.
|
|
19
|
+
*
|
|
20
|
+
* @returns the owning repo name, or null if no repo matches.
|
|
21
|
+
*/
|
|
22
|
+
export declare function mapPathToRepo(filePath: string, repos: RepoPrefixEntry[]): string | null;
|
|
23
|
+
//# sourceMappingURL=path-mapper.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"path-mapper.d.ts","sourceRoot":"","sources":["../../src/watchers/path-mapper.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,gDAAgD;IAChD,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,eAAe,EAAE,GAAG,MAAM,GAAG,IAAI,CAevF"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure function: maps an absolute file path to the name of the owning repo.
|
|
3
|
+
*
|
|
4
|
+
* Strategy: longest-prefix match against each repo's root directory.
|
|
5
|
+
* Returns null if no repo claims the path.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Finds the repo that owns `filePath` by longest-prefix match on `rootDir`.
|
|
9
|
+
*
|
|
10
|
+
* Rules:
|
|
11
|
+
* - `rootDir` must be a prefix of `filePath` with a path-separator boundary
|
|
12
|
+
* (avoids `/foo-bar` matching `/foo`).
|
|
13
|
+
* - When multiple repos match, the one with the longest `rootDir` wins.
|
|
14
|
+
*
|
|
15
|
+
* @returns the owning repo name, or null if no repo matches.
|
|
16
|
+
*/
|
|
17
|
+
export function mapPathToRepo(filePath, repos) {
|
|
18
|
+
let bestMatch = null;
|
|
19
|
+
for (const repo of repos) {
|
|
20
|
+
const prefix = repo.rootDir.endsWith('/') ? repo.rootDir : `${repo.rootDir}/`;
|
|
21
|
+
const isMatch = filePath === repo.rootDir || filePath.startsWith(prefix);
|
|
22
|
+
if (isMatch) {
|
|
23
|
+
if (bestMatch === null || repo.rootDir.length > bestMatch.rootDir.length) {
|
|
24
|
+
bestMatch = repo;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return bestMatch?.name ?? null;
|
|
29
|
+
}
|