@projitect/cli-internals 0.0.0-stage → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/applier.d.ts +12 -0
- package/dist/applier.d.ts.map +1 -0
- package/dist/applier.js +69 -0
- package/dist/applier.js.map +1 -0
- package/dist/commands/add.d.ts +34 -0
- package/dist/commands/add.d.ts.map +1 -0
- package/dist/commands/add.js +64 -0
- package/dist/commands/add.js.map +1 -0
- package/dist/commands/build.d.ts +39 -0
- package/dist/commands/build.d.ts.map +1 -0
- package/dist/commands/build.js +108 -0
- package/dist/commands/build.js.map +1 -0
- package/dist/commands/dispatch.d.ts +5 -0
- package/dist/commands/dispatch.d.ts.map +1 -0
- package/dist/commands/dispatch.js +211 -0
- package/dist/commands/dispatch.js.map +1 -0
- package/dist/commands/explain.d.ts +10 -0
- package/dist/commands/explain.d.ts.map +1 -0
- package/dist/commands/explain.js +46 -0
- package/dist/commands/explain.js.map +1 -0
- package/dist/commands/init.d.ts +34 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +153 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/inspect-json.d.ts +51 -0
- package/dist/commands/inspect-json.d.ts.map +1 -0
- package/dist/commands/inspect-json.js +58 -0
- package/dist/commands/inspect-json.js.map +1 -0
- package/dist/commands/inspect.d.ts +41 -0
- package/dist/commands/inspect.d.ts.map +1 -0
- package/dist/commands/inspect.js +74 -0
- package/dist/commands/inspect.js.map +1 -0
- package/dist/commands/remodel.d.ts +22 -0
- package/dist/commands/remodel.d.ts.map +1 -0
- package/dist/commands/remodel.js +49 -0
- package/dist/commands/remodel.js.map +1 -0
- package/dist/config-cascade.d.ts +25 -0
- package/dist/config-cascade.d.ts.map +1 -0
- package/dist/config-cascade.js +45 -0
- package/dist/config-cascade.js.map +1 -0
- package/dist/differ.d.ts +31 -0
- package/dist/differ.d.ts.map +1 -0
- package/dist/differ.js +134 -0
- package/dist/differ.js.map +1 -0
- package/dist/edit-pjt.d.ts +19 -0
- package/dist/edit-pjt.d.ts.map +1 -0
- package/dist/edit-pjt.js +40 -0
- package/dist/edit-pjt.js.map +1 -0
- package/dist/filesystem-impl.d.ts +25 -0
- package/dist/filesystem-impl.d.ts.map +1 -0
- package/dist/filesystem-impl.js +153 -0
- package/dist/filesystem-impl.js.map +1 -0
- package/dist/git.d.ts +29 -0
- package/dist/git.d.ts.map +1 -0
- package/dist/git.js +63 -0
- package/dist/git.js.map +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +21 -0
- package/dist/index.js.map +1 -0
- package/dist/ipc/client.d.ts +11 -0
- package/dist/ipc/client.d.ts.map +1 -0
- package/dist/ipc/client.js +45 -0
- package/dist/ipc/client.js.map +1 -0
- package/dist/ipc/index.d.ts +5 -0
- package/dist/ipc/index.d.ts.map +1 -0
- package/dist/ipc/index.js +5 -0
- package/dist/ipc/index.js.map +1 -0
- package/dist/ipc/protocol.d.ts +251 -0
- package/dist/ipc/protocol.d.ts.map +1 -0
- package/dist/ipc/protocol.js +41 -0
- package/dist/ipc/protocol.js.map +1 -0
- package/dist/ipc/server.d.ts +9 -0
- package/dist/ipc/server.d.ts.map +1 -0
- package/dist/ipc/server.js +23 -0
- package/dist/ipc/server.js.map +1 -0
- package/dist/ipc/transport.d.ts +12 -0
- package/dist/ipc/transport.d.ts.map +1 -0
- package/dist/ipc/transport.js +19 -0
- package/dist/ipc/transport.js.map +1 -0
- package/dist/loader.d.ts +45 -0
- package/dist/loader.d.ts.map +1 -0
- package/dist/loader.js +65 -0
- package/dist/loader.js.map +1 -0
- package/dist/lockfile.d.ts +22 -0
- package/dist/lockfile.d.ts.map +1 -0
- package/dist/lockfile.js +59 -0
- package/dist/lockfile.js.map +1 -0
- package/dist/permissions.d.ts +20 -0
- package/dist/permissions.d.ts.map +1 -0
- package/dist/permissions.js +86 -0
- package/dist/permissions.js.map +1 -0
- package/dist/plan.d.ts +278 -0
- package/dist/plan.d.ts.map +1 -0
- package/dist/plan.js +212 -0
- package/dist/plan.js.map +1 -0
- package/dist/platform/file-system.d.ts +3 -0
- package/dist/platform/file-system.d.ts.map +1 -0
- package/dist/platform/file-system.js +109 -0
- package/dist/platform/file-system.js.map +1 -0
- package/dist/platform/index.d.ts +17 -0
- package/dist/platform/index.d.ts.map +1 -0
- package/dist/platform/index.js +20 -0
- package/dist/platform/index.js.map +1 -0
- package/dist/platform/process-runner.d.ts +41 -0
- package/dist/platform/process-runner.d.ts.map +1 -0
- package/dist/platform/process-runner.js +120 -0
- package/dist/platform/process-runner.js.map +1 -0
- package/dist/platform/stdio.d.ts +9 -0
- package/dist/platform/stdio.d.ts.map +1 -0
- package/dist/platform/stdio.js +11 -0
- package/dist/platform/stdio.js.map +1 -0
- package/dist/platform/terminal.d.ts +3 -0
- package/dist/platform/terminal.d.ts.map +1 -0
- package/dist/platform/terminal.js +81 -0
- package/dist/platform/terminal.js.map +1 -0
- package/dist/pm.d.ts +36 -0
- package/dist/pm.d.ts.map +1 -0
- package/dist/pm.js +95 -0
- package/dist/pm.js.map +1 -0
- package/dist/post-apply.d.ts +20 -0
- package/dist/post-apply.d.ts.map +1 -0
- package/dist/post-apply.js +29 -0
- package/dist/post-apply.js.map +1 -0
- package/dist/project-file.d.ts +8 -0
- package/dist/project-file.d.ts.map +1 -0
- package/dist/project-file.js +12 -0
- package/dist/project-file.js.map +1 -0
- package/dist/region.d.ts +105 -0
- package/dist/region.d.ts.map +1 -0
- package/dist/region.js +94 -0
- package/dist/region.js.map +1 -0
- package/dist/remover.d.ts +16 -0
- package/dist/remover.d.ts.map +1 -0
- package/dist/remover.js +88 -0
- package/dist/remover.js.map +1 -0
- package/package.json +52 -3
- package/src/applier.ts +91 -0
- package/src/commands/add.ts +88 -0
- package/src/commands/build.ts +156 -0
- package/src/commands/dispatch.ts +345 -0
- package/src/commands/explain.ts +81 -0
- package/src/commands/init.ts +233 -0
- package/src/commands/inspect-json.ts +68 -0
- package/src/commands/inspect.ts +106 -0
- package/src/commands/remodel.ts +72 -0
- package/src/config-cascade.ts +60 -0
- package/src/differ.ts +188 -0
- package/src/edit-pjt.ts +56 -0
- package/src/filesystem-impl.ts +230 -0
- package/src/git.ts +87 -0
- package/src/index.ts +54 -0
- package/src/ipc/client.ts +108 -0
- package/src/ipc/index.ts +12 -0
- package/src/ipc/protocol.ts +48 -0
- package/src/ipc/server.ts +62 -0
- package/src/ipc/transport.ts +28 -0
- package/src/loader.ts +115 -0
- package/src/lockfile.ts +81 -0
- package/src/permissions.ts +106 -0
- package/src/plan.ts +459 -0
- package/src/platform/file-system.ts +155 -0
- package/src/platform/index.ts +26 -0
- package/src/platform/process-runner.ts +150 -0
- package/src/platform/stdio.ts +11 -0
- package/src/platform/terminal.ts +89 -0
- package/src/pm.ts +163 -0
- package/src/post-apply.ts +45 -0
- package/src/project-file.ts +15 -0
- package/src/region.ts +139 -0
- package/src/remover.ts +127 -0
- package/README.md +0 -3
package/src/loader.ts
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import path from "node:path"
|
|
2
|
+
import { promises as fs } from "node:fs"
|
|
3
|
+
import { pathToFileURL } from "node:url"
|
|
4
|
+
import { Array, Effect, Match, Predicate } from "effect"
|
|
5
|
+
import { PredicateX, StructX } from "@nunofyobiz/effect-extras"
|
|
6
|
+
import { Errors } from "@projitect/core"
|
|
7
|
+
import type { BlueprintFileSystem, Blueprint } from "@projitect/core"
|
|
8
|
+
import type { DirectoryBlueprint } from "@projitect/blueprint"
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The shape `.pjt.ts` produces when the user calls `pjt({ blueprints, config })`. Either an
|
|
12
|
+
* array of blueprints or an Effect that builds the array. The runner unwraps the Effect form
|
|
13
|
+
* inside the planner so blueprint-tree construction itself can be a typed pipeline.
|
|
14
|
+
*
|
|
15
|
+
* The Effect form may fail with any `BlueprintError` and may require `BlueprintFileSystem` —
|
|
16
|
+
* the CLI provides a "config-time" FS layer with broad read permissions before invoking it.
|
|
17
|
+
*/
|
|
18
|
+
export type BlueprintTree = readonly (Blueprint.Blueprint | DirectoryBlueprint)[]
|
|
19
|
+
|
|
20
|
+
export interface ProjitectFile {
|
|
21
|
+
readonly _tag: "ProjitectConfig"
|
|
22
|
+
readonly blueprints:
|
|
23
|
+
| BlueprintTree
|
|
24
|
+
| Effect.Effect<BlueprintTree, Errors.BlueprintError, BlueprintFileSystem>
|
|
25
|
+
readonly config?: Record<string, unknown>
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The user calls this in `.pjt.ts`:
|
|
30
|
+
*
|
|
31
|
+
* export default pjt({ blueprints: [...] })
|
|
32
|
+
*
|
|
33
|
+
* It's an identity tag — we only inspect the default export for the `_tag` marker so we can
|
|
34
|
+
* distinguish accidental other exports from a real projitect config.
|
|
35
|
+
*/
|
|
36
|
+
export const pjt = (input: {
|
|
37
|
+
readonly blueprints: ProjitectFile["blueprints"]
|
|
38
|
+
readonly config?: Record<string, unknown>
|
|
39
|
+
}): ProjitectFile => ({
|
|
40
|
+
_tag: "ProjitectConfig",
|
|
41
|
+
blueprints: input.blueprints,
|
|
42
|
+
...StructX.defined("config", input.config),
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
/** Type guard: did the user pass an Effect, or a literal array? */
|
|
46
|
+
export const isEffectTree = (
|
|
47
|
+
v: ProjitectFile["blueprints"],
|
|
48
|
+
): v is Effect.Effect<BlueprintTree, Errors.BlueprintError, BlueprintFileSystem> =>
|
|
49
|
+
!Array.isArray(v)
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Dynamic-import the user's `.pjt.ts` and return the typed default export.
|
|
53
|
+
*
|
|
54
|
+
* The user's file is TypeScript. On Node 23.6+ with `--experimental-strip-types`, the import
|
|
55
|
+
* succeeds natively. On older Node, the binary shim in `packages/projitect` registers `tsx` as
|
|
56
|
+
* a loader before reaching this function. Either way, by the time we get here, `import(path)`
|
|
57
|
+
* Just Works.
|
|
58
|
+
*/
|
|
59
|
+
export const loadBlueprintFile = (params: {
|
|
60
|
+
readonly blueprintFile: string
|
|
61
|
+
readonly projectRoot: string
|
|
62
|
+
}): Effect.Effect<
|
|
63
|
+
ProjitectFile,
|
|
64
|
+
Errors.LoaderImportFailed | Errors.LoaderInvalidDefaultExport | Errors.ConfigBlueprintFileNotFound
|
|
65
|
+
> => {
|
|
66
|
+
const { blueprintFile, projectRoot } = params
|
|
67
|
+
const full = path.resolve(projectRoot, blueprintFile)
|
|
68
|
+
return Effect.gen(function* () {
|
|
69
|
+
const exists = yield* Effect.promise(() =>
|
|
70
|
+
fs.access(full).then(
|
|
71
|
+
() => true,
|
|
72
|
+
() => false,
|
|
73
|
+
),
|
|
74
|
+
)
|
|
75
|
+
if (!exists) {
|
|
76
|
+
return yield* new Errors.ConfigBlueprintFileNotFound({
|
|
77
|
+
id: "pjt.config.blueprint-file-not-found",
|
|
78
|
+
blueprintFile,
|
|
79
|
+
message: `No blueprint file found at ${full}. Run \`pjt init\` to create one.`,
|
|
80
|
+
})
|
|
81
|
+
}
|
|
82
|
+
const module_ = yield* Effect.tryPromise({
|
|
83
|
+
try: (): Promise<{ readonly default?: unknown }> => import(pathToFileURL(full).href),
|
|
84
|
+
catch: (e) =>
|
|
85
|
+
new Errors.LoaderImportFailed({
|
|
86
|
+
id: "pjt.loader.import-failed",
|
|
87
|
+
blueprintFile,
|
|
88
|
+
cause: e instanceof Error ? e.message : String(e),
|
|
89
|
+
message: `Failed to import ${blueprintFile}: ${e instanceof Error ? e.message : String(e)}`,
|
|
90
|
+
}),
|
|
91
|
+
})
|
|
92
|
+
const value = module_.default
|
|
93
|
+
if (!isProjitectFile(value)) {
|
|
94
|
+
return yield* new Errors.LoaderInvalidDefaultExport({
|
|
95
|
+
id: "pjt.loader.invalid-default-export",
|
|
96
|
+
blueprintFile,
|
|
97
|
+
received: describe(value),
|
|
98
|
+
message: `${blueprintFile} must \`export default pjt({...})\`. Received ${describe(value)}.`,
|
|
99
|
+
})
|
|
100
|
+
}
|
|
101
|
+
return value
|
|
102
|
+
})
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const isProjitectFile = (v: unknown): v is ProjitectFile =>
|
|
106
|
+
PredicateX.unsafeIsRecord(v) && v["_tag"] === "ProjitectConfig"
|
|
107
|
+
|
|
108
|
+
const describe = (v: unknown): string =>
|
|
109
|
+
Match.value(v).pipe(
|
|
110
|
+
Match.when(Predicate.isUndefined, () => "undefined"),
|
|
111
|
+
Match.when(Predicate.isNull, () => "null"),
|
|
112
|
+
Match.when(Array.isArray, () => "Array"),
|
|
113
|
+
Match.when(PredicateX.unsafeIsRecord, () => 'object without `_tag: "ProjitectConfig"`'),
|
|
114
|
+
Match.orElse((value) => typeof value),
|
|
115
|
+
)
|
package/src/lockfile.ts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { Effect, Schema } from "effect"
|
|
2
|
+
import { BlueprintFileSystem, Errors, PjtLock } from "@projitect/core"
|
|
3
|
+
import { readIfExists } from "./project-file.js"
|
|
4
|
+
|
|
5
|
+
const LOCKFILE_NAME = ".pjt.lock"
|
|
6
|
+
const CURRENT_VERSION = 1 as const
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Read `.pjt.lock` from the project root of the provided `BlueprintFileSystem`. Returns `null` if the file doesn't exist (fresh project).
|
|
10
|
+
* Fails with `pjt.lock.parse-failed` on JSON or schema errors, `pjt.lock.version-mismatch` if
|
|
11
|
+
* the lockfile was written by a newer projitect.
|
|
12
|
+
*/
|
|
13
|
+
export const readLockfile: Effect.Effect<
|
|
14
|
+
PjtLock.PjtLock | null,
|
|
15
|
+
Errors.LockParseFailed | Errors.LockVersionMismatch,
|
|
16
|
+
BlueprintFileSystem
|
|
17
|
+
> = Effect.gen(function* () {
|
|
18
|
+
const file = LOCKFILE_NAME
|
|
19
|
+
const raw = yield* readIfExists(file)
|
|
20
|
+
if (raw === null) {
|
|
21
|
+
return null
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const parsed = yield* Effect.try({
|
|
25
|
+
try: () => JSON.parse(raw) as unknown,
|
|
26
|
+
catch: (e) =>
|
|
27
|
+
new Errors.LockParseFailed({
|
|
28
|
+
id: "pjt.lock.parse-failed",
|
|
29
|
+
path: file,
|
|
30
|
+
cause: e instanceof Error ? e.message : String(e),
|
|
31
|
+
message: `Could not parse ${LOCKFILE_NAME}. The file is corrupted — delete it and rerun \`pjt remodel\` to rebuild.`,
|
|
32
|
+
}),
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
if (
|
|
36
|
+
typeof parsed === "object" &&
|
|
37
|
+
parsed !== null &&
|
|
38
|
+
"version" in parsed &&
|
|
39
|
+
typeof parsed.version === "number" &&
|
|
40
|
+
parsed.version > CURRENT_VERSION
|
|
41
|
+
) {
|
|
42
|
+
return yield* new Errors.LockVersionMismatch({
|
|
43
|
+
id: "pjt.lock.version-mismatch",
|
|
44
|
+
found: parsed.version,
|
|
45
|
+
expected: CURRENT_VERSION,
|
|
46
|
+
message: `${LOCKFILE_NAME} was written by a newer projitect (version ${parsed.version}). Upgrade the \`projitect\` devDep.`,
|
|
47
|
+
})
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return yield* Schema.decodeUnknownEffect(PjtLock.PjtLock)(parsed).pipe(
|
|
51
|
+
Effect.mapError(
|
|
52
|
+
(e) =>
|
|
53
|
+
new Errors.LockParseFailed({
|
|
54
|
+
id: "pjt.lock.parse-failed",
|
|
55
|
+
path: file,
|
|
56
|
+
cause: String(e),
|
|
57
|
+
message: `${LOCKFILE_NAME} does not match the expected schema. Delete it and rerun \`pjt remodel\` to rebuild.`,
|
|
58
|
+
}),
|
|
59
|
+
),
|
|
60
|
+
)
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Write `.pjt.lock` through the provided `BlueprintFileSystem`. Always writes the current schema
|
|
65
|
+
* version.
|
|
66
|
+
*/
|
|
67
|
+
export const writeLockfile = (params: {
|
|
68
|
+
readonly lock: PjtLock.PjtLock
|
|
69
|
+
}): Effect.Effect<void, Errors.FsWriteFailed | Errors.FsPermissionDenied, BlueprintFileSystem> =>
|
|
70
|
+
Effect.gen(function* () {
|
|
71
|
+
const fs = yield* BlueprintFileSystem
|
|
72
|
+
yield* fs.writeFile(LOCKFILE_NAME, `${JSON.stringify(params.lock, null, 2)}\n`)
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Build the set of blueprint ids currently expected to be present from a lockfile. Used by the
|
|
77
|
+
* planner to compute which blueprints have left the tree (their ops will be turned into
|
|
78
|
+
* removals on next apply).
|
|
79
|
+
*/
|
|
80
|
+
export const blueprintIds = (lock: PjtLock.PjtLock | null): ReadonlySet<string> =>
|
|
81
|
+
new Set(lock === null ? [] : Object.keys(lock.blueprints))
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { Array } from "effect"
|
|
2
|
+
import type { Permission } from "@projitect/core"
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Finds the `}` closing the group opened at `open`, and the top-level comma positions between them.
|
|
6
|
+
* Returns `undefined` when the group is unbalanced or has no top-level comma (so `{a}` stays literal).
|
|
7
|
+
*/
|
|
8
|
+
const parseBraceGroup = (
|
|
9
|
+
glob: string,
|
|
10
|
+
open: number,
|
|
11
|
+
): { readonly close: number; readonly commas: readonly number[] } | undefined => {
|
|
12
|
+
const commas: number[] = []
|
|
13
|
+
let depth = 0
|
|
14
|
+
for (let index = open; index < glob.length; index++) {
|
|
15
|
+
const ch = glob.charAt(index)
|
|
16
|
+
if (ch === "{") {
|
|
17
|
+
depth++
|
|
18
|
+
} else if (ch === "}") {
|
|
19
|
+
depth--
|
|
20
|
+
if (depth === 0) {
|
|
21
|
+
return Array.isArrayNonEmpty(commas) ? { close: index, commas } : undefined
|
|
22
|
+
}
|
|
23
|
+
} else if (ch === "," && depth === 1) {
|
|
24
|
+
commas.push(index)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return undefined
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Minimal glob matcher for permission globs. Supports asterisk (any non-slash chars), double
|
|
32
|
+
* asterisk (any chars including slashes) and `{a,b}` alternation (alternatives may be empty or
|
|
33
|
+
* nested). A brace group needs a closing brace and at least one top-level comma; otherwise the
|
|
34
|
+
* braces and commas match literally. No character classes, `?`, negation or extglobs.
|
|
35
|
+
*
|
|
36
|
+
* Examples that match `src/(double-star)/*.{ts,tsx}`: `src/a.ts`, `src/foo/b.tsx`.
|
|
37
|
+
*/
|
|
38
|
+
const globToPattern = (glob: string): string => {
|
|
39
|
+
let pattern = ""
|
|
40
|
+
for (let index = 0; index < glob.length; index++) {
|
|
41
|
+
const ch = glob.charAt(index)
|
|
42
|
+
const group = ch === "{" ? parseBraceGroup(glob, index) : undefined
|
|
43
|
+
if (group !== undefined) {
|
|
44
|
+
const bounds = [index, ...group.commas, group.close]
|
|
45
|
+
const alternatives = Array.zipWith(bounds, bounds.slice(1), (start, end) =>
|
|
46
|
+
globToPattern(glob.slice(start + 1, end)),
|
|
47
|
+
)
|
|
48
|
+
pattern = `${pattern}(?:${alternatives.join("|")})`
|
|
49
|
+
index = group.close
|
|
50
|
+
} else if (ch === "*" && glob.charAt(index + 1) === "*") {
|
|
51
|
+
pattern = `${pattern}.*`
|
|
52
|
+
index++
|
|
53
|
+
} else if (ch === "*") {
|
|
54
|
+
pattern = `${pattern}[^/]*`
|
|
55
|
+
} else if (".+^${}()|[]\\".includes(ch)) {
|
|
56
|
+
pattern = `${pattern}\\${ch}`
|
|
57
|
+
} else {
|
|
58
|
+
pattern = pattern + ch
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return pattern
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const globToRegex = (glob: string): RegExp => new RegExp(`^${globToPattern(glob)}$`, "u")
|
|
65
|
+
|
|
66
|
+
const matchesGlob = (path: string, glob: string): boolean => globToRegex(glob).test(path)
|
|
67
|
+
|
|
68
|
+
export type FsOp = "read" | "write" | "exists" | "remove" | "removeEmptyDir" | "mkdir" | "listDir"
|
|
69
|
+
|
|
70
|
+
const opCategory = (op: FsOp): "read" | "write" =>
|
|
71
|
+
op === "read" || op === "exists" || op === "listDir" ? "read" : "write"
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Returns `true` if the declared permissions cover the requested operation on the given path.
|
|
75
|
+
* `read`-permissioned globs cover `read | exists | listDir`. `write`-permissioned globs cover all
|
|
76
|
+
* write-side operations (`write | remove | removeEmptyDir | mkdir`) **and implicitly** the read operations on the
|
|
77
|
+
* same path — a blueprint that writes a file naturally needs to read it back for region updates.
|
|
78
|
+
*/
|
|
79
|
+
export const isPermitted = (
|
|
80
|
+
permissions: readonly Permission.Permission[],
|
|
81
|
+
op: FsOp,
|
|
82
|
+
path: string,
|
|
83
|
+
): boolean => {
|
|
84
|
+
const category = opCategory(op)
|
|
85
|
+
// A permission grants the op when its glob matches and it's a write (writes imply reads) or
|
|
86
|
+
// it's a read covering a read-category op.
|
|
87
|
+
return Array.some(
|
|
88
|
+
permissions,
|
|
89
|
+
(p) =>
|
|
90
|
+
p.kind !== "exec" && matchesGlob(path, p.glob) && (p.kind === "write" || category === "read"),
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Returns `true` if some declared `exec` permission names `command`.
|
|
96
|
+
*
|
|
97
|
+
* Decision: the permission's `command` matches by **exact string** against the full command line —
|
|
98
|
+
* no glob, no normalisation. A glob such as `pnpm *` would let a blueprint run any subcommand,
|
|
99
|
+
* which defeats declaring the permission; exact matching fails closed, so `pnpm lint:fix` is denied
|
|
100
|
+
* when only `pnpm lint` is declared. Relaxing to globs later is additive (the schema stores a plain
|
|
101
|
+
* string). `read`/`write` permissions never grant exec, and `exec` never grants file access.
|
|
102
|
+
*/
|
|
103
|
+
export const isCommandPermitted = (
|
|
104
|
+
permissions: readonly Permission.Permission[],
|
|
105
|
+
command: string,
|
|
106
|
+
): boolean => Array.some(permissions, (p) => p.kind === "exec" && p.command === command)
|