@decohere/devkit 0.0.1-dev.119
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 +14 -0
- package/commonjs/src/cli/action.js +65 -0
- package/commonjs/src/cli/command.js +127 -0
- package/commonjs/src/cli/index.js +61 -0
- package/commonjs/src/cli/module.type.js +18 -0
- package/commonjs/src/cli/options.js +115 -0
- package/commonjs/src/diagnostics/index.js +22 -0
- package/commonjs/src/diagnostics/trace.js +79 -0
- package/commonjs/src/interop/docker/docker.js +54 -0
- package/commonjs/src/interop/errors/interop.error.js +12 -0
- package/commonjs/src/interop/git/git.js +134 -0
- package/commonjs/src/interop/git/git.types.js +4 -0
- package/commonjs/src/interop/index.js +97 -0
- package/commonjs/src/interop/node/index.js +18 -0
- package/commonjs/src/interop/node/node.js +45 -0
- package/commonjs/src/interop/npm/npm.js +231 -0
- package/commonjs/src/interop/npm/npm.types.js +16 -0
- package/commonjs/src/interop/pnpm/pnpm.js +34 -0
- package/commonjs/src/interop/pnpm/pnpm.types.js +4 -0
- package/commonjs/src/interop/shell/shell.argv.js +45 -0
- package/commonjs/src/interop/shell/shell.js +105 -0
- package/commonjs/src/interop/shell/shell.types.js +4 -0
- package/commonjs/src/interop/types/detect-package-manager.js +71 -0
- package/commonjs/src/interop/types/package-manager.js +4 -0
- package/commonjs/src/interop/yarn/yarn.js +34 -0
- package/commonjs/src/interop/yarn/yarn.types.js +4 -0
- package/commonjs/src/io/find-directories.js +21 -0
- package/commonjs/src/io/find-file.js +45 -0
- package/commonjs/src/io/index.js +20 -0
- package/commonjs/src/io/watch.js +261 -0
- package/es6/src/cli/action.js +47 -0
- package/es6/src/cli/command.js +106 -0
- package/es6/src/cli/index.js +4 -0
- package/es6/src/cli/module.type.js +11 -0
- package/es6/src/cli/options.js +76 -0
- package/es6/src/diagnostics/index.js +1 -0
- package/es6/src/diagnostics/trace.js +61 -0
- package/es6/src/interop/docker/docker.js +44 -0
- package/es6/src/interop/errors/interop.error.js +2 -0
- package/es6/src/interop/git/git.js +124 -0
- package/es6/src/interop/git/git.types.js +1 -0
- package/es6/src/interop/index.js +21 -0
- package/es6/src/interop/node/index.js +1 -0
- package/es6/src/interop/node/node.js +35 -0
- package/es6/src/interop/npm/npm.js +180 -0
- package/es6/src/interop/npm/npm.types.js +6 -0
- package/es6/src/interop/pnpm/pnpm.js +24 -0
- package/es6/src/interop/pnpm/pnpm.types.js +1 -0
- package/es6/src/interop/shell/shell.argv.js +35 -0
- package/es6/src/interop/shell/shell.js +95 -0
- package/es6/src/interop/shell/shell.types.js +1 -0
- package/es6/src/interop/types/detect-package-manager.js +50 -0
- package/es6/src/interop/types/package-manager.js +1 -0
- package/es6/src/interop/yarn/yarn.js +24 -0
- package/es6/src/interop/yarn/yarn.types.js +1 -0
- package/es6/src/io/find-directories.js +11 -0
- package/es6/src/io/find-file.js +27 -0
- package/es6/src/io/index.js +3 -0
- package/es6/src/io/watch.js +238 -0
- package/package.json +49 -0
- package/types/src/cli/action.d.ts +36 -0
- package/types/src/cli/command.d.ts +34 -0
- package/types/src/cli/index.d.ts +7 -0
- package/types/src/cli/module.type.d.ts +36 -0
- package/types/src/cli/options.d.ts +28 -0
- package/types/src/diagnostics/index.d.ts +1 -0
- package/types/src/diagnostics/trace.d.ts +10 -0
- package/types/src/interop/docker/docker.d.ts +19 -0
- package/types/src/interop/errors/interop.error.d.ts +2 -0
- package/types/src/interop/git/git.d.ts +16 -0
- package/types/src/interop/git/git.types.d.ts +14 -0
- package/types/src/interop/index.d.ts +14 -0
- package/types/src/interop/node/index.d.ts +1 -0
- package/types/src/interop/node/node.d.ts +5 -0
- package/types/src/interop/npm/npm.d.ts +29 -0
- package/types/src/interop/npm/npm.types.d.ts +24 -0
- package/types/src/interop/pnpm/pnpm.d.ts +9 -0
- package/types/src/interop/pnpm/pnpm.types.d.ts +3 -0
- package/types/src/interop/shell/shell.argv.d.ts +5 -0
- package/types/src/interop/shell/shell.d.ts +6 -0
- package/types/src/interop/shell/shell.types.d.ts +9 -0
- package/types/src/interop/types/detect-package-manager.d.ts +4 -0
- package/types/src/interop/types/package-manager.d.ts +10 -0
- package/types/src/interop/yarn/yarn.d.ts +9 -0
- package/types/src/interop/yarn/yarn.types.d.ts +3 -0
- package/types/src/io/find-directories.d.ts +2 -0
- package/types/src/io/find-file.d.ts +9 -0
- package/types/src/io/index.d.ts +3 -0
- package/types/src/io/watch.d.ts +16 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Shell } from "../shell/shell.js";
|
|
2
|
+
const Commands = {
|
|
3
|
+
install: "install"
|
|
4
|
+
};
|
|
5
|
+
export class Pnpm extends Shell {
|
|
6
|
+
name = "pnpm";
|
|
7
|
+
program = "pnpm";
|
|
8
|
+
async install(options = {
|
|
9
|
+
cwd: "./"
|
|
10
|
+
}) {
|
|
11
|
+
const { cwd } = options;
|
|
12
|
+
return this.run({
|
|
13
|
+
cwd
|
|
14
|
+
}, Commands.install);
|
|
15
|
+
}
|
|
16
|
+
async syncLockFile(options = {
|
|
17
|
+
cwd: "./"
|
|
18
|
+
}) {
|
|
19
|
+
const { cwd } = options;
|
|
20
|
+
return this.run({
|
|
21
|
+
cwd
|
|
22
|
+
}, Commands.install, "--lockfile-only", "--ignore-scripts");
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
function fields(args) {
|
|
2
|
+
const positional = [];
|
|
3
|
+
const flags = {};
|
|
4
|
+
for(let i = 0; i < args.length; i++){
|
|
5
|
+
const arg = args[i];
|
|
6
|
+
if (arg === "--") {
|
|
7
|
+
positional.push(...args.slice(i + 1));
|
|
8
|
+
break;
|
|
9
|
+
}
|
|
10
|
+
if (arg.startsWith("--")) {
|
|
11
|
+
const body = arg.slice(2);
|
|
12
|
+
const eq = body.indexOf("=");
|
|
13
|
+
if (eq !== -1) {
|
|
14
|
+
flags[body.slice(0, eq)] = body.slice(eq + 1);
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
const next = args[i + 1];
|
|
18
|
+
if (next != null && !next.startsWith("-")) {
|
|
19
|
+
flags[body] = next;
|
|
20
|
+
i += 1;
|
|
21
|
+
} else {
|
|
22
|
+
flags[body] = true;
|
|
23
|
+
}
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
positional.push(arg);
|
|
27
|
+
}
|
|
28
|
+
return {
|
|
29
|
+
_: positional,
|
|
30
|
+
...flags
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
export const shellArgv = {
|
|
34
|
+
fields
|
|
35
|
+
};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { scribe } from "@decohere/scribe";
|
|
3
|
+
import { InteropError } from "../errors/interop.error.js";
|
|
4
|
+
import { shellArgv } from "./shell.argv.js";
|
|
5
|
+
export class Shell {
|
|
6
|
+
async run(options, ...args) {
|
|
7
|
+
const logger = scribe.logger(Shell.name);
|
|
8
|
+
const argv = args ?? [];
|
|
9
|
+
const argvFields = shellArgv.fields(argv);
|
|
10
|
+
const positionals = Array.isArray(argvFields._) ? argvFields._ : [];
|
|
11
|
+
const commandName = [
|
|
12
|
+
this.program,
|
|
13
|
+
...positionals
|
|
14
|
+
].join(" ").trim();
|
|
15
|
+
logger.info("Running", {
|
|
16
|
+
program: this.program,
|
|
17
|
+
args: argvFields
|
|
18
|
+
});
|
|
19
|
+
return new Promise((resolve, reject)=>{
|
|
20
|
+
let settled = false;
|
|
21
|
+
const resolveOnce = (response)=>{
|
|
22
|
+
if (settled) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
settled = true;
|
|
26
|
+
resolve(response);
|
|
27
|
+
};
|
|
28
|
+
const rejectOnce = (error)=>{
|
|
29
|
+
if (settled) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
settled = true;
|
|
33
|
+
reject(error);
|
|
34
|
+
};
|
|
35
|
+
const { cwd, env } = options;
|
|
36
|
+
const spawnOpt = {
|
|
37
|
+
cwd: cwd ?? ".",
|
|
38
|
+
stdio: [
|
|
39
|
+
"inherit",
|
|
40
|
+
"pipe",
|
|
41
|
+
"pipe"
|
|
42
|
+
],
|
|
43
|
+
env: env ?? {
|
|
44
|
+
...process.env
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
const proc = spawn(this.program, argv, spawnOpt);
|
|
48
|
+
const responseData = [];
|
|
49
|
+
const errorData = [];
|
|
50
|
+
proc.on("close", (code, signal)=>{
|
|
51
|
+
if (code === 0) {
|
|
52
|
+
resolveOnce({
|
|
53
|
+
message: `${commandName} completed`,
|
|
54
|
+
data: responseData
|
|
55
|
+
});
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const stderr = errorData.join("\n").trim();
|
|
59
|
+
const stdout = responseData.join("\n").trim();
|
|
60
|
+
const exitCodeText = code == null ? "unknown" : String(code);
|
|
61
|
+
const signalText = signal ? ` signal ${signal}` : "";
|
|
62
|
+
const base = `Running '${commandName}' failed with code ${exitCodeText}${signalText}`;
|
|
63
|
+
const details = [
|
|
64
|
+
stderr ? `stderr:\n${stderr}` : "",
|
|
65
|
+
!stderr && stdout ? `stdout:\n${stdout}` : ""
|
|
66
|
+
].filter(Boolean).join("\n");
|
|
67
|
+
rejectOnce(new InteropError(details ? `${base}\n${details}` : base));
|
|
68
|
+
});
|
|
69
|
+
proc.on("error", (err)=>{
|
|
70
|
+
rejectOnce(err);
|
|
71
|
+
});
|
|
72
|
+
proc.stdout.on("error", (err)=>{
|
|
73
|
+
rejectOnce(err);
|
|
74
|
+
});
|
|
75
|
+
proc.stdout.on("data", (data)=>{
|
|
76
|
+
const lines = this.parseStream(data);
|
|
77
|
+
console.log(data.toString());
|
|
78
|
+
responseData.push(...lines);
|
|
79
|
+
});
|
|
80
|
+
proc.stderr?.on("error", (err)=>{
|
|
81
|
+
rejectOnce(err);
|
|
82
|
+
});
|
|
83
|
+
proc.stderr?.on("data", (data)=>{
|
|
84
|
+
const lines = this.parseStream(data);
|
|
85
|
+
console.error(data.toString());
|
|
86
|
+
errorData.push(...lines);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
parseStream(data) {
|
|
91
|
+
const dataStr = data.toString();
|
|
92
|
+
const lines = dataStr.split("\n").filter((l)=>l !== "");
|
|
93
|
+
return lines;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { createInterface } from "node:readline/promises";
|
|
4
|
+
import { Npm } from "../npm/npm.js";
|
|
5
|
+
import { Pnpm } from "../pnpm/pnpm.js";
|
|
6
|
+
import { Yarn } from "../yarn/yarn.js";
|
|
7
|
+
const LOCKFILES = [
|
|
8
|
+
[
|
|
9
|
+
"pnpm-lock.yaml",
|
|
10
|
+
()=>new Pnpm()
|
|
11
|
+
],
|
|
12
|
+
[
|
|
13
|
+
"yarn.lock",
|
|
14
|
+
()=>new Yarn()
|
|
15
|
+
],
|
|
16
|
+
[
|
|
17
|
+
"package-lock.json",
|
|
18
|
+
()=>new Npm()
|
|
19
|
+
]
|
|
20
|
+
];
|
|
21
|
+
function fromLockfile(cwd) {
|
|
22
|
+
for (const [file, factory] of LOCKFILES){
|
|
23
|
+
if (existsSync(join(cwd, file))) return factory();
|
|
24
|
+
}
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
function fromUserAgent() {
|
|
28
|
+
const agent = process.env.npm_config_user_agent ?? "";
|
|
29
|
+
if (agent.startsWith("pnpm")) return new Pnpm();
|
|
30
|
+
if (agent.startsWith("yarn")) return new Yarn();
|
|
31
|
+
if (agent.startsWith("npm")) return new Npm();
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
export function detectPackageManager(cwd) {
|
|
35
|
+
return fromLockfile(cwd) ?? fromUserAgent();
|
|
36
|
+
}
|
|
37
|
+
export function packageManagerFromString(name) {
|
|
38
|
+
if (name === "yarn") return new Yarn();
|
|
39
|
+
if (name === "npm") return new Npm();
|
|
40
|
+
return new Pnpm();
|
|
41
|
+
}
|
|
42
|
+
export async function promptForPackageManager() {
|
|
43
|
+
const rl = createInterface({
|
|
44
|
+
input: process.stdin,
|
|
45
|
+
output: process.stdout
|
|
46
|
+
});
|
|
47
|
+
const answer = await rl.question("Package manager? [pnpm/yarn/npm] (default: pnpm): ");
|
|
48
|
+
rl.close();
|
|
49
|
+
return packageManagerFromString(answer.trim().toLowerCase());
|
|
50
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Shell } from "../shell/shell.js";
|
|
2
|
+
const Commands = {
|
|
3
|
+
install: "install"
|
|
4
|
+
};
|
|
5
|
+
export class Yarn extends Shell {
|
|
6
|
+
name = "yarn";
|
|
7
|
+
program = "yarn";
|
|
8
|
+
async install(options = {
|
|
9
|
+
cwd: "./"
|
|
10
|
+
}) {
|
|
11
|
+
const { cwd } = options;
|
|
12
|
+
return this.run({
|
|
13
|
+
cwd
|
|
14
|
+
}, Commands.install);
|
|
15
|
+
}
|
|
16
|
+
async syncLockFile(options = {
|
|
17
|
+
cwd: "./"
|
|
18
|
+
}) {
|
|
19
|
+
const { cwd } = options;
|
|
20
|
+
return this.run({
|
|
21
|
+
cwd
|
|
22
|
+
}, Commands.install, "--mode", "update-lockfile", "--ignore-scripts");
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { glob } from "glob";
|
|
2
|
+
export async function findDirectories(...pattern) {
|
|
3
|
+
const paths = await glob(pattern, {
|
|
4
|
+
withFileTypes: true
|
|
5
|
+
});
|
|
6
|
+
const dirs = paths.filter((p)=>p.isDirectory()).map((p)=>({
|
|
7
|
+
name: p.name,
|
|
8
|
+
parentPath: p.parentPath
|
|
9
|
+
}));
|
|
10
|
+
return dirs;
|
|
11
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { glob } from "glob";
|
|
2
|
+
import { FileEx } from "@decohere/core/io";
|
|
3
|
+
export async function findFile(pattern) {
|
|
4
|
+
const files = await glob(pattern, {
|
|
5
|
+
includeChildMatches: false
|
|
6
|
+
});
|
|
7
|
+
if (files.length === 0) {
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
const file = {
|
|
11
|
+
relativePath: files.at(0)
|
|
12
|
+
};
|
|
13
|
+
return file;
|
|
14
|
+
}
|
|
15
|
+
export async function findFiles(patterns, options) {
|
|
16
|
+
const paths = await glob(patterns, {
|
|
17
|
+
...options,
|
|
18
|
+
withFileTypes: true
|
|
19
|
+
});
|
|
20
|
+
const files = paths.filter((p)=>p.isFile()).map((p)=>{
|
|
21
|
+
return new FileEx({
|
|
22
|
+
relativePath: p.relative(),
|
|
23
|
+
fullPath: p.fullpath()
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
return files;
|
|
27
|
+
}
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { watch as nativeWatch, readdirSync, statSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import picomatch from "picomatch";
|
|
4
|
+
import { FileEx } from "@decohere/core/io";
|
|
5
|
+
export const WatchEvents = {
|
|
6
|
+
add: "add",
|
|
7
|
+
change: "change",
|
|
8
|
+
addDir: "addDir",
|
|
9
|
+
unlink: "unlink",
|
|
10
|
+
unlinkDir: "unlinkDir"
|
|
11
|
+
};
|
|
12
|
+
export function watch(directory, options = {
|
|
13
|
+
cwd: "."
|
|
14
|
+
}) {
|
|
15
|
+
const { include, ignore, ignoreInitial = false } = options;
|
|
16
|
+
const changeCoalesceMs = 25;
|
|
17
|
+
const handlers = {
|
|
18
|
+
ready: new Set(),
|
|
19
|
+
add: new Set(),
|
|
20
|
+
change: new Set(),
|
|
21
|
+
addDir: new Set(),
|
|
22
|
+
unlink: new Set(),
|
|
23
|
+
unlinkDir: new Set()
|
|
24
|
+
};
|
|
25
|
+
let resolveReady;
|
|
26
|
+
const ready = new Promise((resolve)=>{
|
|
27
|
+
resolveReady = resolve;
|
|
28
|
+
});
|
|
29
|
+
const snapshot = buildSnapshot(directory);
|
|
30
|
+
const isFiltered = (relativePath)=>{
|
|
31
|
+
const posixRelativePath = toPosixPath(relativePath);
|
|
32
|
+
const posixFullPath = toPosixPath(path.resolve(directory, relativePath));
|
|
33
|
+
const matchesEitherPath = (patterns)=>picomatch.isMatch(posixRelativePath, patterns) || picomatch.isMatch(posixFullPath, patterns);
|
|
34
|
+
if (include && include.length > 0) {
|
|
35
|
+
return !matchesEitherPath(include);
|
|
36
|
+
}
|
|
37
|
+
if (ignore && ignore.length > 0) {
|
|
38
|
+
return matchesEitherPath(ignore);
|
|
39
|
+
}
|
|
40
|
+
return false;
|
|
41
|
+
};
|
|
42
|
+
const emit = (event, relativePath)=>{
|
|
43
|
+
if (isFiltered(relativePath)) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const fullPath = path.resolve(directory, relativePath);
|
|
47
|
+
for (const handler of handlers[event]){
|
|
48
|
+
handler(new FileEx({
|
|
49
|
+
fullPath,
|
|
50
|
+
relativePath
|
|
51
|
+
}));
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
const pendingChangeTimers = new Map();
|
|
55
|
+
const emitChange = (relativePath)=>{
|
|
56
|
+
const existing = pendingChangeTimers.get(relativePath);
|
|
57
|
+
if (existing != null) {
|
|
58
|
+
clearTimeout(existing);
|
|
59
|
+
}
|
|
60
|
+
const timer = setTimeout(()=>{
|
|
61
|
+
pendingChangeTimers.delete(relativePath);
|
|
62
|
+
emit("change", relativePath);
|
|
63
|
+
}, changeCoalesceMs);
|
|
64
|
+
pendingChangeTimers.set(relativePath, timer);
|
|
65
|
+
};
|
|
66
|
+
const emitReady = ()=>{
|
|
67
|
+
for (const handler of handlers.ready){
|
|
68
|
+
handler();
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
queueMicrotask(()=>{
|
|
72
|
+
if (!ignoreInitial) {
|
|
73
|
+
for (const [relativePath, entry] of snapshot){
|
|
74
|
+
emit(entry.isDirectory ? "addDir" : "add", relativePath);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
emitReady();
|
|
78
|
+
resolveReady?.();
|
|
79
|
+
resolveReady = undefined;
|
|
80
|
+
});
|
|
81
|
+
// One non-recursive watcher per directory. Node's recursive mode on Linux tracks files
|
|
82
|
+
// individually and goes silent for a file once an editor replaces it with an atomic save
|
|
83
|
+
// (write temp file, rename over the original); directory watchers keep seeing it (#505).
|
|
84
|
+
const directoryWatchers = new Map();
|
|
85
|
+
const watchDirectory = (relativeDir)=>{
|
|
86
|
+
if (directoryWatchers.has(relativeDir)) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
try {
|
|
90
|
+
const watcher = nativeWatch(path.resolve(directory, relativeDir), (_eventType, fileName)=>{
|
|
91
|
+
if (fileName != null) {
|
|
92
|
+
onFsEvent(path.posix.join(relativeDir, toPosixPath(fileName.toString())));
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
watcher.on("error", ()=>unwatchDirectory(relativeDir));
|
|
96
|
+
directoryWatchers.set(relativeDir, watcher);
|
|
97
|
+
} catch {
|
|
98
|
+
// The directory vanished before we could watch it; its unlink event covers it.
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
const unwatchDirectory = (relativeDir)=>{
|
|
102
|
+
for (const [watchedDir, watcher] of directoryWatchers){
|
|
103
|
+
if (watchedDir === relativeDir || watchedDir.startsWith(`${relativeDir}/`)) {
|
|
104
|
+
watcher.close();
|
|
105
|
+
directoryWatchers.delete(watchedDir);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
/** A directory appeared: watch it, and report what was created in it before we were watching. */ const addDirectory = (relativeDir)=>{
|
|
110
|
+
watchDirectory(relativeDir);
|
|
111
|
+
const found = new Map();
|
|
112
|
+
try {
|
|
113
|
+
collectSnapshotEntries(directory, path.resolve(directory, relativeDir), found);
|
|
114
|
+
} catch {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
for (const [relativePath, entry] of found){
|
|
118
|
+
if (snapshot.has(relativePath)) continue;
|
|
119
|
+
snapshot.set(relativePath, entry);
|
|
120
|
+
if (entry.isDirectory) watchDirectory(relativePath);
|
|
121
|
+
emit(entry.isDirectory ? "addDir" : "add", relativePath);
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
/** A directory went away: stop watching it and forget everything that was under it. */ const removeDirectoryContents = (relativeDir)=>{
|
|
125
|
+
unwatchDirectory(relativeDir);
|
|
126
|
+
// oxlint-disable-next-line unicorn/no-useless-spread -- iterate a copy: the loop deletes from the snapshot
|
|
127
|
+
for (const [relativePath, entry] of [
|
|
128
|
+
...snapshot
|
|
129
|
+
]){
|
|
130
|
+
if (relativePath.startsWith(`${relativeDir}/`)) {
|
|
131
|
+
snapshot.delete(relativePath);
|
|
132
|
+
emit(entry.isDirectory ? "unlinkDir" : "unlink", relativePath);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
const onFsEvent = (eventPath)=>{
|
|
137
|
+
const relativePath = eventPath === "." ? "" : eventPath;
|
|
138
|
+
if (relativePath.length === 0) {
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
const fullPath = path.resolve(directory, relativePath);
|
|
142
|
+
const previous = snapshot.get(relativePath);
|
|
143
|
+
try {
|
|
144
|
+
const stat = statSync(fullPath);
|
|
145
|
+
const current = toSnapshotEntry(stat);
|
|
146
|
+
snapshot.set(relativePath, current);
|
|
147
|
+
if (previous == null) {
|
|
148
|
+
emit(current.isDirectory ? "addDir" : "add", relativePath);
|
|
149
|
+
if (current.isDirectory) addDirectory(relativePath);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
if (previous.isDirectory !== current.isDirectory) {
|
|
153
|
+
if (previous.isDirectory) removeDirectoryContents(relativePath);
|
|
154
|
+
emit(previous.isDirectory ? "unlinkDir" : "unlink", relativePath);
|
|
155
|
+
emit(current.isDirectory ? "addDir" : "add", relativePath);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
if (!current.isDirectory && previous.fingerprint !== current.fingerprint) {
|
|
159
|
+
emitChange(relativePath);
|
|
160
|
+
}
|
|
161
|
+
} catch {
|
|
162
|
+
if (previous == null) {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
snapshot.delete(relativePath);
|
|
166
|
+
const pending = pendingChangeTimers.get(relativePath);
|
|
167
|
+
if (pending != null) {
|
|
168
|
+
clearTimeout(pending);
|
|
169
|
+
pendingChangeTimers.delete(relativePath);
|
|
170
|
+
}
|
|
171
|
+
emit(previous.isDirectory ? "unlinkDir" : "unlink", relativePath);
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
watchDirectory("");
|
|
175
|
+
for (const [relativePath, entry] of snapshot){
|
|
176
|
+
if (entry.isDirectory) watchDirectory(relativePath);
|
|
177
|
+
}
|
|
178
|
+
const watchApi = {
|
|
179
|
+
ready,
|
|
180
|
+
on (event, handler) {
|
|
181
|
+
if (event === "ready") {
|
|
182
|
+
handlers.ready.add(handler);
|
|
183
|
+
return watchApi;
|
|
184
|
+
}
|
|
185
|
+
handlers[event].add(handler);
|
|
186
|
+
return watchApi;
|
|
187
|
+
},
|
|
188
|
+
close: ()=>{
|
|
189
|
+
for (const pending of pendingChangeTimers.values()){
|
|
190
|
+
clearTimeout(pending);
|
|
191
|
+
}
|
|
192
|
+
pendingChangeTimers.clear();
|
|
193
|
+
for (const watcher of directoryWatchers.values()){
|
|
194
|
+
watcher.close();
|
|
195
|
+
}
|
|
196
|
+
directoryWatchers.clear();
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
return watchApi;
|
|
200
|
+
}
|
|
201
|
+
function buildSnapshot(directoryPath) {
|
|
202
|
+
const snapshot = new Map();
|
|
203
|
+
collectSnapshotEntries(directoryPath, directoryPath, snapshot);
|
|
204
|
+
return snapshot;
|
|
205
|
+
}
|
|
206
|
+
/** Never source, and huge: watching every folder inside them could exhaust OS watch limits. */ const UNWATCHED_DIRECTORIES = new Set([
|
|
207
|
+
"node_modules",
|
|
208
|
+
".git"
|
|
209
|
+
]);
|
|
210
|
+
function collectSnapshotEntries(rootDirectoryPath, currentDirectoryPath, snapshot) {
|
|
211
|
+
for (const entry of readdirSync(currentDirectoryPath, {
|
|
212
|
+
encoding: "utf8"
|
|
213
|
+
})){
|
|
214
|
+
const name = typeof entry === "string" ? entry : String(entry);
|
|
215
|
+
const fullPath = path.join(currentDirectoryPath, name);
|
|
216
|
+
if (UNWATCHED_DIRECTORIES.has(name)) {
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
const relativePath = toPosixPath(path.relative(rootDirectoryPath, fullPath));
|
|
220
|
+
const stat = statSync(fullPath);
|
|
221
|
+
const snapshotEntry = toSnapshotEntry(stat);
|
|
222
|
+
snapshot.set(relativePath, snapshotEntry);
|
|
223
|
+
if (!snapshotEntry.isDirectory) {
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
collectSnapshotEntries(rootDirectoryPath, fullPath, snapshot);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
function toSnapshotEntry(stat) {
|
|
230
|
+
const isDirectory = stat.isDirectory();
|
|
231
|
+
return {
|
|
232
|
+
isDirectory,
|
|
233
|
+
fingerprint: isDirectory ? `dir:${stat.mtimeMs}` : `file:${stat.mtimeMs}:${stat.size}`
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
function toPosixPath(filePath) {
|
|
237
|
+
return filePath.split(path.sep).join(path.posix.sep);
|
|
238
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@decohere/devkit",
|
|
3
|
+
"version": "0.0.1-dev.119",
|
|
4
|
+
"description": "The shared base for Decohere's command-line tools: the command framework, git/npm/pnpm/docker wrappers, tracing, and file search and watch. Never needed in a production app.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"typescript",
|
|
7
|
+
"cli",
|
|
8
|
+
"commander",
|
|
9
|
+
"tooling",
|
|
10
|
+
"decohere"
|
|
11
|
+
],
|
|
12
|
+
"license": "ISC",
|
|
13
|
+
"author": "Typesmiths",
|
|
14
|
+
"type": "module",
|
|
15
|
+
"main": "index.js",
|
|
16
|
+
"exports": {
|
|
17
|
+
"./cli": {
|
|
18
|
+
"types": "./types/src/cli/index.d.ts",
|
|
19
|
+
"import": "./es6/src/cli/index.js",
|
|
20
|
+
"require": "./commonjs/src/cli/index.js"
|
|
21
|
+
},
|
|
22
|
+
"./diagnostics": {
|
|
23
|
+
"types": "./types/src/diagnostics/index.d.ts",
|
|
24
|
+
"import": "./es6/src/diagnostics/index.js",
|
|
25
|
+
"require": "./commonjs/src/diagnostics/index.js"
|
|
26
|
+
},
|
|
27
|
+
"./interop": {
|
|
28
|
+
"types": "./types/src/interop/index.d.ts",
|
|
29
|
+
"import": "./es6/src/interop/index.js",
|
|
30
|
+
"require": "./commonjs/src/interop/index.js"
|
|
31
|
+
},
|
|
32
|
+
"./io": {
|
|
33
|
+
"types": "./types/src/io/index.d.ts",
|
|
34
|
+
"import": "./es6/src/io/index.js",
|
|
35
|
+
"require": "./commonjs/src/io/index.js"
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"scripts": {
|
|
39
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
40
|
+
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"@decohere/core": "0.0.1-dev.119",
|
|
43
|
+
"@decohere/scribe": "0.0.1-dev.119",
|
|
44
|
+
"commander": "^13.1.0",
|
|
45
|
+
"glob": "^13.0.1",
|
|
46
|
+
"picomatch": "^4.0.4"
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {}
|
|
49
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { Constructable } from "@decohere/core/reflection";
|
|
2
|
+
import type { UnionToIntersection } from "@decohere/core/system";
|
|
3
|
+
export declare const ActionMap: unique symbol;
|
|
4
|
+
export type ActionClassMap = Record<string, Constructable<Action>>;
|
|
5
|
+
export interface ResultStatus {
|
|
6
|
+
succeeded: boolean;
|
|
7
|
+
error?: string;
|
|
8
|
+
}
|
|
9
|
+
export interface Result {
|
|
10
|
+
readonly name: string;
|
|
11
|
+
readonly succeeded: boolean;
|
|
12
|
+
readonly skipped: boolean;
|
|
13
|
+
readonly message: string;
|
|
14
|
+
}
|
|
15
|
+
export declare class ActionResponse<TData = unknown> implements Result {
|
|
16
|
+
static Success<TData>(name: string, data?: TData[]): ActionResponse<TData>;
|
|
17
|
+
static Skipped(name: string): ActionResponse<unknown>;
|
|
18
|
+
static Blocking(name: string): ActionResponse<unknown>;
|
|
19
|
+
static Error<TData = unknown>(name: string, message: string): ActionResponse<TData>;
|
|
20
|
+
static Error<TData extends ResultStatus>(name: string, data: TData[]): ActionResponse<TData>;
|
|
21
|
+
constructor(data: Partial<ActionResponse<TData>>);
|
|
22
|
+
readonly name: string;
|
|
23
|
+
readonly succeeded: boolean;
|
|
24
|
+
readonly skipped: boolean;
|
|
25
|
+
readonly blocking: boolean;
|
|
26
|
+
readonly message: string;
|
|
27
|
+
readonly data: TData[];
|
|
28
|
+
}
|
|
29
|
+
export interface Action<T extends object = object> {
|
|
30
|
+
run(options: T): Promise<ActionResponse>;
|
|
31
|
+
}
|
|
32
|
+
export type Actions = Record<string, Action>;
|
|
33
|
+
export type ActionOptions<A extends Action> = A extends Action<infer T> ? T : never;
|
|
34
|
+
export type MergeOptions<TActions extends Record<string, Action>> = UnionToIntersection<{
|
|
35
|
+
[K in keyof TActions]: ActionOptions<TActions[K]>;
|
|
36
|
+
}[keyof TActions]>;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Command as CommanderType, type OptionValues } from "commander";
|
|
2
|
+
import type { Constructable } from "@decohere/core/reflection";
|
|
3
|
+
import type { ActionResponse, Actions } from "./action.js";
|
|
4
|
+
export type CommandType = Constructable<DecohereCommand>;
|
|
5
|
+
export type CommandHandler = (opts: OptionValues, cmd: DecohereCommand) => Promise<void>;
|
|
6
|
+
export type CommandResponse<TActions extends Actions = Actions> = {
|
|
7
|
+
[ActionName in keyof TActions]?: ActionResponse;
|
|
8
|
+
};
|
|
9
|
+
export declare const CommandResponse: {
|
|
10
|
+
success<T extends Actions>(_ctx: unknown, responses: CommandResponse<T>): CommandResponse<T>;
|
|
11
|
+
help<T extends Actions = Actions>(): CommandResponse<T>;
|
|
12
|
+
fromAction(name: string, response: ActionResponse): CommandResponse;
|
|
13
|
+
};
|
|
14
|
+
/** Thrown when a command execution fails due to action errors. */
|
|
15
|
+
export declare class CommandError extends Error {
|
|
16
|
+
}
|
|
17
|
+
export declare class DecohereCommand<TActions extends Actions = Actions> extends CommanderType {
|
|
18
|
+
get actions(): readonly Constructable<import("./action.js").Action>[];
|
|
19
|
+
constructor(name?: string | Constructable | Record<string, CommandType>, handler?: CommandHandler);
|
|
20
|
+
/**
|
|
21
|
+
* Whether the command, with these options, runs until it is stopped (`flux serve`,
|
|
22
|
+
* `forge build --watch`). The CLI then listens for SIGINT and SIGTERM from the moment it
|
|
23
|
+
* starts, so a stop during start-up still shuts down cleanly; other commands keep Node's
|
|
24
|
+
* default, where Ctrl+C ends them at once.
|
|
25
|
+
*/
|
|
26
|
+
runsUntilStopped(_options: OptionValues): boolean;
|
|
27
|
+
run(actions: TActions, options: OptionValues): Promise<CommandResponse<TActions>>;
|
|
28
|
+
registerSubcommand(command: CommandType, onAction?: CommandHandler): void;
|
|
29
|
+
registerSubcommand(commands: Record<string, DecohereCommand | CommandType>, onAction?: CommandHandler): void;
|
|
30
|
+
protected runActions(actions: TActions, options: OptionValues): Promise<CommandResponse<TActions>>;
|
|
31
|
+
protected getSelectedActions(actions: TActions): Array<[keyof TActions, TActions[keyof TActions]]>;
|
|
32
|
+
protected throwOnErrors(responses: CommandResponse<TActions>): void;
|
|
33
|
+
private parseCommandName;
|
|
34
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export type { Action, ActionClassMap, ActionOptions, Actions, MergeOptions, Result, ResultStatus, } from "./action.js";
|
|
2
|
+
export { ActionMap, ActionResponse } from "./action.js";
|
|
3
|
+
export type { CommandHandler, CommandType } from "./command.js";
|
|
4
|
+
export { CommandError, CommandResponse, DecohereCommand } from "./command.js";
|
|
5
|
+
export type { ApplicationModule, CommandMap as CommandClassMap, ModuleConfig, WorkspaceFactoryOptions, } from "./module.type.js";
|
|
6
|
+
export { ModuleManifest } from "./module.type.js";
|
|
7
|
+
export { ConfigFileOption, ConfigurationOption, DryRunOption, EnvFileIfExistsOption, EnvFileOption, LogLevelOption, NoBuildOptions, NoCleanOptions, ProjectOption, } from "./options.js";
|