@projitect/blueprint 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/add-sections.d.ts +87 -0
- package/dist/add-sections.d.ts.map +1 -0
- package/dist/add-sections.js +60 -0
- package/dist/add-sections.js.map +1 -0
- package/dist/constructors.d.ts +103 -0
- package/dist/constructors.d.ts.map +1 -0
- package/dist/constructors.js +88 -0
- package/dist/constructors.js.map +1 -0
- package/dist/detect.d.ts +19 -0
- package/dist/detect.d.ts.map +1 -0
- package/dist/detect.js +36 -0
- package/dist/detect.js.map +1 -0
- package/dist/directory.d.ts +17 -0
- package/dist/directory.d.ts.map +1 -0
- package/dist/directory.js +8 -0
- package/dist/directory.js.map +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +22 -0
- package/dist/index.js.map +1 -0
- package/dist/sections.d.ts +75 -0
- package/dist/sections.d.ts.map +1 -0
- package/dist/sections.js +85 -0
- package/dist/sections.js.map +1 -0
- package/dist/splice.d.ts +23 -0
- package/dist/splice.d.ts.map +1 -0
- package/dist/splice.js +78 -0
- package/dist/splice.js.map +1 -0
- package/package.json +41 -3
- package/src/add-sections.ts +140 -0
- package/src/constructors.ts +202 -0
- package/src/detect.ts +48 -0
- package/src/directory.ts +30 -0
- package/src/index.ts +21 -0
- package/src/sections.ts +99 -0
- package/src/splice.ts +99 -0
- package/README.md +0 -3
package/dist/sections.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Section helpers — thin wrappers around `regionFile` that bake in the right comment
|
|
3
|
+
* delimiters for two common file shapes:
|
|
4
|
+
*
|
|
5
|
+
* - **`markdownSection`** for HTML/MDX/XML files where comments need a closing `-->`.
|
|
6
|
+
* The fenced region renders invisible in markdown output — readers never see the markers.
|
|
7
|
+
* Useful for README.md, AGENTS.md, CONTRIBUTING.md, generated MDX docs, etc.
|
|
8
|
+
*
|
|
9
|
+
* - **`ignoreSection`** for `#`-comment "ignore"-style files. Same shape as the gitignore
|
|
10
|
+
* convention; the helper exists because the name documents intent better than a bare
|
|
11
|
+
* `regionFile({ commentPrefix: "#" })` does, and the surface guides authors toward the
|
|
12
|
+
* right primitive when they're scaffolding a `.eslintignore` / `.prettierignore` /
|
|
13
|
+
* `.dockerignore` / `.npmignore` blueprint.
|
|
14
|
+
*
|
|
15
|
+
* Both helpers re-emit the underlying `RegionFileSpec`-shaped result of `regionFile`, so
|
|
16
|
+
* everything that works for `regionFile` works for these (extra permissions, descriptions,
|
|
17
|
+
* etc.). They aren't shipped as separate npm packages because there's no opinionated content
|
|
18
|
+
* to bundle — the blueprint author always provides their own.
|
|
19
|
+
*/
|
|
20
|
+
import { StructX } from "@nunofyobiz/effect-extras";
|
|
21
|
+
import { regionFile } from "./constructors.js";
|
|
22
|
+
/**
|
|
23
|
+
* Region-mode blueprint for HTML / MDX / XML files. The fence is rendered as two HTML
|
|
24
|
+
* comments on their own lines:
|
|
25
|
+
*
|
|
26
|
+
* <!-- pjt:<owner-id> start -->
|
|
27
|
+
* <body>
|
|
28
|
+
* <!-- pjt:<owner-id> end -->
|
|
29
|
+
*
|
|
30
|
+
* The markers don't render in HTML output (they're comments), so readers of the published
|
|
31
|
+
* file never see them. `pjt inspect` flags hand-edits between the markers; lines outside
|
|
32
|
+
* the fence belong to the user.
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```ts
|
|
36
|
+
* markdownSection({
|
|
37
|
+
* id: "my-org:readme:badges",
|
|
38
|
+
* version: "1.0.0",
|
|
39
|
+
* path: "README.md",
|
|
40
|
+
* content: "\n",
|
|
41
|
+
* })
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
export const markdownSection = (spec) => regionFile({
|
|
45
|
+
id: spec.id,
|
|
46
|
+
version: spec.version,
|
|
47
|
+
...StructX.defined("description", spec.description),
|
|
48
|
+
path: spec.path,
|
|
49
|
+
commentPrefix: "<!--",
|
|
50
|
+
commentSuffix: " -->",
|
|
51
|
+
content: spec.content,
|
|
52
|
+
...StructX.defined("extraPermissions", spec.extraPermissions),
|
|
53
|
+
});
|
|
54
|
+
/**
|
|
55
|
+
* Region-mode blueprint for `#`-comment ignore-style files: `.gitignore`, `.eslintignore`,
|
|
56
|
+
* `.prettierignore`, `.dockerignore`, `.npmignore`, etc. The fence is two `#`-prefixed lines
|
|
57
|
+
* on either side of the section content:
|
|
58
|
+
*
|
|
59
|
+
* # pjt:<owner-id> start
|
|
60
|
+
* <body>
|
|
61
|
+
* # pjt:<owner-id> end
|
|
62
|
+
*
|
|
63
|
+
* Equivalent to `regionFile({ commentPrefix: "#", ... })`, but the name documents the
|
|
64
|
+
* intent and makes the right primitive easy to find.
|
|
65
|
+
*
|
|
66
|
+
* @example
|
|
67
|
+
* ```ts
|
|
68
|
+
* ignoreSection({
|
|
69
|
+
* id: "my-org:eslintignore:generated",
|
|
70
|
+
* version: "1.0.0",
|
|
71
|
+
* path: ".eslintignore",
|
|
72
|
+
* content: "dist/\nbuild/\ncoverage/\n",
|
|
73
|
+
* })
|
|
74
|
+
* ```
|
|
75
|
+
*/
|
|
76
|
+
export const ignoreSection = (spec) => regionFile({
|
|
77
|
+
id: spec.id,
|
|
78
|
+
version: spec.version,
|
|
79
|
+
...StructX.defined("description", spec.description),
|
|
80
|
+
path: spec.path,
|
|
81
|
+
commentPrefix: "#",
|
|
82
|
+
content: spec.content,
|
|
83
|
+
...StructX.defined("extraPermissions", spec.extraPermissions),
|
|
84
|
+
});
|
|
85
|
+
//# sourceMappingURL=sections.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sections.js","sourceRoot":"","sources":["../src/sections.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,2BAA2B,CAAA;AAEnD,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AAW9C;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,IAAiB,EAAuB,EAAE,CACxE,UAAU,CAAC;IACT,EAAE,EAAE,IAAI,CAAC,EAAE;IACX,OAAO,EAAE,IAAI,CAAC,OAAO;IACrB,GAAG,OAAO,CAAC,OAAO,CAAC,aAAa,EAAE,IAAI,CAAC,WAAW,CAAC;IACnD,IAAI,EAAE,IAAI,CAAC,IAAI;IACf,aAAa,EAAE,MAAM;IACrB,aAAa,EAAE,MAAM;IACrB,OAAO,EAAE,IAAI,CAAC,OAAO;IACrB,GAAG,OAAO,CAAC,OAAO,CAAC,kBAAkB,EAAE,IAAI,CAAC,gBAAgB,CAAC;CAC9D,CAAC,CAAA;AAEJ;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,IAAiB,EAAuB,EAAE,CACtE,UAAU,CAAC;IACT,EAAE,EAAE,IAAI,CAAC,EAAE;IACX,OAAO,EAAE,IAAI,CAAC,OAAO;IACrB,GAAG,OAAO,CAAC,OAAO,CAAC,aAAa,EAAE,IAAI,CAAC,WAAW,CAAC;IACnD,IAAI,EAAE,IAAI,CAAC,IAAI;IACf,aAAa,EAAE,GAAG;IAClB,OAAO,EAAE,IAAI,CAAC,OAAO;IACrB,GAAG,OAAO,CAAC,OAAO,CAAC,kBAAkB,EAAE,IAAI,CAAC,gBAAgB,CAAC;CAC9D,CAAC,CAAA"}
|
package/dist/splice.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import { Errors } from "@projitect/core";
|
|
3
|
+
/**
|
|
4
|
+
* Splice an import line and one or more blueprint call lines into `.pjt.ts` text using the
|
|
5
|
+
* convention markers. The import goes between `// pjt:imports start/end` (deduped against
|
|
6
|
+
* existing content), the calls go between `// pjt:blueprints start/end` (appended, with 4-space
|
|
7
|
+
* indent to match the standard template). An empty `callLines` leaves the blueprints block alone.
|
|
8
|
+
*
|
|
9
|
+
* Pure string-to-string: the caller owns reading and writing the file. `blueprintFile` is only
|
|
10
|
+
* used to name the file in the error message.
|
|
11
|
+
*
|
|
12
|
+
* Fails with `pjt.add.markers-missing` if either marker pair is missing — the user has
|
|
13
|
+
* hand-edited the file and we won't guess.
|
|
14
|
+
*/
|
|
15
|
+
export declare const spliceBlueprintFile: (params: {
|
|
16
|
+
readonly content: string;
|
|
17
|
+
readonly blueprintFile: string;
|
|
18
|
+
/** Import statement to splice (one line, no trailing newline). Deduped. */
|
|
19
|
+
readonly importLine: string;
|
|
20
|
+
/** Blueprint call lines (one entry per line, no leading indent, no trailing newline). */
|
|
21
|
+
readonly callLines: readonly string[];
|
|
22
|
+
}) => Effect.Effect<string, Errors.AddMarkersMissing>;
|
|
23
|
+
//# sourceMappingURL=splice.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"splice.d.ts","sourceRoot":"","sources":["../src/splice.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAC/B,OAAO,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAA;AAQxC;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,mBAAmB,GAAI,QAAQ;IAC1C,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAA;IAC9B,2EAA2E;IAC3E,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAC3B,yFAAyF;IACzF,QAAQ,CAAC,SAAS,EAAE,SAAS,MAAM,EAAE,CAAA;CACtC,KAAG,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,iBAAiB,CAY9C,CAAA"}
|
package/dist/splice.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import { Errors } from "@projitect/core";
|
|
3
|
+
import { StringX } from "@nunofyobiz/effect-extras";
|
|
4
|
+
const IMPORTS_START = "// pjt:imports start";
|
|
5
|
+
const IMPORTS_END = "// pjt:imports end";
|
|
6
|
+
const BLUEPRINTS_START = "// pjt:blueprints start";
|
|
7
|
+
const BLUEPRINTS_END = "// pjt:blueprints end";
|
|
8
|
+
/**
|
|
9
|
+
* Splice an import line and one or more blueprint call lines into `.pjt.ts` text using the
|
|
10
|
+
* convention markers. The import goes between `// pjt:imports start/end` (deduped against
|
|
11
|
+
* existing content), the calls go between `// pjt:blueprints start/end` (appended, with 4-space
|
|
12
|
+
* indent to match the standard template). An empty `callLines` leaves the blueprints block alone.
|
|
13
|
+
*
|
|
14
|
+
* Pure string-to-string: the caller owns reading and writing the file. `blueprintFile` is only
|
|
15
|
+
* used to name the file in the error message.
|
|
16
|
+
*
|
|
17
|
+
* Fails with `pjt.add.markers-missing` if either marker pair is missing — the user has
|
|
18
|
+
* hand-edited the file and we won't guess.
|
|
19
|
+
*/
|
|
20
|
+
export const spliceBlueprintFile = (params) => Effect.gen(function* () {
|
|
21
|
+
const afterImport = yield* spliceImport({
|
|
22
|
+
content: params.content,
|
|
23
|
+
blueprintFile: params.blueprintFile,
|
|
24
|
+
importLine: params.importLine,
|
|
25
|
+
});
|
|
26
|
+
return yield* spliceCalls({
|
|
27
|
+
content: afterImport,
|
|
28
|
+
blueprintFile: params.blueprintFile,
|
|
29
|
+
callLines: params.callLines,
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
const spliceImport = (params) => {
|
|
33
|
+
const { content, blueprintFile, importLine } = params;
|
|
34
|
+
return Effect.gen(function* () {
|
|
35
|
+
const lines = content.split("\n");
|
|
36
|
+
const startIndex = lines.findIndex((l) => l.trim() === IMPORTS_START);
|
|
37
|
+
const endIndex = lines.findIndex((l) => l.trim() === IMPORTS_END);
|
|
38
|
+
if (startIndex === -1 || endIndex === -1 || endIndex <= startIndex) {
|
|
39
|
+
return yield* new Errors.AddMarkersMissing({
|
|
40
|
+
id: "pjt.add.markers-missing",
|
|
41
|
+
blueprintFile,
|
|
42
|
+
missingMarker: `${IMPORTS_START} / ${IMPORTS_END}`,
|
|
43
|
+
message: `${blueprintFile} is missing the \`pjt:imports start\` / \`pjt:imports end\` markers. ` +
|
|
44
|
+
"Restore them (see `pjt init`'s starter template) and re-run `pjt add`.",
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
// Dedupe: skip if any line between the markers already equals the import
|
|
48
|
+
const between = lines.slice(startIndex + 1, endIndex).map((l) => l.trim());
|
|
49
|
+
if (between.includes(importLine.trim())) {
|
|
50
|
+
return content;
|
|
51
|
+
}
|
|
52
|
+
return StringX.insertBeforeLine(content, endIndex, [importLine]);
|
|
53
|
+
});
|
|
54
|
+
};
|
|
55
|
+
const spliceCalls = (params) => {
|
|
56
|
+
const { content, blueprintFile, callLines } = params;
|
|
57
|
+
return Effect.gen(function* () {
|
|
58
|
+
if (callLines.length === 0) {
|
|
59
|
+
return content;
|
|
60
|
+
}
|
|
61
|
+
const lines = content.split("\n");
|
|
62
|
+
const startIndex = lines.findIndex((l) => l.trim() === BLUEPRINTS_START);
|
|
63
|
+
const endIndex = lines.findIndex((l) => l.trim() === BLUEPRINTS_END);
|
|
64
|
+
if (startIndex === -1 || endIndex === -1 || endIndex <= startIndex) {
|
|
65
|
+
return yield* new Errors.AddMarkersMissing({
|
|
66
|
+
id: "pjt.add.markers-missing",
|
|
67
|
+
blueprintFile,
|
|
68
|
+
missingMarker: `${BLUEPRINTS_START} / ${BLUEPRINTS_END}`,
|
|
69
|
+
message: `${blueprintFile} is missing the \`pjt:blueprints start\` / \`pjt:blueprints end\` markers. ` +
|
|
70
|
+
"Restore them and re-run `pjt add`.",
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
const indent = " "; // 4-space, matching the starter template
|
|
74
|
+
const newCalls = callLines.map((c) => `${indent}${c.replace(/^\s+/u, "")}`);
|
|
75
|
+
return StringX.insertBeforeLine(content, endIndex, newCalls);
|
|
76
|
+
});
|
|
77
|
+
};
|
|
78
|
+
//# sourceMappingURL=splice.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"splice.js","sourceRoot":"","sources":["../src/splice.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,44 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@projitect/blueprint",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "Authoring SDK for projitect blueprints — BlueprintFileSystem service, region/merge/owned/seed constructors, directory scope, detectors.",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/projitect/projitect.git",
|
|
8
|
+
"directory": "packages/blueprint"
|
|
9
|
+
},
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"type": "module",
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"import": "./dist/index.js"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"dist",
|
|
20
|
+
"src",
|
|
21
|
+
"!**/*.tsbuildinfo"
|
|
22
|
+
],
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@nunofyobiz/effect-extras": "3.2.0"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"effect": "4.0.0-rc.111",
|
|
28
|
+
"typescript": "5.9.3",
|
|
29
|
+
"@projitect/core": "1.0.1",
|
|
30
|
+
"@projitect/test-kit": "1.0.1"
|
|
31
|
+
},
|
|
32
|
+
"peerDependencies": {
|
|
33
|
+
"effect": "4.0.0-rc.111",
|
|
34
|
+
"@projitect/core": "1.0.1"
|
|
35
|
+
},
|
|
36
|
+
"engines": {
|
|
37
|
+
"node": ">=22.12.0"
|
|
38
|
+
},
|
|
39
|
+
"scripts": {
|
|
40
|
+
"build": "tsc --build",
|
|
41
|
+
"build:clean": "tsc --build --clean",
|
|
42
|
+
"tc": "tsc --build"
|
|
43
|
+
}
|
|
6
44
|
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { Array, Effect, Match } from "effect"
|
|
2
|
+
import { Prompt } from "effect/unstable/cli"
|
|
3
|
+
import type { Errors } from "@projitect/core"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The shape of the `"projitect"` field in a blueprint package's `package.json`. Defines how
|
|
7
|
+
* `pjt add <pkg>` should splice into the user's `.pjt.ts`.
|
|
8
|
+
*/
|
|
9
|
+
export interface ProjitectPackageMetadata {
|
|
10
|
+
/**
|
|
11
|
+
* `"blueprint"` for single-call packages (e.g. one default export). `"blueprint-set"` for
|
|
12
|
+
* packages that expose multiple sections (e.g. `gitignores.macOs()`, `gitignores.node()`).
|
|
13
|
+
*/
|
|
14
|
+
readonly type: "blueprint" | "blueprint-set"
|
|
15
|
+
/** The import statement to splice into `// pjt:imports`, e.g. `import { foo } from "foo"`. */
|
|
16
|
+
readonly import: string
|
|
17
|
+
/** For `type: "blueprint"` only — the call template, e.g. `myBlueprint()`. */
|
|
18
|
+
readonly call?: string
|
|
19
|
+
/** For `type: "blueprint-set"` only — substitute `{section}` with each chosen name. */
|
|
20
|
+
readonly callTemplate?: string
|
|
21
|
+
/** For `type: "blueprint-set"` only — the list of available section names. */
|
|
22
|
+
readonly sections?: readonly string[]
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Strategy for picking which sections of a `blueprint-set` package to splice.
|
|
27
|
+
*
|
|
28
|
+
* - `"all"` — silent default; pick every section the package declares. Used when the caller
|
|
29
|
+
* wants the legacy "no --section means everything" behavior.
|
|
30
|
+
* - `{ kind: "explicit", sections }` — caller already knows. `--section macOs,node` lands here.
|
|
31
|
+
* - `{ kind: "ask", choose }` — interactive: defer to the supplied callback (typically
|
|
32
|
+
* `chooseSectionsInteractive`). The callback runs only when the installed package's metadata
|
|
33
|
+
* says `type: "blueprint-set"` and exposes sections.
|
|
34
|
+
*
|
|
35
|
+
* `R` parametrizes the callback's requirement channel so the caller can hand us an Effect
|
|
36
|
+
* that needs `Prompt.Environment` (FileSystem | Path | Terminal).
|
|
37
|
+
*/
|
|
38
|
+
export type SectionStrategy<R = never> =
|
|
39
|
+
| { readonly _tag: "All" }
|
|
40
|
+
| { readonly _tag: "Explicit"; readonly sections: readonly string[] }
|
|
41
|
+
| {
|
|
42
|
+
readonly _tag: "Ask"
|
|
43
|
+
readonly choose: (
|
|
44
|
+
metadata: ProjitectPackageMetadata,
|
|
45
|
+
) => Effect.Effect<readonly string[], Errors.ProjitectError, R>
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* What the user wants spliced from a package. Cancellation is its own case so it can never be
|
|
50
|
+
* mistaken for "all":
|
|
51
|
+
*
|
|
52
|
+
* - `All` — every declared section.
|
|
53
|
+
* - `Chosen` — exactly these sections (non-empty).
|
|
54
|
+
* - `Nothing` — the user picked no sections or quit the prompt; the host must not write.
|
|
55
|
+
*/
|
|
56
|
+
export type SectionSelection =
|
|
57
|
+
| { readonly _tag: "All" }
|
|
58
|
+
| { readonly _tag: "Chosen"; readonly sections: readonly string[] }
|
|
59
|
+
| { readonly _tag: "Nothing" }
|
|
60
|
+
|
|
61
|
+
const chosenOr = (sections: readonly string[], whenEmpty: SectionSelection): SectionSelection =>
|
|
62
|
+
Array.isReadonlyArrayNonEmpty(sections) ? { _tag: "Chosen", sections } : whenEmpty
|
|
63
|
+
|
|
64
|
+
/** Resolve which sections the user wants, per `strategy`. */
|
|
65
|
+
export const resolveSections = <R>(params: {
|
|
66
|
+
readonly metadata: ProjitectPackageMetadata
|
|
67
|
+
readonly strategy: SectionStrategy<R>
|
|
68
|
+
}): Effect.Effect<SectionSelection, Errors.ProjitectError, R> => {
|
|
69
|
+
const { metadata, strategy } = params
|
|
70
|
+
// Section strategy only matters for blueprint-sets. For `type: "blueprint"` the selection is
|
|
71
|
+
// ignored — `computeSplice` emits a single non-templated call line.
|
|
72
|
+
if (metadata.type === "blueprint") {
|
|
73
|
+
return Effect.succeed({ _tag: "All" })
|
|
74
|
+
}
|
|
75
|
+
return Match.valueTags(strategy, {
|
|
76
|
+
All: () => Effect.succeed<SectionSelection>({ _tag: "All" }),
|
|
77
|
+
Explicit: (explicit) => Effect.succeed(chosenOr(explicit.sections, { _tag: "All" })),
|
|
78
|
+
Ask: (ask) =>
|
|
79
|
+
Effect.map(ask.choose(metadata), (picked) => chosenOr(picked, { _tag: "Nothing" })),
|
|
80
|
+
})
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Compute the import line and call lines to splice for the selection. A `Nothing` selection
|
|
85
|
+
* yields no call lines and no sections; hosts check `selection._tag` and skip the write.
|
|
86
|
+
*/
|
|
87
|
+
export const computeSplice = (params: {
|
|
88
|
+
readonly metadata: ProjitectPackageMetadata
|
|
89
|
+
readonly selection: SectionSelection
|
|
90
|
+
}): {
|
|
91
|
+
readonly importLine: string
|
|
92
|
+
readonly callLines: readonly string[]
|
|
93
|
+
readonly sectionsAdded: readonly string[]
|
|
94
|
+
} => {
|
|
95
|
+
const { metadata, selection } = params
|
|
96
|
+
if (metadata.type === "blueprint") {
|
|
97
|
+
const call =
|
|
98
|
+
metadata.call ?? "/* TODO: replace with the blueprint call from the package's README */"
|
|
99
|
+
return {
|
|
100
|
+
importLine: metadata.import,
|
|
101
|
+
callLines: [`${call},`],
|
|
102
|
+
sectionsAdded: [],
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
// Blueprint-set
|
|
106
|
+
const allSections = metadata.sections ?? []
|
|
107
|
+
const chosen = Match.value(selection).pipe(
|
|
108
|
+
Match.tag("All", () => allSections),
|
|
109
|
+
Match.tag("Chosen", (picked) => picked.sections.filter((s) => allSections.includes(s))),
|
|
110
|
+
Match.tag("Nothing", () => []),
|
|
111
|
+
Match.exhaustive,
|
|
112
|
+
)
|
|
113
|
+
const template = metadata.callTemplate ?? "{section}()"
|
|
114
|
+
return {
|
|
115
|
+
importLine: metadata.import,
|
|
116
|
+
callLines: chosen.map((s) => `${template.replace("{section}", s)},`),
|
|
117
|
+
sectionsAdded: chosen,
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Interactive section picker for a `type: "blueprint-set"` package. Ctrl-C yields an empty
|
|
123
|
+
* selection rather than crashing; `resolveSections` maps an empty `Ask` result to
|
|
124
|
+
* `Nothing`, so quitting never splices anything.
|
|
125
|
+
*
|
|
126
|
+
* Returns an empty selection if no sections are declared (defensive default — shouldn't happen
|
|
127
|
+
* for well-formed metadata, but we don't want to crash on a malformed package).
|
|
128
|
+
*/
|
|
129
|
+
export const chooseSectionsInteractive = (
|
|
130
|
+
metadata: ProjitectPackageMetadata,
|
|
131
|
+
): Effect.Effect<readonly string[], never, Prompt.Environment> => {
|
|
132
|
+
const sections = metadata.sections ?? []
|
|
133
|
+
if (!Array.isReadonlyArrayNonEmpty(sections)) {
|
|
134
|
+
return Effect.succeed([])
|
|
135
|
+
}
|
|
136
|
+
return Prompt.multiSelect({
|
|
137
|
+
message: "Pick sections to add (space to toggle, enter to confirm):",
|
|
138
|
+
choices: sections.map((name) => ({ title: name, value: name })),
|
|
139
|
+
}).pipe(Effect.catchTag("QuitError", () => Effect.succeed<readonly string[]>([])))
|
|
140
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { Array, Effect } from "effect"
|
|
2
|
+
import { StructX } from "@nunofyobiz/effect-extras"
|
|
3
|
+
import type { Blueprint, Permission, Errors, BlueprintFileSystem } from "@projitect/core"
|
|
4
|
+
import { ChangeSet } from "@projitect/core"
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Spec for a region-mode blueprint: replace or insert the marked section in a shared text file.
|
|
8
|
+
* Multiple blueprints can coexist in the same file as long as they use different `ownerId`s.
|
|
9
|
+
*/
|
|
10
|
+
export interface RegionFileSpec {
|
|
11
|
+
readonly id: string
|
|
12
|
+
readonly version: string
|
|
13
|
+
readonly description?: string
|
|
14
|
+
readonly path: string
|
|
15
|
+
readonly commentPrefix?: string
|
|
16
|
+
/**
|
|
17
|
+
* Optional closing delimiter for prefix/suffix-pair comment styles. Set to `" -->"` for
|
|
18
|
+
* HTML/MDX. Leave undefined (default) for single-prefix styles like `#` and `//`.
|
|
19
|
+
*/
|
|
20
|
+
readonly commentSuffix?: string
|
|
21
|
+
readonly content: string
|
|
22
|
+
readonly extraPermissions?: readonly Permission.Permission[]
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Create a region-mode blueprint. The blueprint's `id` doubles as the region's `ownerId`.
|
|
27
|
+
* `commentPrefix` defaults to `#` (suitable for `.gitignore` / YAML / shell).
|
|
28
|
+
*
|
|
29
|
+
* For HTML / MDX / XML files, prefer the `markdownSection` helper — it sets the right
|
|
30
|
+
* prefix and suffix pair for you. For ignore-style files, prefer `ignoreSection` — same
|
|
31
|
+
* shape with a name that documents its intent.
|
|
32
|
+
*/
|
|
33
|
+
export const regionFile = (spec: RegionFileSpec): Blueprint.Blueprint => ({
|
|
34
|
+
id: spec.id,
|
|
35
|
+
version: spec.version,
|
|
36
|
+
...StructX.defined("description", spec.description),
|
|
37
|
+
permissions: [
|
|
38
|
+
{ kind: "write", glob: spec.path },
|
|
39
|
+
{ kind: "read", glob: spec.path },
|
|
40
|
+
...(spec.extraPermissions ?? []),
|
|
41
|
+
],
|
|
42
|
+
plan: Effect.succeed(
|
|
43
|
+
ChangeSet.of(
|
|
44
|
+
ChangeSet.RegionOp.make({
|
|
45
|
+
ownerId: spec.id,
|
|
46
|
+
path: spec.path,
|
|
47
|
+
commentPrefix: spec.commentPrefix ?? "#",
|
|
48
|
+
...StructX.defined("commentSuffix", spec.commentSuffix),
|
|
49
|
+
content: spec.content,
|
|
50
|
+
}),
|
|
51
|
+
),
|
|
52
|
+
),
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Spec for a merge-mode blueprint: deep-merge a partial value into a structured (JSON) file.
|
|
57
|
+
* The `ownedKeys` list declares which dotted-path keys this blueprint claims — two blueprints
|
|
58
|
+
* claiming the same key is a `pjt.plan.conflict-merge` error.
|
|
59
|
+
*/
|
|
60
|
+
export interface JsonMergeSpec {
|
|
61
|
+
readonly id: string
|
|
62
|
+
readonly version: string
|
|
63
|
+
readonly description?: string
|
|
64
|
+
readonly path: string
|
|
65
|
+
readonly ownedKeys: readonly string[]
|
|
66
|
+
readonly value: unknown
|
|
67
|
+
readonly extraPermissions?: readonly Permission.Permission[]
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Create a merge-mode blueprint for a JSON file (typically `package.json` / `tsconfig.json`).
|
|
72
|
+
*/
|
|
73
|
+
export const jsonMerge = (spec: JsonMergeSpec): Blueprint.Blueprint => ({
|
|
74
|
+
id: spec.id,
|
|
75
|
+
version: spec.version,
|
|
76
|
+
...StructX.defined("description", spec.description),
|
|
77
|
+
permissions: [
|
|
78
|
+
{ kind: "write", glob: spec.path },
|
|
79
|
+
{ kind: "read", glob: spec.path },
|
|
80
|
+
...(spec.extraPermissions ?? []),
|
|
81
|
+
],
|
|
82
|
+
plan: Effect.succeed(
|
|
83
|
+
ChangeSet.of(
|
|
84
|
+
ChangeSet.MergeOp.make({
|
|
85
|
+
ownerId: spec.id,
|
|
86
|
+
path: spec.path,
|
|
87
|
+
ownedKeys: spec.ownedKeys,
|
|
88
|
+
value: spec.value,
|
|
89
|
+
}),
|
|
90
|
+
),
|
|
91
|
+
),
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Spec for an owned-mode blueprint: this blueprint owns the file's entire content. Any other
|
|
96
|
+
* blueprint touching the same path is a `pjt.plan.conflict-owned` error.
|
|
97
|
+
*/
|
|
98
|
+
export interface OwnFileSpec {
|
|
99
|
+
readonly id: string
|
|
100
|
+
readonly version: string
|
|
101
|
+
readonly description?: string
|
|
102
|
+
readonly path: string
|
|
103
|
+
readonly content: string
|
|
104
|
+
/** Directory the file's owner manages; empty directories are pruned up to it on removal. */
|
|
105
|
+
readonly ownedDir?: string
|
|
106
|
+
readonly extraPermissions?: readonly Permission.Permission[]
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export const ownFile = (spec: OwnFileSpec): Blueprint.Blueprint => ({
|
|
110
|
+
id: spec.id,
|
|
111
|
+
version: spec.version,
|
|
112
|
+
...StructX.defined("description", spec.description),
|
|
113
|
+
permissions: [{ kind: "write", glob: spec.path }, ...(spec.extraPermissions ?? [])],
|
|
114
|
+
plan: Effect.succeed(
|
|
115
|
+
ChangeSet.of(
|
|
116
|
+
ChangeSet.OwnedOp.make({
|
|
117
|
+
ownerId: spec.id,
|
|
118
|
+
path: spec.path,
|
|
119
|
+
content: spec.content,
|
|
120
|
+
...StructX.defined("ownedDir", spec.ownedDir),
|
|
121
|
+
}),
|
|
122
|
+
),
|
|
123
|
+
),
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Spec for a seed-mode blueprint: write the file at first build, never enforce it again. Useful
|
|
128
|
+
* for templates the project maintainer is expected to customize (e.g. README, initial `.pjt.ts`).
|
|
129
|
+
*/
|
|
130
|
+
export interface SeedFileSpec {
|
|
131
|
+
readonly id: string
|
|
132
|
+
readonly version: string
|
|
133
|
+
readonly description?: string
|
|
134
|
+
readonly path: string
|
|
135
|
+
readonly content: string
|
|
136
|
+
readonly extraPermissions?: readonly Permission.Permission[]
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export const seedFile = (spec: SeedFileSpec): Blueprint.Blueprint => ({
|
|
140
|
+
id: spec.id,
|
|
141
|
+
version: spec.version,
|
|
142
|
+
...StructX.defined("description", spec.description),
|
|
143
|
+
permissions: [{ kind: "write", glob: spec.path }, ...(spec.extraPermissions ?? [])],
|
|
144
|
+
plan: Effect.succeed(
|
|
145
|
+
ChangeSet.of(
|
|
146
|
+
ChangeSet.SeedOp.make({
|
|
147
|
+
ownerId: spec.id,
|
|
148
|
+
path: spec.path,
|
|
149
|
+
content: spec.content,
|
|
150
|
+
}),
|
|
151
|
+
),
|
|
152
|
+
),
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Spec for a post-apply command: after every file operation has succeeded, `pjt build` and
|
|
157
|
+
* `pjt remodel` run `fix` from the project root. `check` is the command that reports whether the
|
|
158
|
+
* fix is needed. The blueprint declares an `exec` permission for each of the two command lines.
|
|
159
|
+
*/
|
|
160
|
+
export interface ExecSpec {
|
|
161
|
+
readonly id: string
|
|
162
|
+
readonly version: string
|
|
163
|
+
readonly description?: string
|
|
164
|
+
readonly check: string
|
|
165
|
+
readonly fix: string
|
|
166
|
+
readonly extraPermissions?: readonly Permission.Permission[]
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export const exec = (spec: ExecSpec): Blueprint.Blueprint => ({
|
|
170
|
+
id: spec.id,
|
|
171
|
+
version: spec.version,
|
|
172
|
+
...StructX.defined("description", spec.description),
|
|
173
|
+
permissions: [
|
|
174
|
+
...Array.dedupe([spec.fix, spec.check]).map(
|
|
175
|
+
(command): Permission.Permission => ({ kind: "exec", command }),
|
|
176
|
+
),
|
|
177
|
+
...(spec.extraPermissions ?? []),
|
|
178
|
+
],
|
|
179
|
+
plan: Effect.succeed(
|
|
180
|
+
ChangeSet.of(ChangeSet.ExecOp.make({ ownerId: spec.id, command: spec.fix, check: spec.check })),
|
|
181
|
+
),
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Build a blueprint from a user-supplied plan Effect. Use this when your plan needs to read
|
|
186
|
+
* existing files, branch on detected state, or compose multiple operations.
|
|
187
|
+
*/
|
|
188
|
+
export interface ComputedSpec {
|
|
189
|
+
readonly id: string
|
|
190
|
+
readonly version: string
|
|
191
|
+
readonly description?: string
|
|
192
|
+
readonly permissions: readonly Permission.Permission[]
|
|
193
|
+
readonly plan: Effect.Effect<ChangeSet.ChangeSet, Errors.BlueprintError, BlueprintFileSystem>
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export const computed = (spec: ComputedSpec): Blueprint.Blueprint => ({
|
|
197
|
+
id: spec.id,
|
|
198
|
+
version: spec.version,
|
|
199
|
+
...StructX.defined("description", spec.description),
|
|
200
|
+
permissions: spec.permissions,
|
|
201
|
+
plan: spec.plan,
|
|
202
|
+
})
|
package/src/detect.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Effect } from "effect"
|
|
2
|
+
import { BlueprintFileSystem } from "@projitect/core"
|
|
3
|
+
import type { Errors } from "@projitect/core"
|
|
4
|
+
|
|
5
|
+
export type PackageManager = "pnpm" | "npm" | "yarn" | "bun"
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Inspect lockfiles to determine which package manager the project uses. Reads (in order):
|
|
9
|
+
* `pnpm-lock.yaml`, `bun.lockb`, `yarn.lock`, `package-lock.json`. Falls back to `npm` if none
|
|
10
|
+
* are present.
|
|
11
|
+
*
|
|
12
|
+
* Returns the detected manager; the blueprint must declare `read:` permission on the relevant
|
|
13
|
+
* lockfile path globs (e.g. `pnpm-lock.yaml`).
|
|
14
|
+
*/
|
|
15
|
+
export const detectPackageManager: Effect.Effect<
|
|
16
|
+
PackageManager,
|
|
17
|
+
Errors.FsPermissionDenied | Errors.FsReadFailed,
|
|
18
|
+
BlueprintFileSystem
|
|
19
|
+
> = Effect.gen(function* () {
|
|
20
|
+
const fs = yield* BlueprintFileSystem
|
|
21
|
+
if (yield* fs.exists("pnpm-lock.yaml")) {
|
|
22
|
+
return "pnpm" as const
|
|
23
|
+
}
|
|
24
|
+
if (yield* fs.exists("bun.lockb")) {
|
|
25
|
+
return "bun" as const
|
|
26
|
+
}
|
|
27
|
+
if (yield* fs.exists("yarn.lock")) {
|
|
28
|
+
return "yarn" as const
|
|
29
|
+
}
|
|
30
|
+
if (yield* fs.exists("package-lock.json")) {
|
|
31
|
+
return "npm" as const
|
|
32
|
+
}
|
|
33
|
+
return "npm" as const
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Read and parse `package.json` from the project root. Returns the parsed object as `unknown` —
|
|
38
|
+
* blueprints should decode it with a `Schema` if they need typed access to fields.
|
|
39
|
+
*/
|
|
40
|
+
export const readPackageJson: Effect.Effect<
|
|
41
|
+
unknown,
|
|
42
|
+
Errors.FsReadFailed | Errors.FsPermissionDenied,
|
|
43
|
+
BlueprintFileSystem
|
|
44
|
+
> = Effect.gen(function* () {
|
|
45
|
+
const fs = yield* BlueprintFileSystem
|
|
46
|
+
const raw = yield* fs.readFile("package.json")
|
|
47
|
+
return JSON.parse(raw) as unknown
|
|
48
|
+
})
|
package/src/directory.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Predicate } from "effect"
|
|
2
|
+
import type { Blueprint } from "@projitect/core"
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Scope children to a subdirectory. Inside a `directory(name, [...])` group, every child
|
|
6
|
+
* blueprint's `path` is reinterpreted relative to `name/`, and write permissions outside the
|
|
7
|
+
* subtree are stripped from the children's declared `Permission`s.
|
|
8
|
+
*
|
|
9
|
+
* Implementation note: `directory` returns a `DirectoryBlueprint` — a tagged variant that the
|
|
10
|
+
* planner unpacks. Authors should treat the return value as opaque.
|
|
11
|
+
*/
|
|
12
|
+
export interface DirectoryBlueprint {
|
|
13
|
+
readonly _tag: "Directory"
|
|
14
|
+
readonly name: string
|
|
15
|
+
readonly children: readonly (Blueprint.Blueprint | DirectoryBlueprint)[]
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const directory = (
|
|
19
|
+
name: string,
|
|
20
|
+
children: readonly (Blueprint.Blueprint | DirectoryBlueprint)[],
|
|
21
|
+
): DirectoryBlueprint => ({
|
|
22
|
+
_tag: "Directory",
|
|
23
|
+
name,
|
|
24
|
+
children,
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
export const isDirectoryBlueprint = (
|
|
28
|
+
v: Blueprint.Blueprint | DirectoryBlueprint,
|
|
29
|
+
// `Directory` is the only member of the union carrying a `_tag`, so its presence is decisive.
|
|
30
|
+
): v is DirectoryBlueprint => Predicate.hasProperty(v, "_tag")
|