@projitect/cli-internals 0.0.0-stage → 1.0.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 (173) hide show
  1. package/LICENSE +21 -0
  2. package/dist/applier.d.ts +12 -0
  3. package/dist/applier.d.ts.map +1 -0
  4. package/dist/applier.js +69 -0
  5. package/dist/applier.js.map +1 -0
  6. package/dist/commands/add.d.ts +34 -0
  7. package/dist/commands/add.d.ts.map +1 -0
  8. package/dist/commands/add.js +64 -0
  9. package/dist/commands/add.js.map +1 -0
  10. package/dist/commands/build.d.ts +39 -0
  11. package/dist/commands/build.d.ts.map +1 -0
  12. package/dist/commands/build.js +108 -0
  13. package/dist/commands/build.js.map +1 -0
  14. package/dist/commands/dispatch.d.ts +5 -0
  15. package/dist/commands/dispatch.d.ts.map +1 -0
  16. package/dist/commands/dispatch.js +211 -0
  17. package/dist/commands/dispatch.js.map +1 -0
  18. package/dist/commands/explain.d.ts +10 -0
  19. package/dist/commands/explain.d.ts.map +1 -0
  20. package/dist/commands/explain.js +46 -0
  21. package/dist/commands/explain.js.map +1 -0
  22. package/dist/commands/init.d.ts +33 -0
  23. package/dist/commands/init.d.ts.map +1 -0
  24. package/dist/commands/init.js +128 -0
  25. package/dist/commands/init.js.map +1 -0
  26. package/dist/commands/inspect-json.d.ts +51 -0
  27. package/dist/commands/inspect-json.d.ts.map +1 -0
  28. package/dist/commands/inspect-json.js +58 -0
  29. package/dist/commands/inspect-json.js.map +1 -0
  30. package/dist/commands/inspect.d.ts +41 -0
  31. package/dist/commands/inspect.d.ts.map +1 -0
  32. package/dist/commands/inspect.js +74 -0
  33. package/dist/commands/inspect.js.map +1 -0
  34. package/dist/commands/remodel.d.ts +22 -0
  35. package/dist/commands/remodel.d.ts.map +1 -0
  36. package/dist/commands/remodel.js +49 -0
  37. package/dist/commands/remodel.js.map +1 -0
  38. package/dist/config-cascade.d.ts +25 -0
  39. package/dist/config-cascade.d.ts.map +1 -0
  40. package/dist/config-cascade.js +45 -0
  41. package/dist/config-cascade.js.map +1 -0
  42. package/dist/differ.d.ts +31 -0
  43. package/dist/differ.d.ts.map +1 -0
  44. package/dist/differ.js +134 -0
  45. package/dist/differ.js.map +1 -0
  46. package/dist/edit-pjt.d.ts +19 -0
  47. package/dist/edit-pjt.d.ts.map +1 -0
  48. package/dist/edit-pjt.js +40 -0
  49. package/dist/edit-pjt.js.map +1 -0
  50. package/dist/filesystem-impl.d.ts +25 -0
  51. package/dist/filesystem-impl.d.ts.map +1 -0
  52. package/dist/filesystem-impl.js +153 -0
  53. package/dist/filesystem-impl.js.map +1 -0
  54. package/dist/git.d.ts +29 -0
  55. package/dist/git.d.ts.map +1 -0
  56. package/dist/git.js +63 -0
  57. package/dist/git.js.map +1 -0
  58. package/dist/index.d.ts +22 -0
  59. package/dist/index.d.ts.map +1 -0
  60. package/dist/index.js +21 -0
  61. package/dist/index.js.map +1 -0
  62. package/dist/ipc/client.d.ts +11 -0
  63. package/dist/ipc/client.d.ts.map +1 -0
  64. package/dist/ipc/client.js +45 -0
  65. package/dist/ipc/client.js.map +1 -0
  66. package/dist/ipc/index.d.ts +5 -0
  67. package/dist/ipc/index.d.ts.map +1 -0
  68. package/dist/ipc/index.js +5 -0
  69. package/dist/ipc/index.js.map +1 -0
  70. package/dist/ipc/protocol.d.ts +251 -0
  71. package/dist/ipc/protocol.d.ts.map +1 -0
  72. package/dist/ipc/protocol.js +41 -0
  73. package/dist/ipc/protocol.js.map +1 -0
  74. package/dist/ipc/server.d.ts +9 -0
  75. package/dist/ipc/server.d.ts.map +1 -0
  76. package/dist/ipc/server.js +23 -0
  77. package/dist/ipc/server.js.map +1 -0
  78. package/dist/ipc/transport.d.ts +12 -0
  79. package/dist/ipc/transport.d.ts.map +1 -0
  80. package/dist/ipc/transport.js +19 -0
  81. package/dist/ipc/transport.js.map +1 -0
  82. package/dist/loader.d.ts +45 -0
  83. package/dist/loader.d.ts.map +1 -0
  84. package/dist/loader.js +65 -0
  85. package/dist/loader.js.map +1 -0
  86. package/dist/lockfile.d.ts +22 -0
  87. package/dist/lockfile.d.ts.map +1 -0
  88. package/dist/lockfile.js +59 -0
  89. package/dist/lockfile.js.map +1 -0
  90. package/dist/permissions.d.ts +20 -0
  91. package/dist/permissions.d.ts.map +1 -0
  92. package/dist/permissions.js +86 -0
  93. package/dist/permissions.js.map +1 -0
  94. package/dist/plan.d.ts +278 -0
  95. package/dist/plan.d.ts.map +1 -0
  96. package/dist/plan.js +212 -0
  97. package/dist/plan.js.map +1 -0
  98. package/dist/platform/file-system.d.ts +3 -0
  99. package/dist/platform/file-system.d.ts.map +1 -0
  100. package/dist/platform/file-system.js +109 -0
  101. package/dist/platform/file-system.js.map +1 -0
  102. package/dist/platform/index.d.ts +17 -0
  103. package/dist/platform/index.d.ts.map +1 -0
  104. package/dist/platform/index.js +20 -0
  105. package/dist/platform/index.js.map +1 -0
  106. package/dist/platform/process-runner.d.ts +41 -0
  107. package/dist/platform/process-runner.d.ts.map +1 -0
  108. package/dist/platform/process-runner.js +120 -0
  109. package/dist/platform/process-runner.js.map +1 -0
  110. package/dist/platform/stdio.d.ts +9 -0
  111. package/dist/platform/stdio.d.ts.map +1 -0
  112. package/dist/platform/stdio.js +11 -0
  113. package/dist/platform/stdio.js.map +1 -0
  114. package/dist/platform/terminal.d.ts +3 -0
  115. package/dist/platform/terminal.d.ts.map +1 -0
  116. package/dist/platform/terminal.js +81 -0
  117. package/dist/platform/terminal.js.map +1 -0
  118. package/dist/pm.d.ts +31 -0
  119. package/dist/pm.d.ts.map +1 -0
  120. package/dist/pm.js +91 -0
  121. package/dist/pm.js.map +1 -0
  122. package/dist/post-apply.d.ts +20 -0
  123. package/dist/post-apply.d.ts.map +1 -0
  124. package/dist/post-apply.js +29 -0
  125. package/dist/post-apply.js.map +1 -0
  126. package/dist/project-file.d.ts +8 -0
  127. package/dist/project-file.d.ts.map +1 -0
  128. package/dist/project-file.js +12 -0
  129. package/dist/project-file.js.map +1 -0
  130. package/dist/region.d.ts +105 -0
  131. package/dist/region.d.ts.map +1 -0
  132. package/dist/region.js +94 -0
  133. package/dist/region.js.map +1 -0
  134. package/dist/remover.d.ts +16 -0
  135. package/dist/remover.d.ts.map +1 -0
  136. package/dist/remover.js +88 -0
  137. package/dist/remover.js.map +1 -0
  138. package/package.json +52 -3
  139. package/src/applier.ts +91 -0
  140. package/src/commands/add.ts +88 -0
  141. package/src/commands/build.ts +156 -0
  142. package/src/commands/dispatch.ts +345 -0
  143. package/src/commands/explain.ts +81 -0
  144. package/src/commands/init.ts +193 -0
  145. package/src/commands/inspect-json.ts +68 -0
  146. package/src/commands/inspect.ts +106 -0
  147. package/src/commands/remodel.ts +72 -0
  148. package/src/config-cascade.ts +60 -0
  149. package/src/differ.ts +188 -0
  150. package/src/edit-pjt.ts +56 -0
  151. package/src/filesystem-impl.ts +230 -0
  152. package/src/git.ts +87 -0
  153. package/src/index.ts +54 -0
  154. package/src/ipc/client.ts +108 -0
  155. package/src/ipc/index.ts +12 -0
  156. package/src/ipc/protocol.ts +48 -0
  157. package/src/ipc/server.ts +62 -0
  158. package/src/ipc/transport.ts +28 -0
  159. package/src/loader.ts +115 -0
  160. package/src/lockfile.ts +81 -0
  161. package/src/permissions.ts +106 -0
  162. package/src/plan.ts +459 -0
  163. package/src/platform/file-system.ts +155 -0
  164. package/src/platform/index.ts +26 -0
  165. package/src/platform/process-runner.ts +150 -0
  166. package/src/platform/stdio.ts +11 -0
  167. package/src/platform/terminal.ts +89 -0
  168. package/src/pm.ts +131 -0
  169. package/src/post-apply.ts +45 -0
  170. package/src/project-file.ts +15 -0
  171. package/src/region.ts +139 -0
  172. package/src/remover.ts +127 -0
  173. 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
+ )
@@ -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)