@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/dist/remover.js
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { Array, Effect, Match, Option, Predicate } from "effect";
|
|
2
|
+
import { PredicateX, RecordX, StringX } from "@nunofyobiz/effect-extras";
|
|
3
|
+
import { BlueprintFileSystem } from "@projitect/core";
|
|
4
|
+
import { readIfExists } from "./project-file.js";
|
|
5
|
+
import { findRegion } from "./region.js";
|
|
6
|
+
/**
|
|
7
|
+
* Apply removal operations through the project `BlueprintFileSystem`. Used by `pjt remodel` / `pjt build` after a blueprint
|
|
8
|
+
* leaves `.pjt.ts` — the operations come from the diff between the previous lockfile and the
|
|
9
|
+
* current blueprint set.
|
|
10
|
+
*
|
|
11
|
+
* Returns the list of paths actually mutated.
|
|
12
|
+
*/
|
|
13
|
+
export const applyRemovals = (params) => Effect.forEach(params.removals, (op) => Effect.map(applyOne(op), (mutated) => (mutated ? Option.some(op.path) : Option.none()))).pipe(Effect.map(Array.getSomes));
|
|
14
|
+
const applyOne = (op) => Match.valueTags(op, {
|
|
15
|
+
Region: (region) => removeRegion(region),
|
|
16
|
+
Merge: (merge) => removeMergeKeys(merge.path, merge.ownedKeys),
|
|
17
|
+
Owned: (owned) => removeFile(owned.path, owned.ownedDir),
|
|
18
|
+
// Seed is write-once; we never delete the file when a seed blueprint leaves.
|
|
19
|
+
Seed: () => Effect.succeed(false),
|
|
20
|
+
});
|
|
21
|
+
const removeRegion = (op) => Effect.gen(function* () {
|
|
22
|
+
const fs = yield* BlueprintFileSystem;
|
|
23
|
+
const current = yield* readIfExists(op.path);
|
|
24
|
+
if (current === null) {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
const found = yield* findRegion({
|
|
28
|
+
fileContent: current,
|
|
29
|
+
ownerId: op.ownerId,
|
|
30
|
+
commentPrefix: op.commentPrefix,
|
|
31
|
+
commentSuffix: op.commentSuffix ?? "",
|
|
32
|
+
path: op.path,
|
|
33
|
+
});
|
|
34
|
+
if (found._tag === "Absent") {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
const next = StringX.replaceLineRange(current, found.startLine, found.endLine, []);
|
|
38
|
+
yield* fs.writeFile(op.path, next);
|
|
39
|
+
return true;
|
|
40
|
+
});
|
|
41
|
+
const removeMergeKeys = (file, keys) => Effect.gen(function* () {
|
|
42
|
+
const fs = yield* BlueprintFileSystem;
|
|
43
|
+
const raw = yield* readIfExists(file);
|
|
44
|
+
if (raw === null) {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
const parsed = yield* Effect.try(() => JSON.parse(raw)).pipe(Effect.option);
|
|
48
|
+
if (Option.isNone(parsed) || !PredicateX.unsafeIsRecord(parsed.value)) {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
// Fold each dotted key out of the tree, tracking whether anything actually changed.
|
|
52
|
+
const seed = {
|
|
53
|
+
tree: parsed.value,
|
|
54
|
+
mutated: false,
|
|
55
|
+
};
|
|
56
|
+
const result = Array.reduce(keys, seed, (state, key) => Option.match(RecordX.deleteByPath(state.tree, key.split(".")), {
|
|
57
|
+
onSome: (tree) => ({ tree, mutated: true }),
|
|
58
|
+
onNone: () => state,
|
|
59
|
+
}));
|
|
60
|
+
if (!result.mutated) {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
yield* fs.writeFile(file, `${JSON.stringify(result.tree, null, 2)}\n`);
|
|
64
|
+
return true;
|
|
65
|
+
});
|
|
66
|
+
const removeFile = (file, ownedDir) => Effect.gen(function* () {
|
|
67
|
+
const fs = yield* BlueprintFileSystem;
|
|
68
|
+
yield* fs.remove(file);
|
|
69
|
+
if (Predicate.isNotUndefined(ownedDir)) {
|
|
70
|
+
yield* pruneEmptyDirs(parentDir(file), ownedDir);
|
|
71
|
+
}
|
|
72
|
+
return true;
|
|
73
|
+
}).pipe(Effect.orElseSucceed(() => false));
|
|
74
|
+
// Project-relative paths use `/`; the parent of a top-level name is the project root (`""`).
|
|
75
|
+
const parentDir = (file) => file.slice(0, Math.max(0, file.lastIndexOf("/")));
|
|
76
|
+
/** Remove empty directories upward from `dir` to `ownedDir` inclusive, never above it. */
|
|
77
|
+
const pruneEmptyDirs = (dir, ownedDir) => Effect.gen(function* () {
|
|
78
|
+
if (dir !== ownedDir && !dir.startsWith(`${ownedDir}/`)) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
const fs = yield* BlueprintFileSystem;
|
|
82
|
+
// Atomic: fails (and stops the climb) if `dir` holds any entry, however recently it appeared.
|
|
83
|
+
yield* fs.removeEmptyDir(dir);
|
|
84
|
+
if (dir !== ownedDir) {
|
|
85
|
+
yield* pruneEmptyDirs(parentDir(dir), ownedDir);
|
|
86
|
+
}
|
|
87
|
+
}).pipe(Effect.orElseSucceed(() => undefined));
|
|
88
|
+
//# sourceMappingURL=remover.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"remover.js","sourceRoot":"","sources":["../src/remover.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,55 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@projitect/cli-internals",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "Engine for the projitect CLI — loader, planner, differ, applier, and command dispatchers.",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/projitect/projitect.git",
|
|
8
|
+
"directory": "packages/cli-internals"
|
|
9
|
+
},
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"type": "module",
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"import": "./dist/index.js"
|
|
16
|
+
},
|
|
17
|
+
"./bin": {
|
|
18
|
+
"types": "./dist/bin.d.ts",
|
|
19
|
+
"import": "./dist/bin.js"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"dist",
|
|
24
|
+
"src",
|
|
25
|
+
"!**/*.tsbuildinfo"
|
|
26
|
+
],
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@nunofyobiz/effect-extras": "3.2.0"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@types/node": "22.19.19",
|
|
32
|
+
"effect": "4.0.0-rc.111",
|
|
33
|
+
"typescript": "5.9.3",
|
|
34
|
+
"@projitect/blueprint": "1.0.1",
|
|
35
|
+
"@projitect/blueprint-gitignore": "1.0.1",
|
|
36
|
+
"@projitect/blueprint-markdown": "1.0.1",
|
|
37
|
+
"@projitect/blueprint-next": "1.0.1",
|
|
38
|
+
"@projitect/blueprint-react": "1.0.1",
|
|
39
|
+
"@projitect/core": "1.0.1",
|
|
40
|
+
"@projitect/test-kit": "1.0.1"
|
|
41
|
+
},
|
|
42
|
+
"peerDependencies": {
|
|
43
|
+
"effect": "4.0.0-rc.111",
|
|
44
|
+
"@projitect/blueprint": "1.0.1",
|
|
45
|
+
"@projitect/core": "1.0.1"
|
|
46
|
+
},
|
|
47
|
+
"engines": {
|
|
48
|
+
"node": ">=22.12.0"
|
|
49
|
+
},
|
|
50
|
+
"scripts": {
|
|
51
|
+
"build": "tsc --build",
|
|
52
|
+
"build:clean": "tsc --build --clean",
|
|
53
|
+
"tc": "tsc --build"
|
|
54
|
+
}
|
|
6
55
|
}
|
package/src/applier.ts
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { Effect, Match } from "effect"
|
|
2
|
+
import { RecordX } from "@nunofyobiz/effect-extras"
|
|
3
|
+
import { BlueprintFileSystem } from "@projitect/core"
|
|
4
|
+
import type { Errors } from "@projitect/core"
|
|
5
|
+
import type { FilePlan, ProjectPlan, RegionPlanFile, MergePlanFile } from "./plan.js"
|
|
6
|
+
import { readIfExists } from "./project-file.js"
|
|
7
|
+
import { findRegion, renderRegion, upsertRegion } from "./region.js"
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Apply a plan through the project `BlueprintFileSystem` (which creates parent directories). Used by `pjt remodel` (non-destructive) and `pjt build` (after wiping
|
|
11
|
+
* the project tree). Returns the list of paths actually written.
|
|
12
|
+
*/
|
|
13
|
+
export const applyPlan = (params: {
|
|
14
|
+
readonly plan: ProjectPlan
|
|
15
|
+
}): Effect.Effect<
|
|
16
|
+
readonly string[],
|
|
17
|
+
| Errors.RegionMissingEnd
|
|
18
|
+
| Errors.RegionDuplicate
|
|
19
|
+
| Errors.FsWriteFailed
|
|
20
|
+
| Errors.FsPermissionDenied,
|
|
21
|
+
BlueprintFileSystem
|
|
22
|
+
> => {
|
|
23
|
+
const { plan } = params
|
|
24
|
+
return Effect.gen(function* () {
|
|
25
|
+
const fs = yield* BlueprintFileSystem
|
|
26
|
+
const written: string[] = []
|
|
27
|
+
for (const file of plan.files) {
|
|
28
|
+
const current = yield* readIfExists(file.path)
|
|
29
|
+
const next = yield* nextContent({ file, current })
|
|
30
|
+
if (next === null) {
|
|
31
|
+
continue
|
|
32
|
+
} // Seed-mode no-op
|
|
33
|
+
if (next === current) {
|
|
34
|
+
continue
|
|
35
|
+
} // Already in sync
|
|
36
|
+
yield* fs.writeFile(file.path, next)
|
|
37
|
+
written.push(file.path)
|
|
38
|
+
}
|
|
39
|
+
return written
|
|
40
|
+
})
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const nextContent = (params: {
|
|
44
|
+
readonly file: FilePlan
|
|
45
|
+
readonly current: string | null
|
|
46
|
+
}): Effect.Effect<string | null, Errors.RegionMissingEnd | Errors.RegionDuplicate> =>
|
|
47
|
+
Match.valueTags(params.file, {
|
|
48
|
+
Region: (file) => nextRegion(file, params.current),
|
|
49
|
+
Merge: (file) => Effect.succeed(nextMerge(file, params.current)),
|
|
50
|
+
Owned: (file) => Effect.succeed(file.content),
|
|
51
|
+
Seed: (file) => Effect.succeed(params.current === null ? file.content : null),
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
const nextRegion = (
|
|
55
|
+
file: RegionPlanFile,
|
|
56
|
+
current: string | null,
|
|
57
|
+
): Effect.Effect<string, Errors.RegionMissingEnd | Errors.RegionDuplicate> =>
|
|
58
|
+
Effect.gen(function* () {
|
|
59
|
+
let content = current ?? ""
|
|
60
|
+
for (const region of file.regions) {
|
|
61
|
+
const found = yield* findRegion({
|
|
62
|
+
fileContent: content,
|
|
63
|
+
ownerId: region.ownerId,
|
|
64
|
+
commentPrefix: file.commentPrefix,
|
|
65
|
+
commentSuffix: file.commentSuffix,
|
|
66
|
+
path: file.path,
|
|
67
|
+
})
|
|
68
|
+
const rendered = renderRegion({
|
|
69
|
+
ownerId: region.ownerId,
|
|
70
|
+
commentPrefix: file.commentPrefix,
|
|
71
|
+
commentSuffix: file.commentSuffix,
|
|
72
|
+
content: region.content,
|
|
73
|
+
})
|
|
74
|
+
content = upsertRegion({ fileContent: content, existing: found, rendered })
|
|
75
|
+
}
|
|
76
|
+
return content
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
const nextMerge = (file: MergePlanFile, current: string | null): string => {
|
|
80
|
+
const existing = current === null ? {} : (safeParseJson(current) ?? {})
|
|
81
|
+
const merged = RecordX.deepMerge(existing, file.value)
|
|
82
|
+
return `${JSON.stringify(merged, null, 2)}\n`
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const safeParseJson = (text: string): unknown => {
|
|
86
|
+
try {
|
|
87
|
+
return JSON.parse(text) as unknown
|
|
88
|
+
} catch {
|
|
89
|
+
return null
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { Effect } from "effect"
|
|
2
|
+
import { Errors } from "@projitect/core"
|
|
3
|
+
import type { ProjitectConfig } from "@projitect/core"
|
|
4
|
+
import { computeSplice, resolveSections } from "@projitect/blueprint"
|
|
5
|
+
import type { ProjitectPackageMetadata, SectionStrategy } from "@projitect/blueprint"
|
|
6
|
+
import { detect, installDev as installDevelopment, readProjitectMetadata } from "../pm.js"
|
|
7
|
+
import { splice } from "../edit-pjt.js"
|
|
8
|
+
|
|
9
|
+
export interface AddResult {
|
|
10
|
+
readonly pm: string
|
|
11
|
+
readonly pkg: string
|
|
12
|
+
readonly metadata: ProjitectPackageMetadata
|
|
13
|
+
readonly sectionsAdded: readonly string[]
|
|
14
|
+
readonly splicedIntoBlueprintFile: boolean
|
|
15
|
+
/** The user picked no sections (or quit the prompt); `.pjt.ts` was left untouched. */
|
|
16
|
+
readonly selectionCancelled: boolean
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* `pjt add <package>` — install a blueprint package and splice it into `.pjt.ts`.
|
|
21
|
+
*
|
|
22
|
+
* Flow:
|
|
23
|
+
* 1. Detect package manager (pnpm / yarn / bun / npm).
|
|
24
|
+
* 2. Shell out `<pm> add -D <package>`.
|
|
25
|
+
* 3. Read the installed package's `package.json` for the `"projitect"` metadata field.
|
|
26
|
+
* - If present and `type: "blueprint-set"`: section selection resolves per `strategy`
|
|
27
|
+
* (all / explicit / ask).
|
|
28
|
+
* - If present and `type: "blueprint"`: a single call line, strategy ignored.
|
|
29
|
+
* - If absent: fails with `pjt.add.metadata-missing`. The package stays installed; the
|
|
30
|
+
* message tells the user to import it in `.pjt.ts` by hand and run `pjt remodel`.
|
|
31
|
+
* 4. Splice the import + call lines into `.pjt.ts` between the convention markers
|
|
32
|
+
* (`pjt:imports` and `pjt:blueprints`).
|
|
33
|
+
*/
|
|
34
|
+
export const add = <R = never>(params: {
|
|
35
|
+
readonly config: ProjitectConfig.ProjitectConfig
|
|
36
|
+
readonly pkg: string
|
|
37
|
+
readonly strategy: SectionStrategy<R>
|
|
38
|
+
}): Effect.Effect<AddResult, Errors.ProjitectError, R> =>
|
|
39
|
+
Effect.gen(function* () {
|
|
40
|
+
const pm = yield* detect({ projectRoot: params.config.projectRoot })
|
|
41
|
+
yield* installDevelopment({ projectRoot: params.config.projectRoot, pm, pkg: params.pkg })
|
|
42
|
+
const metadata = yield* readProjitectMetadata({
|
|
43
|
+
projectRoot: params.config.projectRoot,
|
|
44
|
+
pkg: params.pkg,
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
if (metadata === null) {
|
|
48
|
+
return yield* new Errors.AddMetadataMissing({
|
|
49
|
+
id: "pjt.add.metadata-missing",
|
|
50
|
+
pkg: params.pkg,
|
|
51
|
+
blueprintFile: params.config.blueprintFile,
|
|
52
|
+
message:
|
|
53
|
+
`${params.pkg} was installed but has no \`"projitect"\` metadata in its package.json, so \`pjt add\` can't splice it into ${params.config.blueprintFile}. ` +
|
|
54
|
+
`Import it in ${params.config.blueprintFile} by hand, then run \`pjt remodel\`.`,
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const selection = yield* resolveSections({ metadata, strategy: params.strategy })
|
|
59
|
+
|
|
60
|
+
if (selection._tag === "Nothing") {
|
|
61
|
+
return {
|
|
62
|
+
pm,
|
|
63
|
+
pkg: params.pkg,
|
|
64
|
+
metadata,
|
|
65
|
+
sectionsAdded: [],
|
|
66
|
+
splicedIntoBlueprintFile: false,
|
|
67
|
+
selectionCancelled: true,
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const { importLine, callLines, sectionsAdded } = computeSplice({ metadata, selection })
|
|
72
|
+
|
|
73
|
+
yield* splice({
|
|
74
|
+
projectRoot: params.config.projectRoot,
|
|
75
|
+
blueprintFile: params.config.blueprintFile,
|
|
76
|
+
importLine,
|
|
77
|
+
callLines,
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
pm,
|
|
82
|
+
pkg: params.pkg,
|
|
83
|
+
metadata,
|
|
84
|
+
sectionsAdded,
|
|
85
|
+
splicedIntoBlueprintFile: true,
|
|
86
|
+
selectionCancelled: false,
|
|
87
|
+
}
|
|
88
|
+
})
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs"
|
|
2
|
+
import path from "node:path"
|
|
3
|
+
import { Effect } from "effect"
|
|
4
|
+
import type { Terminal } from "effect"
|
|
5
|
+
import { Prompt } from "effect/unstable/cli"
|
|
6
|
+
import { StructX } from "@nunofyobiz/effect-extras"
|
|
7
|
+
import { Errors } from "@projitect/core"
|
|
8
|
+
import type { ProjitectConfig } from "@projitect/core"
|
|
9
|
+
import { ensureGitRepo, gitStatus } from "../git.js"
|
|
10
|
+
import type { ProcessSpawner } from "../platform/process-runner.js"
|
|
11
|
+
import { remodel } from "./remodel.js"
|
|
12
|
+
import type { RemodelResult } from "./remodel.js"
|
|
13
|
+
|
|
14
|
+
export interface BuildResult {
|
|
15
|
+
/** True when the confirmation prompt was declined, so nothing was wiped, written or run. */
|
|
16
|
+
readonly cancelled: boolean
|
|
17
|
+
readonly wiped: readonly string[]
|
|
18
|
+
readonly remodel: RemodelResult
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Paths that survive `pjt build --force`. Everything else under projectRoot is deleted before
|
|
23
|
+
* the plan re-applies. This intentionally preserves the manifest (`package.json`), the blueprint
|
|
24
|
+
* file, the lockfile, the dependency store, the git history, and PM lockfiles so the project is
|
|
25
|
+
* recoverable.
|
|
26
|
+
*/
|
|
27
|
+
const PRESERVED = new Set<string>([
|
|
28
|
+
".git",
|
|
29
|
+
"node_modules",
|
|
30
|
+
".pjt.ts",
|
|
31
|
+
".pjt.lock",
|
|
32
|
+
"package.json",
|
|
33
|
+
"pnpm-lock.yaml",
|
|
34
|
+
"package-lock.json",
|
|
35
|
+
"yarn.lock",
|
|
36
|
+
"bun.lockb",
|
|
37
|
+
])
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* `pjt build --force` — wipe the project tree (modulo preserved entries) and re-apply the plan
|
|
41
|
+
* from scratch.
|
|
42
|
+
*
|
|
43
|
+
* Safety net:
|
|
44
|
+
* 1. `--force` is required (the dispatcher's flag config enforces this at the CLI layer).
|
|
45
|
+
* 2. The project must be a git repository (`pjt.git.not-a-repo` otherwise).
|
|
46
|
+
* 3. The working tree must be clean (`pjt.apply.dirty-git`), unless `--force-dirty` is set.
|
|
47
|
+
* 4. An interactive confirmation prompt fires unless `--yes` is passed.
|
|
48
|
+
*/
|
|
49
|
+
type BuildError = Errors.ProjitectError | Terminal.QuitError
|
|
50
|
+
type BuildRequirements = Prompt.Environment
|
|
51
|
+
|
|
52
|
+
export const build = (params: {
|
|
53
|
+
readonly config: ProjitectConfig.ProjitectConfig
|
|
54
|
+
readonly force: boolean
|
|
55
|
+
readonly forceDirty: boolean
|
|
56
|
+
readonly yes: boolean
|
|
57
|
+
readonly spawner?: ProcessSpawner
|
|
58
|
+
}): Effect.Effect<BuildResult, BuildError, BuildRequirements> =>
|
|
59
|
+
Effect.gen(function* () {
|
|
60
|
+
if (!params.force) {
|
|
61
|
+
return yield* new Errors.ApplyDirtyGit({
|
|
62
|
+
id: "pjt.apply.dirty-git",
|
|
63
|
+
command: "build",
|
|
64
|
+
message:
|
|
65
|
+
"`pjt build` requires the `--force` flag. This command is destructive — it wipes the project tree before re-applying.",
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
yield* ensureGitRepo({ projectRoot: params.config.projectRoot })
|
|
70
|
+
|
|
71
|
+
if (!params.forceDirty) {
|
|
72
|
+
const status = yield* gitStatus({ projectRoot: params.config.projectRoot })
|
|
73
|
+
if (!status.clean) {
|
|
74
|
+
return yield* new Errors.ApplyDirtyGit({
|
|
75
|
+
id: "pjt.apply.dirty-git",
|
|
76
|
+
command: "build",
|
|
77
|
+
message:
|
|
78
|
+
`Git working tree is dirty (${status.lines.length} uncommitted change${status.lines.length === 1 ? "" : "s"}). ` +
|
|
79
|
+
"Commit your changes (`git add -A && git commit -m wip`) and re-run `pjt build --force`, or pass `--force-dirty` to wipe anyway.",
|
|
80
|
+
})
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (!params.yes) {
|
|
85
|
+
const confirmed = yield* Prompt.confirm({
|
|
86
|
+
message: `This will delete everything under ${params.config.projectRoot} except .git, node_modules, .pjt.ts, .pjt.lock, package.json, and the PM lockfile. Continue?`,
|
|
87
|
+
initial: false,
|
|
88
|
+
})
|
|
89
|
+
if (!confirmed) {
|
|
90
|
+
return {
|
|
91
|
+
cancelled: true,
|
|
92
|
+
wiped: [],
|
|
93
|
+
remodel: { written: [], removed: [], ran: [] },
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const wiped = yield* wipeTree(params.config.projectRoot)
|
|
99
|
+
const remodelResult = yield* remodel({
|
|
100
|
+
config: params.config,
|
|
101
|
+
...StructX.defined("spawner", params.spawner),
|
|
102
|
+
})
|
|
103
|
+
return { cancelled: false, wiped, remodel: remodelResult }
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
const count = (n: number, singular: string, plural: string): string =>
|
|
107
|
+
`${n} ${n === 1 ? singular : plural}`
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* The human summary `pjt build` prints. Cancellation is the declined prompt, not "nothing
|
|
111
|
+
* happened": a build that only ran post-apply commands is a success and lists them.
|
|
112
|
+
*/
|
|
113
|
+
export const renderBuildResult = (result: BuildResult): string => {
|
|
114
|
+
const { cancelled, wiped, remodel: applied } = result
|
|
115
|
+
const lines = cancelled
|
|
116
|
+
? ["Cancelled. No changes made."]
|
|
117
|
+
: [
|
|
118
|
+
...(wiped.length > 0
|
|
119
|
+
? [`Wiped ${count(wiped.length, "entry", "entries")}:`, ...wiped.map((p) => ` ${p}`)]
|
|
120
|
+
: []),
|
|
121
|
+
...(applied.written.length > 0
|
|
122
|
+
? [
|
|
123
|
+
`Rebuilt ${count(applied.written.length, "file", "files")} from the plan:`,
|
|
124
|
+
...applied.written.map((p) => ` ${p}`),
|
|
125
|
+
]
|
|
126
|
+
: []),
|
|
127
|
+
...(applied.ran.length > 0
|
|
128
|
+
? [
|
|
129
|
+
`Ran ${count(applied.ran.length, "command", "commands")}:`,
|
|
130
|
+
...applied.ran.map((c) => ` ${c}`),
|
|
131
|
+
]
|
|
132
|
+
: []),
|
|
133
|
+
]
|
|
134
|
+
return `${lines.join("\n")}\n`
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const wipeTree = (projectRoot: string): Effect.Effect<readonly string[], Errors.FsWriteFailed> =>
|
|
138
|
+
Effect.tryPromise({
|
|
139
|
+
try: async () => {
|
|
140
|
+
const entries = await fs.readdir(projectRoot)
|
|
141
|
+
const toRemove = entries.filter((name) => !PRESERVED.has(name))
|
|
142
|
+
await Promise.all(
|
|
143
|
+
toRemove.map((name) =>
|
|
144
|
+
fs.rm(path.join(projectRoot, name), { recursive: true, force: true }),
|
|
145
|
+
),
|
|
146
|
+
)
|
|
147
|
+
return toRemove
|
|
148
|
+
},
|
|
149
|
+
catch: (e) =>
|
|
150
|
+
new Errors.FsWriteFailed({
|
|
151
|
+
id: "pjt.fs.write-failed",
|
|
152
|
+
path: projectRoot,
|
|
153
|
+
cause: e instanceof Error ? e.message : String(e),
|
|
154
|
+
message: `Failed to wipe project tree at ${projectRoot}`,
|
|
155
|
+
}),
|
|
156
|
+
})
|