@intentius/chant 0.81.0 → 0.82.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.
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/workspace/lineage-cli.d.ts +5 -1
- package/dist/workspace/lineage-cli.d.ts.map +1 -1
- package/dist/workspace/lineage-init.d.ts +47 -4
- package/dist/workspace/lineage-init.d.ts.map +1 -1
- package/dist/workspace/lineage-lock.d.ts +15 -2
- package/dist/workspace/lineage-lock.d.ts.map +1 -1
- package/dist/workspace/lineage-upgrade-cli.d.ts +1 -1
- package/dist/workspace/lineage-upgrade.d.ts +8 -2
- package/dist/workspace/lineage-upgrade.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/cli/handlers/init.ts +7 -3
- package/src/cli/main.ts +4 -2
- package/src/workspace/behold-kinds.test.ts +10 -14
- package/src/workspace/lineage-cli.ts +21 -1
- package/src/workspace/lineage-init-dir.test.ts +243 -0
- package/src/workspace/lineage-init.ts +218 -23
- package/src/workspace/lineage-lock.ts +11 -3
- package/src/workspace/lineage-upgrade-cli.ts +2 -2
- package/src/workspace/lineage-upgrade.ts +109 -10
package/dist/cli/main.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../../src/cli/main.ts"],"names":[],"mappings":";AAMA,OAAO,EAAkB,KAAK,UAAU,EAAE,KAAK,UAAU,EAAE,MAAM,YAAY,CAAC;AAiG9E;;GAEG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,UAAU,CA2apD;
|
|
1
|
+
{"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../../src/cli/main.ts"],"names":[],"mappings":";AAMA,OAAO,EAAkB,KAAK,UAAU,EAAE,KAAK,UAAU,EAAE,MAAM,YAAY,CAAC;AAiG9E;;GAEG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,UAAU,CA2apD;AAyjBD;;;;;;;;;;GAUG;AACH,wBAAsB,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAuBzE;AAID;;;;;;;;;GASG;AACH,eAAO,MAAM,eAAe,EAAE,UAAU,EA8HvC,CAAC;AAiKF;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,CAAC,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAuB5E"}
|
|
@@ -9,12 +9,14 @@
|
|
|
9
9
|
* project (#2525 rule 4).
|
|
10
10
|
*/
|
|
11
11
|
import type { CommandContext } from "../cli/registry.js";
|
|
12
|
-
import { type ManualStep } from "./lineage-lock.js";
|
|
12
|
+
import { type LineageSource, type ManualStep } from "./lineage-lock.js";
|
|
13
13
|
export interface LineageScopeView {
|
|
14
14
|
scope: string;
|
|
15
15
|
kind: "template" | "vendor";
|
|
16
16
|
name?: string;
|
|
17
17
|
template: string;
|
|
18
|
+
/** Where the files came from, as the lock records it: its `type` and the repository, directory, lexicon or URL. */
|
|
19
|
+
source: LineageSource;
|
|
18
20
|
ref?: string;
|
|
19
21
|
address: Record<string, unknown> | null;
|
|
20
22
|
files: number;
|
|
@@ -22,6 +24,8 @@ export interface LineageScopeView {
|
|
|
22
24
|
missing: string[];
|
|
23
25
|
manualSteps: ManualStep[];
|
|
24
26
|
}
|
|
27
|
+
/** One line naming a scope's source kind and where it points (#2647). */
|
|
28
|
+
export declare function describeSource(source: LineageSource): string;
|
|
25
29
|
/** The lock as `--json` prints it: one view per scope, with its state in the tree. */
|
|
26
30
|
export declare function lineageView(root: string): {
|
|
27
31
|
lock: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lineage-cli.d.ts","sourceRoot":"","sources":["../../src/workspace/lineage-cli.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAGH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AACtD,OAAO,EAA6E,KAAK,UAAU,EAAE,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"lineage-cli.d.ts","sourceRoot":"","sources":["../../src/workspace/lineage-cli.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAGH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AACtD,OAAO,EAA6E,KAAK,aAAa,EAAE,KAAK,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAIhJ,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,UAAU,GAAG,QAAQ,CAAC;IAC5B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,mHAAmH;IACnH,MAAM,EAAE,aAAa,CAAC;IACtB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IACxC,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,WAAW,EAAE,UAAU,EAAE,CAAC;CAC3B;AAED,yEAAyE;AACzE,wBAAgB,cAAc,CAAC,MAAM,EAAE,aAAa,GAAG,MAAM,CAa5D;AAED,sFAAsF;AACtF,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,gBAAgB,EAAE,CAAA;CAAE,GAAG,IAAI,CAoB7F;AAED,wBAAsB,mBAAmB,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAoC9E"}
|
|
@@ -4,9 +4,14 @@
|
|
|
4
4
|
* with one lineage, scope `"."`, so a project made from a template can be
|
|
5
5
|
* upgraded later without first running adopt-lineage.
|
|
6
6
|
*
|
|
7
|
-
* `--from
|
|
8
|
-
* catalogue (`test/egress-catalogue.ts`). The commit and the tree
|
|
9
|
-
* chosen directory become the content address.
|
|
7
|
+
* `--from <repo>@<ref>` fetches with `git`: the one network step, listed in
|
|
8
|
+
* the egress catalogue (`test/egress-catalogue.ts`). The commit and the tree
|
|
9
|
+
* of the chosen directory become the content address.
|
|
10
|
+
*
|
|
11
|
+
* `--from <dir>[#<member>]` copies a template directory already on disk
|
|
12
|
+
* (#2647), for a host that carries the template as plain files with no
|
|
13
|
+
* `.git` and no way to reach its repository. It reaches no network, and the
|
|
14
|
+
* digest of the copied files is the whole content address.
|
|
10
15
|
*
|
|
11
16
|
* Plain `chant init` without `--template` writes no lock. That keeps the
|
|
12
17
|
* output of an existing command unchanged (#2525 rule 2), and this module
|
|
@@ -59,7 +64,40 @@ export declare function readTemplateTree(scratch: string, commit: string, member
|
|
|
59
64
|
* never copied: it describes the template's lineage, not the new project's.
|
|
60
65
|
*/
|
|
61
66
|
export declare function fetchTemplate(spec: TemplateSpec): FetchedTemplate;
|
|
67
|
+
export interface DirTemplateSpec {
|
|
68
|
+
kind: "dir";
|
|
69
|
+
/** The directory as written, without `#<member>`. */
|
|
70
|
+
path: string;
|
|
71
|
+
/** The directory, absolute. */
|
|
72
|
+
abs: string;
|
|
73
|
+
/** The directory inside it to instantiate (`#<member>`). */
|
|
74
|
+
member?: string;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Parse `<path>[#<member>]` when `<path>` is an existing directory, or return
|
|
78
|
+
* null for the git form. A spec without `@` that names no directory is
|
|
79
|
+
* refused here, since it cannot be a `<repo>@<ref>` either. A directory whose
|
|
80
|
+
* name happens to parse as `<repo>@<ref>` is a directory: what is on disk wins.
|
|
81
|
+
*/
|
|
82
|
+
export declare function parseDirSpec(spec: string, cwd?: string): DirTemplateSpec | null;
|
|
83
|
+
/** The template's directory as `#<member>` names it, relative to its root, for messages. */
|
|
84
|
+
export declare function dirLabel(path: string, member?: string): string;
|
|
85
|
+
/**
|
|
86
|
+
* Read the files of a template directory on disk, the way
|
|
87
|
+
* {@link readTemplateTree} reads a commit: symbolic links, submodules and the
|
|
88
|
+
* template's own lineage lock are skipped with the reason, and so is
|
|
89
|
+
* `node_modules`. Inside a git checkout the files are the ones git would
|
|
90
|
+
* commit, tracked or untracked but not ignored. Elsewhere, or when git is not
|
|
91
|
+
* installed, every file is copied. The executable bit is the owner's.
|
|
92
|
+
*/
|
|
93
|
+
export declare function readTemplateDir(abs: string, member: string | undefined, label: string): Omit<FetchedTemplate, "commit" | "tree">;
|
|
94
|
+
/**
|
|
95
|
+
* How a directory source is recorded: an absolute path as given, a relative
|
|
96
|
+
* one re-expressed from the project, so it resolves from the lock's directory.
|
|
97
|
+
*/
|
|
98
|
+
export declare function recordedDirPath(given: string, abs: string, projectDir: string): string;
|
|
62
99
|
export interface InitFromOptions {
|
|
100
|
+
/** `<repo>@<ref>[#<member>]`, or `<dir>[#<member>]` for a directory on disk (#2647). */
|
|
63
101
|
from: string;
|
|
64
102
|
/** Target directory (defaults to cwd). */
|
|
65
103
|
path?: string;
|
|
@@ -72,12 +110,17 @@ export interface InitFromResult {
|
|
|
72
110
|
createdFiles: string[];
|
|
73
111
|
warnings: string[];
|
|
74
112
|
error?: string;
|
|
113
|
+
/** The git form's spec. */
|
|
75
114
|
spec?: TemplateSpec;
|
|
115
|
+
/** The directory form's spec (#2647). */
|
|
116
|
+
dir?: DirTemplateSpec;
|
|
76
117
|
commit?: string;
|
|
118
|
+
/** The lineage's `template` id. */
|
|
119
|
+
template?: string;
|
|
77
120
|
/** The parameter values used, recorded in the lock. */
|
|
78
121
|
parameters?: Record<string, string>;
|
|
79
122
|
}
|
|
80
|
-
/** `chant init --from <repo>@<ref>[#<member>] [path]`. */
|
|
123
|
+
/** `chant init --from <repo>@<ref>[#<member>] [path]`, or `--from <dir>[#<member>]`. */
|
|
81
124
|
export declare function initFromCommand(options: InitFromOptions): Promise<InitFromResult>;
|
|
82
125
|
export interface TemplateLockInput {
|
|
83
126
|
targetDir: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lineage-init.d.ts","sourceRoot":"","sources":["../../src/workspace/lineage-init.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"lineage-init.d.ts","sourceRoot":"","sources":["../../src/workspace/lineage-init.ts"],"names":[],"mappings":"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"}
|
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
* The lock records where a project's files came from. It holds one lineage per
|
|
5
5
|
* scope, keyed by the scope's directory relative to the lock's root:
|
|
6
6
|
*
|
|
7
|
-
* - a project made by `chant init --from <repo>@<ref
|
|
8
|
-
* `chant init --template <name>` has one
|
|
7
|
+
* - a project made by `chant init --from <repo>@<ref>`,
|
|
8
|
+
* `chant init --from <dir>` or `chant init --template <name>` has one
|
|
9
|
+
* scope, `"."`;
|
|
9
10
|
* - each `chant vendor` target is a scope of kind `vendor` (copied, no
|
|
10
11
|
* parameters), which replaces its entry in `vendor.json` (ws-038).
|
|
11
12
|
*
|
|
@@ -44,6 +45,10 @@ declare const SourceSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
44
45
|
repo: z.ZodString;
|
|
45
46
|
url: z.ZodString;
|
|
46
47
|
path: z.ZodOptional<z.ZodString>;
|
|
48
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
49
|
+
type: z.ZodLiteral<"dir">;
|
|
50
|
+
path: z.ZodString;
|
|
51
|
+
member: z.ZodOptional<z.ZodString>;
|
|
47
52
|
}, z.core.$strict>, z.ZodObject<{
|
|
48
53
|
type: z.ZodLiteral<"lexicon">;
|
|
49
54
|
lexicon: z.ZodString;
|
|
@@ -90,6 +95,10 @@ declare const LineageSchema: z.ZodObject<{
|
|
|
90
95
|
repo: z.ZodString;
|
|
91
96
|
url: z.ZodString;
|
|
92
97
|
path: z.ZodOptional<z.ZodString>;
|
|
98
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
99
|
+
type: z.ZodLiteral<"dir">;
|
|
100
|
+
path: z.ZodString;
|
|
101
|
+
member: z.ZodOptional<z.ZodString>;
|
|
93
102
|
}, z.core.$strict>, z.ZodObject<{
|
|
94
103
|
type: z.ZodLiteral<"lexicon">;
|
|
95
104
|
lexicon: z.ZodString;
|
|
@@ -148,6 +157,10 @@ declare const LockSchema: z.ZodObject<{
|
|
|
148
157
|
repo: z.ZodString;
|
|
149
158
|
url: z.ZodString;
|
|
150
159
|
path: z.ZodOptional<z.ZodString>;
|
|
160
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
161
|
+
type: z.ZodLiteral<"dir">;
|
|
162
|
+
path: z.ZodString;
|
|
163
|
+
member: z.ZodOptional<z.ZodString>;
|
|
151
164
|
}, z.core.$strict>, z.ZodObject<{
|
|
152
165
|
type: z.ZodLiteral<"lexicon">;
|
|
153
166
|
lexicon: z.ZodString;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lineage-lock.d.ts","sourceRoot":"","sources":["../../src/workspace/lineage-lock.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"lineage-lock.d.ts","sourceRoot":"","sources":["../../src/workspace/lineage-lock.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `chant workspace upgrade [<scope>] [--to <ref>] [--allow-code] [--dry-run]
|
|
2
|
+
* `chant workspace upgrade [<scope>] [--to <ref|dir>] [--allow-code] [--dry-run]
|
|
3
3
|
* [--output <file>] [--json]` (#2550).
|
|
4
4
|
*
|
|
5
5
|
* Stages the upgrade in a worktree (see ./lineage-upgrade.ts), then decides
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
* 1. Fetch the target version of the template. For a git source that is one
|
|
10
10
|
* `git fetch` of the target ref, plus the commit the scope was made from.
|
|
11
11
|
* This is the command's only network step, catalogued in
|
|
12
|
-
* `test/egress-catalogue.ts`. A local repository reaches nothing
|
|
12
|
+
* `test/egress-catalogue.ts`. A local repository reaches nothing, and
|
|
13
|
+
* neither does a directory source (#2647), which reads `--to <dir>`.
|
|
13
14
|
* 2. Rebuild the merge base offline: each recorded file's content at the base
|
|
14
15
|
* commit, kept only when it has the hash the lock recorded. A file the
|
|
15
16
|
* project never edited is its own merge base. Vendor sources keep no
|
|
@@ -63,7 +64,12 @@ export interface UpgradeOptions {
|
|
|
63
64
|
root: string;
|
|
64
65
|
/** The scope to upgrade: `"."` or a vendor scope's directory. */
|
|
65
66
|
scope?: string;
|
|
66
|
-
/**
|
|
67
|
+
/**
|
|
68
|
+
* The target ref (git) or label (vendor). Defaults to the ref the scope is
|
|
69
|
+
* pinned at. For a directory source, the directory holding the new version,
|
|
70
|
+
* as `<dir>[#<member>]`, resolved from the current directory; the member
|
|
71
|
+
* defaults to the recorded one.
|
|
72
|
+
*/
|
|
67
73
|
to?: string;
|
|
68
74
|
/** Run migrations whose body is code. */
|
|
69
75
|
allowCode?: boolean;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lineage-upgrade.d.ts","sourceRoot":"","sources":["../../src/workspace/lineage-upgrade.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"lineage-upgrade.d.ts","sourceRoot":"","sources":["../../src/workspace/lineage-upgrade.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAOH,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAGhE,OAAO,EAEL,SAAS,EAOT,KAAK,OAAO,EAGZ,KAAK,UAAU,EAChB,MAAM,gBAAgB,CAAC;AAGxB,OAAO,EAAiB,KAAK,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAGpE,gGAAgG;AAChG,eAAO,MAAM,iBAAiB,sBAAsB,CAAC;AAErD,kEAAkE;AAClE,eAAO,MAAM,WAAW,mBAAmB,CAAC;AAE5C,qBAAa,YAAa,SAAQ,SAAS;IAChC,IAAI,SAAkB;CAChC;AAID,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,OAAO,GAAG,MAAM,GAAG,iBAAiB,CAAC;IAC3C,MAAM,EAAE,QAAQ,GAAG,QAAQ,GAAG,SAAS,CAAC;IACxC,yEAAyE;IACzE,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,yDAAyD;AACzD,MAAM,MAAM,WAAW,GAAG,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,EAAE,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC;AAEpH;;;;GAIG;AACH,eAAO,MAAM,UAAU,EAAE,WAarB,CAAC;AAIL,MAAM,WAAW,cAAc;IAC7B,0DAA0D;IAC1D,IAAI,EAAE,MAAM,CAAC;IACb,iEAAiE;IACjE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;;;OAKG;IACH,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,yCAAyC;IACzC,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,4DAA4D;IAC5D,QAAQ,CAAC,EAAE,WAAW,CAAC;CACxB;AAED,MAAM,WAAW,gBAAgB;IAC/B,iEAAiE;IACjE,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,8DAA8D;IAC9D,QAAQ,EAAE,oBAAoB,CAAC;IAC/B,iCAAiC;IACjC,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC;IACtB,gCAAgC;IAChC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IAClB,MAAM,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAA;KAAE,CAAC;IACvC,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,OAAO,EAAE,YAAY,CAAC,SAAS,CAAC,CAAC;IACjC,WAAW,EAAE,UAAU,EAAE,CAAC;IAC1B,gEAAgE;IAChE,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,KAAK,EAAE,MAAM,CAAC;IACd;;;;OAIG;IACH,IAAI,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,OAAO,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IACvD,6EAA6E;IAC7E,OAAO,EAAE,OAAO,CAAC;IACjB,iHAAiH;IACjH,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,gBAAgB,GAAG,IAAI,CAAC;IACpC,MAAM,EAAE,YAAY,EAAE,CAAC;IACvB,QAAQ,EAAE,OAAO,CAAC;IAClB,qEAAqE;IACrE,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,4DAA4D;IAC5D,QAAQ,EAAE,MAAM,CAAC;IACjB,eAAe,EAAE,MAAM,CAAC;IACxB,+CAA+C;IAC/C,OAAO,IAAI,IAAI,CAAC;CACjB;AA0ND,mGAAmG;AACnG,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEtD;AAyDD;;;;;GAKG;AACH,wBAAsB,YAAY,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,aAAa,CAAC,CAoLlF;AAyBD,2EAA2E;AAC3E,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,aAAa,GAAG,IAAI,CAa9D;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,aAAa,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAiBlG;AAED,sEAAsE;AACtE,wBAAgB,cAAc,CAAC,MAAM,EAAE,aAAa,GAAG,MAAM,EAAE,CAqB9D"}
|
package/package.json
CHANGED
package/src/cli/handlers/init.ts
CHANGED
|
@@ -54,14 +54,15 @@ export async function runInitLexicon(ctx: CommandContext): Promise<number> {
|
|
|
54
54
|
* `chant init --from <repo>@<ref>[#<member>] [--param <name>=<value>]... [path]`
|
|
55
55
|
* (#2540, #2627): copy a template repository at a ref, substitute the
|
|
56
56
|
* parameters its `chant.template.json` declares, and record its lineage.
|
|
57
|
+
* `--from <dir>[#<member>]` copies a template directory on disk instead (#2647).
|
|
57
58
|
* The template brings its own lexicons and configuration, so `--lexicon` and
|
|
58
59
|
* `--template` do not apply.
|
|
59
60
|
*/
|
|
60
61
|
async function runInitFrom(ctx: CommandContext): Promise<number> {
|
|
61
62
|
const { args } = ctx;
|
|
62
|
-
const usage = "Usage: chant init --from <repo>@<ref>[#<member>] [--param <name>=<value>]... [path]";
|
|
63
|
+
const usage = "Usage: chant init --from <repo>@<ref>[#<member>] | <dir>[#<member>] [--param <name>=<value>]... [path]";
|
|
63
64
|
if (!args.migrateFrom) {
|
|
64
|
-
console.error(formatError({ message: "--from needs a template: <repo>@<ref>[#<member>]", hint: usage }));
|
|
65
|
+
console.error(formatError({ message: "--from needs a template: <repo>@<ref>[#<member>] or <dir>[#<member>]", hint: usage }));
|
|
65
66
|
return 1;
|
|
66
67
|
}
|
|
67
68
|
if (args.lexicon || args.template) {
|
|
@@ -95,7 +96,10 @@ async function runInitFrom(ctx: CommandContext): Promise<number> {
|
|
|
95
96
|
console.log(formatSuccess("Created:"));
|
|
96
97
|
for (const file of result.createdFiles) console.log(` ${file}`);
|
|
97
98
|
console.log("");
|
|
98
|
-
|
|
99
|
+
const from = result.spec
|
|
100
|
+
? `${result.spec.id} at ${result.spec.ref} (${result.commit!.slice(0, 12)})`
|
|
101
|
+
: `${result.template} (a directory, recorded by digest only)`;
|
|
102
|
+
console.log(`Lineage: ${from}, recorded in .chant/workspace.lock.json`);
|
|
99
103
|
const parameters = Object.entries(result.parameters ?? {});
|
|
100
104
|
if (parameters.length > 0) console.log(`Parameters: ${parameters.map(([k, v]) => `${k}=${JSON.stringify(v)}`).join(", ")}`);
|
|
101
105
|
return 0;
|
package/src/cli/main.ts
CHANGED
|
@@ -549,7 +549,9 @@ Usage:
|
|
|
549
549
|
Commands:
|
|
550
550
|
init Initialize a new chant project
|
|
551
551
|
(--from <repo>@<ref>[#<member>] copies a template
|
|
552
|
-
repository and records its lineage
|
|
552
|
+
repository and records its lineage, and --from
|
|
553
|
+
<dir>[#<member>] copies a template directory on
|
|
554
|
+
disk; --param
|
|
553
555
|
<name>=<value> sets a parameter the template's
|
|
554
556
|
chant.template.json declares, repeatable)
|
|
555
557
|
init lexicon <name> Scaffold a new lexicon plugin project
|
|
@@ -729,7 +731,7 @@ Workspace (level 1, #2524):
|
|
|
729
731
|
manual steps. Needs no workspace file
|
|
730
732
|
workspace lineage resolve <path>
|
|
731
733
|
Close a manual step once the file is merged by hand
|
|
732
|
-
workspace upgrade [<scope>] [--to <ref>] [--allow-code] [--dry-run] [--output <file>]
|
|
734
|
+
workspace upgrade [<scope>] [--to <ref|dir>] [--allow-code] [--dry-run] [--output <file>]
|
|
733
735
|
Bring a lineage scope to a newer template version: fetch
|
|
734
736
|
it, migrate and merge per file in a worktree, run build,
|
|
735
737
|
lint and workspace check there, then gate on the digest
|
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* behold's two closed member kinds, read through chant's workspace (#2545).
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* package.
|
|
4
|
+
* Both ship from the terraform lexicon's `./workspace-kinds` subpath, since
|
|
5
|
+
* a choudoufu estate is a Terraform root the lexicon's choudoufu mode reads.
|
|
6
|
+
* A workspace that pins the lexicon lists members of either kind through
|
|
7
|
+
* `chant workspace ls --json`, with the kinds behold gave the same
|
|
8
|
+
* directories, and reading the kinds never imports the lexicon.
|
|
10
9
|
*
|
|
11
|
-
* The installed
|
|
12
|
-
*
|
|
10
|
+
* The installed package is the repo's own: its package.json and kinds file
|
|
11
|
+
* are copied as they ship, and every code entry its `exports` names is
|
|
13
12
|
* replaced by a module that leaves a marker file when it is imported.
|
|
14
13
|
*/
|
|
15
14
|
|
|
@@ -25,7 +24,6 @@ import { workingTree } from "./tree";
|
|
|
25
24
|
|
|
26
25
|
const REPO = join(import.meta.dirname, "..", "..", "..", "..");
|
|
27
26
|
const TERRAFORM = join(REPO, "lexicons", "terraform");
|
|
28
|
-
const CHOUDOUFU = join(REPO, "packages", "workspace-kind-choudoufu");
|
|
29
27
|
const FIXTURES = join(TERRAFORM, "src", "__fixtures__");
|
|
30
28
|
|
|
31
29
|
const scratch: string[] = [];
|
|
@@ -65,7 +63,7 @@ function install(root: string, from: string): string {
|
|
|
65
63
|
function workspace(): { root: string; installed: string[] } {
|
|
66
64
|
const root = realpathSync(mkdtempSync(join(tmpdir(), "chant-behold-kinds-")));
|
|
67
65
|
scratch.push(root);
|
|
68
|
-
const installed = [install(root, TERRAFORM)
|
|
66
|
+
const installed = [install(root, TERRAFORM)];
|
|
69
67
|
// Estates in the shapes behold serves: a stock root, a choudoufu root with
|
|
70
68
|
// the sidecar, one with the live block, and a chant project beside .tf files.
|
|
71
69
|
cpSync(join(FIXTURES, "no-backend"), join(root, "estates", "stock"), { recursive: true });
|
|
@@ -82,7 +80,6 @@ function workspace(): { root: string; installed: string[] } {
|
|
|
82
80
|
schema: 1,
|
|
83
81
|
pins: [
|
|
84
82
|
{ package: "@intentius/chant-lexicon-terraform", version: version(TERRAFORM) },
|
|
85
|
-
{ package: "@intentius/workspace-kind-choudoufu", version: version(CHOUDOUFU) },
|
|
86
83
|
],
|
|
87
84
|
members: [
|
|
88
85
|
{ name: "stock", dir: "estates/stock", kind: "terraform" },
|
|
@@ -106,7 +103,7 @@ describe("behold's member kinds from their packages (#2545)", () => {
|
|
|
106
103
|
});
|
|
107
104
|
afterEach(() => vi.restoreAllMocks());
|
|
108
105
|
|
|
109
|
-
test("chant workspace ls --json lists terraform and choudoufu members, readable, and imports
|
|
106
|
+
test("chant workspace ls --json lists terraform and choudoufu members, readable, and never imports the lexicon", async () => {
|
|
110
107
|
const { root, installed } = workspace();
|
|
111
108
|
const code = await runWorkspaceLs({ args: parseArgs(["workspace", "ls", root, "--json"]), plugins: [] } as never);
|
|
112
109
|
expect(code).toBe(0);
|
|
@@ -126,12 +123,11 @@ describe("behold's member kinds from their packages (#2545)", () => {
|
|
|
126
123
|
const { root } = workspace();
|
|
127
124
|
const pins = [
|
|
128
125
|
{ package: "@intentius/chant-lexicon-terraform", version: version(TERRAFORM), path: null },
|
|
129
|
-
{ package: "@intentius/workspace-kind-choudoufu", version: version(CHOUDOUFU), path: null },
|
|
130
126
|
];
|
|
131
127
|
const { registry, problems } = loadKindRegistry(pins, root);
|
|
132
128
|
expect(problems).toEqual([]);
|
|
133
129
|
expect(registry.get("terraform")).toMatchObject({ source: "@intentius/chant-lexicon-terraform", precedence: 400 });
|
|
134
|
-
expect(registry.get("choudoufu")).toMatchObject({ source: "@intentius/
|
|
130
|
+
expect(registry.get("choudoufu")).toMatchObject({ source: "@intentius/chant-lexicon-terraform", precedence: 450 });
|
|
135
131
|
const tree = workingTree(root);
|
|
136
132
|
const winner = (dir: string) => resolveKind(registry, tree, dir).winner?.name;
|
|
137
133
|
expect(winner("estates/stock")).toBe("terraform");
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
import { formatError, formatSuccess } from "../cli/format";
|
|
13
13
|
import type { CommandContext } from "../cli/registry";
|
|
14
|
-
import { LOCK_FILE, LockError, readLock, resolveManualStep, scopeStatus, writeLock, type ManualStep } from "./lineage-lock";
|
|
14
|
+
import { LOCK_FILE, LockError, readLock, resolveManualStep, scopeStatus, writeLock, type LineageSource, type ManualStep } from "./lineage-lock";
|
|
15
15
|
|
|
16
16
|
const USAGE = "chant workspace lineage [--json] | chant workspace lineage resolve <path>";
|
|
17
17
|
|
|
@@ -20,6 +20,8 @@ export interface LineageScopeView {
|
|
|
20
20
|
kind: "template" | "vendor";
|
|
21
21
|
name?: string;
|
|
22
22
|
template: string;
|
|
23
|
+
/** Where the files came from, as the lock records it: its `type` and the repository, directory, lexicon or URL. */
|
|
24
|
+
source: LineageSource;
|
|
23
25
|
ref?: string;
|
|
24
26
|
address: Record<string, unknown> | null;
|
|
25
27
|
files: number;
|
|
@@ -28,6 +30,22 @@ export interface LineageScopeView {
|
|
|
28
30
|
manualSteps: ManualStep[];
|
|
29
31
|
}
|
|
30
32
|
|
|
33
|
+
/** One line naming a scope's source kind and where it points (#2647). */
|
|
34
|
+
export function describeSource(source: LineageSource): string {
|
|
35
|
+
switch (source.type) {
|
|
36
|
+
case "git":
|
|
37
|
+
return `git ${source.repo}${source.path ? `#${source.path}` : ""}`;
|
|
38
|
+
case "dir":
|
|
39
|
+
return `dir ${source.path}${source.member ? `#${source.member}` : ""}`;
|
|
40
|
+
case "lexicon":
|
|
41
|
+
return `lexicon ${source.lexicon}/${source.template}`;
|
|
42
|
+
case "local":
|
|
43
|
+
return `local ${source.path}`;
|
|
44
|
+
case "archive":
|
|
45
|
+
return `archive ${source.url}${source.subpath ? `#${source.subpath}` : ""}`;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
31
49
|
/** The lock as `--json` prints it: one view per scope, with its state in the tree. */
|
|
32
50
|
export function lineageView(root: string): { lock: string; scopes: LineageScopeView[] } | null {
|
|
33
51
|
const lock = readLock(root);
|
|
@@ -39,6 +57,7 @@ export function lineageView(root: string): { lock: string; scopes: LineageScopeV
|
|
|
39
57
|
kind: lineage.kind,
|
|
40
58
|
...(lineage.name !== undefined ? { name: lineage.name } : {}),
|
|
41
59
|
template: lineage.template,
|
|
60
|
+
source: lineage.source,
|
|
42
61
|
...(lineage.ref !== undefined ? { ref: lineage.ref } : {}),
|
|
43
62
|
address: lineage.address,
|
|
44
63
|
files: Object.keys(lineage.files).length,
|
|
@@ -73,6 +92,7 @@ export async function runWorkspaceLineage(ctx: CommandContext): Promise<number>
|
|
|
73
92
|
const pin = s.ref ? `@${s.ref}` : "";
|
|
74
93
|
const at = typeof s.address?.commit === "string" ? ` (${s.address.commit.slice(0, 12)})` : "";
|
|
75
94
|
lines.push(`${s.scope} ${s.kind}${s.name ? ` ${s.name}` : ""} ${s.template}${pin}${at}`);
|
|
95
|
+
lines.push(` source: ${describeSource(s.source)}`);
|
|
76
96
|
lines.push(` ${s.files} file(s), ${s.edited.length} edited, ${s.missing.length} deleted`);
|
|
77
97
|
for (const m of s.manualSteps) lines.push(` manual step: ${s.scope === "." ? "" : `${s.scope}/`}${m.path} (${m.reason})`);
|
|
78
98
|
}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* chant #2647: `chant init --from <dir>[#<member>]`, a template directory on
|
|
3
|
+
* disk. The same files and parameters as the git form for the same tree, a
|
|
4
|
+
* lock whose address is the digest alone, and `chant workspace upgrade`
|
|
5
|
+
* from a directory, which rebuilds its merge base only while the recorded
|
|
6
|
+
* directory still holds what the scope was made from.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
|
10
|
+
import { execFileSync } from "node:child_process";
|
|
11
|
+
import { cpSync, existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, realpathSync, rmSync, statSync, symlinkSync, writeFileSync } from "node:fs";
|
|
12
|
+
import { tmpdir } from "node:os";
|
|
13
|
+
import { dirname, join } from "node:path";
|
|
14
|
+
import { initFromCommand, parseDirSpec, recordedDirPath } from "./lineage-init";
|
|
15
|
+
import { LOCK_FILE, readLock } from "./lineage-lock";
|
|
16
|
+
import { describeSource, lineageView } from "./lineage-cli";
|
|
17
|
+
import { stageUpgrade, type ChantRunner } from "./lineage-upgrade";
|
|
18
|
+
|
|
19
|
+
const ENV = { ...process.env, GIT_AUTHOR_NAME: "t", GIT_AUTHOR_EMAIL: "t@t", GIT_COMMITTER_NAME: "t", GIT_COMMITTER_EMAIL: "t@t" };
|
|
20
|
+
|
|
21
|
+
let root: string;
|
|
22
|
+
|
|
23
|
+
function git(cwd: string, args: string[]): string {
|
|
24
|
+
return execFileSync("git", args, { cwd, encoding: "utf-8", env: ENV, stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
25
|
+
}
|
|
26
|
+
function put(base: string, rel: string, content: string): void {
|
|
27
|
+
mkdirSync(dirname(join(base, rel)), { recursive: true });
|
|
28
|
+
writeFileSync(join(base, rel), content);
|
|
29
|
+
}
|
|
30
|
+
function read(base: string, rel: string): string {
|
|
31
|
+
return readFileSync(join(base, rel), "utf-8");
|
|
32
|
+
}
|
|
33
|
+
/** Every file under `dir` except the lock, with its content and executable bit. */
|
|
34
|
+
function tree(dir: string, prefix = ""): Record<string, string> {
|
|
35
|
+
const out: Record<string, string> = {};
|
|
36
|
+
for (const e of readdirSync(join(dir, prefix), { withFileTypes: true })) {
|
|
37
|
+
const rel = prefix ? `${prefix}/${e.name}` : e.name;
|
|
38
|
+
if (e.isDirectory()) Object.assign(out, tree(dir, rel));
|
|
39
|
+
else if (rel !== LOCK_FILE) out[rel] = `${statSync(join(dir, rel)).mode & 0o100 ? "x " : ""}${readFileSync(join(dir, rel), "utf-8")}`;
|
|
40
|
+
}
|
|
41
|
+
return out;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** A template directory with a manifest, an executable, a seed and a lock of its own. */
|
|
45
|
+
function writeTemplate(dir: string, version = "1"): void {
|
|
46
|
+
put(dir, "svc/README.md", `template v${version}\n`);
|
|
47
|
+
put(dir, "svc/src/main.ts", 'export const name = "{{chant:name}}";\n');
|
|
48
|
+
put(dir, "svc/run.sh", "#!/bin/sh\n");
|
|
49
|
+
execFileSync("chmod", ["+x", join(dir, "svc/run.sh")]);
|
|
50
|
+
put(dir, "svc/.mcp.json", "{}\n");
|
|
51
|
+
put(dir, `svc/${LOCK_FILE}`, JSON.stringify({ lockVersion: 1, scopes: {} }));
|
|
52
|
+
put(
|
|
53
|
+
dir,
|
|
54
|
+
"svc/chant.template.json",
|
|
55
|
+
JSON.stringify({ parameters: { name: { type: "string", default: "app" }, owner: { type: "string", default: "ops" } }, files: ["src/main.ts"] }),
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
beforeEach(() => {
|
|
60
|
+
root = realpathSync(mkdtempSync(join(tmpdir(), "chant-init-dir-test-")));
|
|
61
|
+
vi.spyOn(console, "log").mockImplementation(() => undefined);
|
|
62
|
+
vi.spyOn(console, "error").mockImplementation(() => undefined);
|
|
63
|
+
});
|
|
64
|
+
afterEach(() => {
|
|
65
|
+
vi.restoreAllMocks();
|
|
66
|
+
rmSync(root, { recursive: true, force: true });
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe("parseDirSpec", () => {
|
|
70
|
+
test("an existing directory is the directory form, with or without #<member>", () => {
|
|
71
|
+
mkdirSync(join(root, "tpl", "svc"), { recursive: true });
|
|
72
|
+
expect(parseDirSpec("tpl#svc", root)).toEqual({ kind: "dir", path: "tpl", abs: join(root, "tpl"), member: "svc" });
|
|
73
|
+
expect(parseDirSpec(join(root, "tpl"), root)).toEqual({ kind: "dir", path: join(root, "tpl"), abs: join(root, "tpl") });
|
|
74
|
+
});
|
|
75
|
+
test("a directory whose name parses as <repo>@<ref> is still a directory", () => {
|
|
76
|
+
mkdirSync(join(root, "starter@v1"));
|
|
77
|
+
expect(parseDirSpec("starter@v1", root)).toMatchObject({ kind: "dir", abs: join(root, "starter@v1") });
|
|
78
|
+
});
|
|
79
|
+
test("<repo>@<ref> with no such directory is the git form", () => {
|
|
80
|
+
expect(parseDirSpec("acme/starter@v1.2.0#service", root)).toBeNull();
|
|
81
|
+
});
|
|
82
|
+
test("a path with no @ that is not a directory is refused", () => {
|
|
83
|
+
expect(() => parseDirSpec("missing", root)).toThrow(/no directory missing, and not <repo>@<ref>/);
|
|
84
|
+
put(root, "file", "x");
|
|
85
|
+
expect(() => parseDirSpec("file", root)).toThrow(/file is not a directory/);
|
|
86
|
+
mkdirSync(join(root, "tpl"));
|
|
87
|
+
expect(() => parseDirSpec("tpl#../x", root)).toThrow(/not a directory in the directory/);
|
|
88
|
+
});
|
|
89
|
+
test("a relative directory is recorded from the project, an absolute one as given", () => {
|
|
90
|
+
expect(recordedDirPath("../tpl", join(root, "tpl"), join(root, "proj"))).toBe("../tpl");
|
|
91
|
+
expect(recordedDirPath("tpl", join(root, "tpl"), root)).toBe("./tpl");
|
|
92
|
+
expect(recordedDirPath(join(root, "tpl"), join(root, "tpl"), join(root, "proj"))).toBe(join(root, "tpl"));
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
describe("chant init --from <dir>", () => {
|
|
97
|
+
test("copies the same files, with the same parameters and digest, as the git form of the same tree", async () => {
|
|
98
|
+
const tpl = join(root, "tpl");
|
|
99
|
+
writeTemplate(tpl);
|
|
100
|
+
git(tpl, ["init", "-q", "-b", "main"]);
|
|
101
|
+
git(tpl, ["add", "-A"]);
|
|
102
|
+
git(tpl, ["commit", "-q", "-m", "v1"]);
|
|
103
|
+
|
|
104
|
+
const fromGit = await initFromCommand({ from: `${tpl}@main#svc`, path: join(root, "a"), params: { name: "billing" } });
|
|
105
|
+
const fromDir = await initFromCommand({ from: `${tpl}#svc`, path: join(root, "b"), params: { name: "billing" } });
|
|
106
|
+
expect(fromGit.error).toBeUndefined();
|
|
107
|
+
expect(fromDir.error).toBeUndefined();
|
|
108
|
+
expect(fromDir.createdFiles).toEqual(fromGit.createdFiles);
|
|
109
|
+
expect(tree(join(root, "b"))).toEqual(tree(join(root, "a")));
|
|
110
|
+
expect(read(join(root, "b"), "src/main.ts")).toBe('export const name = "billing";\n');
|
|
111
|
+
expect(statSync(join(root, "b", "run.sh")).mode & 0o111).not.toBe(0);
|
|
112
|
+
expect(existsSync(join(root, "b", "chant.template.json"))).toBe(false);
|
|
113
|
+
|
|
114
|
+
const a = readLock(join(root, "a"))!.scopes["."];
|
|
115
|
+
const b = readLock(join(root, "b"))!.scopes["."];
|
|
116
|
+
expect(b.parameters).toEqual({ name: "billing", owner: "ops" });
|
|
117
|
+
expect(b.parameters).toEqual(a.parameters);
|
|
118
|
+
expect(b.files).toEqual(a.files);
|
|
119
|
+
expect(b.address).toEqual({ digest: a.address!.digest });
|
|
120
|
+
// The lock shape: the digest alone, a dir source, no ref.
|
|
121
|
+
expect(b.source).toEqual({ type: "dir", path: tpl, member: "svc" });
|
|
122
|
+
expect(b.template).toBe(`dir:${tpl}#svc`);
|
|
123
|
+
expect(b.ref).toBeUndefined();
|
|
124
|
+
expect(fromDir.template).toBe(`dir:${tpl}#svc`);
|
|
125
|
+
expect(fromDir.commit).toBeUndefined();
|
|
126
|
+
expect(a.source).toMatchObject({ type: "git", path: "svc" });
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("inside a git checkout, .gitignore is respected and untracked files are copied", async () => {
|
|
130
|
+
const tpl = join(root, "tpl");
|
|
131
|
+
git(root, ["init", "-q", "-b", "main"]);
|
|
132
|
+
put(tpl, "keep.ts", "keep\n");
|
|
133
|
+
put(tpl, ".gitignore", "out/\n*.log\n");
|
|
134
|
+
put(tpl, "out/built.js", "built\n");
|
|
135
|
+
put(tpl, "debug.log", "noise\n");
|
|
136
|
+
git(root, ["add", "tpl/keep.ts", "tpl/.gitignore"]);
|
|
137
|
+
git(root, ["commit", "-q", "-m", "t"]);
|
|
138
|
+
put(tpl, "untracked.ts", "new\n");
|
|
139
|
+
|
|
140
|
+
const result = await initFromCommand({ from: tpl, path: join(root, "proj") });
|
|
141
|
+
expect(result.error).toBeUndefined();
|
|
142
|
+
expect(result.createdFiles.sort()).toEqual([".gitignore", LOCK_FILE, "keep.ts", "untracked.ts"].sort());
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("outside git every file is copied, except node_modules, symbolic links and the template's own lock", async () => {
|
|
146
|
+
const tpl = join(root, "tpl");
|
|
147
|
+
writeTemplate(tpl);
|
|
148
|
+
put(tpl, "svc/.gitignore", "*.log\n");
|
|
149
|
+
put(tpl, "svc/debug.log", "copied: no git to read .gitignore\n");
|
|
150
|
+
put(tpl, "svc/node_modules/dep/index.js", "x\n");
|
|
151
|
+
symlinkSync("README.md", join(tpl, "svc", "link.md"));
|
|
152
|
+
|
|
153
|
+
const result = await initFromCommand({ from: `${tpl}#svc`, path: join(root, "proj") });
|
|
154
|
+
expect(result.error).toBeUndefined();
|
|
155
|
+
expect(result.createdFiles.sort()).toEqual([".gitignore", ".mcp.json", LOCK_FILE, "README.md", "debug.log", "run.sh", "src/main.ts"].sort());
|
|
156
|
+
expect(result.warnings).toEqual(["link.md is a symbolic link, not copied", "node_modules is node_modules, not copied"]);
|
|
157
|
+
expect(readLock(join(root, "proj"))!.scopes["."].files).not.toHaveProperty(LOCK_FILE);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test("refuses a missing member, a missing directory and an undeclared --param, writing nothing", async () => {
|
|
161
|
+
const tpl = join(root, "tpl");
|
|
162
|
+
writeTemplate(tpl);
|
|
163
|
+
const target = join(root, "proj");
|
|
164
|
+
expect((await initFromCommand({ from: `${tpl}#nope`, path: target })).error).toBe(`${tpl} has no directory nope`);
|
|
165
|
+
expect((await initFromCommand({ from: join(root, "missing"), path: target })).error).toMatch(/no directory .*missing, and not <repo>@<ref>/);
|
|
166
|
+
expect((await initFromCommand({ from: `${tpl}#svc`, path: target, params: { title: "x" } })).error).toBe("unknown parameter title (declared: name, owner)");
|
|
167
|
+
expect(existsSync(target)).toBe(false);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test("chant workspace lineage names the source kind and path", async () => {
|
|
171
|
+
const tpl = join(root, "tpl");
|
|
172
|
+
writeTemplate(tpl);
|
|
173
|
+
await initFromCommand({ from: `${tpl}#svc`, path: join(root, "proj") });
|
|
174
|
+
const view = lineageView(join(root, "proj"))!;
|
|
175
|
+
expect(view.scopes[0].source).toEqual({ type: "dir", path: tpl, member: "svc" });
|
|
176
|
+
expect(describeSource(view.scopes[0].source)).toBe(`dir ${tpl}#svc`);
|
|
177
|
+
expect(describeSource({ type: "git", repo: "acme/starter", url: "https://github.com/acme/starter.git", path: "svc" })).toBe("git acme/starter#svc");
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
describe("chant workspace upgrade on a directory scope", () => {
|
|
182
|
+
const passing: ChantRunner = async () => ({ exitCode: 0, output: "" });
|
|
183
|
+
let v1: string;
|
|
184
|
+
let v2: string;
|
|
185
|
+
let proj: string;
|
|
186
|
+
|
|
187
|
+
beforeEach(async () => {
|
|
188
|
+
v1 = join(root, "bundle-v1");
|
|
189
|
+
v2 = join(root, "bundle-v2");
|
|
190
|
+
proj = join(root, "proj");
|
|
191
|
+
writeTemplate(v1);
|
|
192
|
+
put(v1, "svc/src/lib.ts", "one\ntwo\nthree\nfour\nfive\nsix\nseven\n");
|
|
193
|
+
const made = await initFromCommand({ from: `${v1}#svc`, path: proj, params: { name: "billing" } });
|
|
194
|
+
expect(made.error).toBeUndefined();
|
|
195
|
+
git(proj, ["init", "-q", "-b", "main"]);
|
|
196
|
+
put(proj, "src/lib.ts", "ONE (ours)\ntwo\nthree\nfour\nfive\nsix\nseven\n");
|
|
197
|
+
git(proj, ["add", "-A"]);
|
|
198
|
+
git(proj, ["commit", "-q", "-m", "init and edit"]);
|
|
199
|
+
|
|
200
|
+
cpSync(v1, v2, { recursive: true });
|
|
201
|
+
put(v2, "svc/README.md", "template v2\n");
|
|
202
|
+
put(v2, "svc/src/lib.ts", "one\ntwo\nthree\nfour\nfive\nsix\nSEVEN (theirs)\n");
|
|
203
|
+
put(v2, "svc/src/new.ts", "new\n");
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test("without --to it refuses, naming adopt-lineage", async () => {
|
|
207
|
+
await expect(stageUpgrade({ root: proj, runChant: passing })).rejects.toThrow(/no --to directory\. .*has no git history.*chant workspace adopt-lineage/);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test("with --to a directory, the recorded directory is the merge base while it holds the same digest", async () => {
|
|
211
|
+
const staged = await stageUpgrade({ root: proj, to: v2, runChant: passing });
|
|
212
|
+
try {
|
|
213
|
+
expect(staged.merged).toEqual(["src/lib.ts"]);
|
|
214
|
+
expect(staged.written.sort()).toEqual(["README.md", "src/new.ts"]);
|
|
215
|
+
expect(staged.manualSteps).toEqual([]);
|
|
216
|
+
expect(staged.from).toBe(`${v1}#svc`);
|
|
217
|
+
expect(staged.to).toBe(`${v2}#svc`);
|
|
218
|
+
expect(read(staged.worktreeProject, "src/lib.ts")).toBe("ONE (ours)\ntwo\nthree\nfour\nfive\nsix\nSEVEN (theirs)\n");
|
|
219
|
+
// The parameter carried into the new version's files.
|
|
220
|
+
expect(read(staged.worktreeProject, "src/main.ts")).toBe('export const name = "billing";\n');
|
|
221
|
+
const lock = readLock(staged.worktreeProject)!.scopes["."];
|
|
222
|
+
expect(lock.source).toEqual({ type: "dir", path: v2, member: "svc" });
|
|
223
|
+
expect(Object.keys(lock.address!)).toEqual(["digest"]);
|
|
224
|
+
expect(lock.ref).toBeUndefined();
|
|
225
|
+
expect(lock.parameters).toEqual({ name: "billing", owner: "ops" });
|
|
226
|
+
} finally {
|
|
227
|
+
staged.dispose();
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
test("refuses when the recorded directory changed or is gone, naming adopt-lineage", async () => {
|
|
232
|
+
put(v1, "svc/README.md", "replaced in place\n");
|
|
233
|
+
await expect(stageUpgrade({ root: proj, to: v2, runChant: passing })).rejects.toThrow(
|
|
234
|
+
/no longer holds the files the scope was made from.*chant workspace adopt-lineage/,
|
|
235
|
+
);
|
|
236
|
+
rmSync(v1, { recursive: true, force: true });
|
|
237
|
+
await expect(stageUpgrade({ root: proj, to: v2, runChant: passing })).rejects.toThrow(/is gone\. .*chant workspace adopt-lineage/);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
test("--to must be a directory", async () => {
|
|
241
|
+
await expect(stageUpgrade({ root: proj, to: "v2.0.0", runChant: passing })).rejects.toThrow(/--to v2\.0\.0: a scope made from a directory upgrades from a directory/);
|
|
242
|
+
});
|
|
243
|
+
});
|
|
@@ -4,9 +4,14 @@
|
|
|
4
4
|
* with one lineage, scope `"."`, so a project made from a template can be
|
|
5
5
|
* upgraded later without first running adopt-lineage.
|
|
6
6
|
*
|
|
7
|
-
* `--from
|
|
8
|
-
* catalogue (`test/egress-catalogue.ts`). The commit and the tree
|
|
9
|
-
* chosen directory become the content address.
|
|
7
|
+
* `--from <repo>@<ref>` fetches with `git`: the one network step, listed in
|
|
8
|
+
* the egress catalogue (`test/egress-catalogue.ts`). The commit and the tree
|
|
9
|
+
* of the chosen directory become the content address.
|
|
10
|
+
*
|
|
11
|
+
* `--from <dir>[#<member>]` copies a template directory already on disk
|
|
12
|
+
* (#2647), for a host that carries the template as plain files with no
|
|
13
|
+
* `.git` and no way to reach its repository. It reaches no network, and the
|
|
14
|
+
* digest of the copied files is the whole content address.
|
|
10
15
|
*
|
|
11
16
|
* Plain `chant init` without `--template` writes no lock. That keeps the
|
|
12
17
|
* output of an existing command unchanged (#2525 rule 2), and this module
|
|
@@ -14,7 +19,7 @@
|
|
|
14
19
|
*/
|
|
15
20
|
|
|
16
21
|
import { execFileSync } from "node:child_process";
|
|
17
|
-
import { chmodSync, existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
22
|
+
import { chmodSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
18
23
|
import { createRequire } from "node:module";
|
|
19
24
|
import { tmpdir } from "node:os";
|
|
20
25
|
import { dirname, isAbsolute, join, posix, relative, resolve, sep } from "node:path";
|
|
@@ -46,24 +51,30 @@ export interface TemplateSpec {
|
|
|
46
51
|
id: string;
|
|
47
52
|
}
|
|
48
53
|
|
|
49
|
-
/**
|
|
50
|
-
|
|
51
|
-
* `git@host:path`, a local path, or `owner/name` for a GitHub repository.
|
|
52
|
-
*/
|
|
53
|
-
export function parseTemplateSpec(spec: string, cwd: string = process.cwd()): TemplateSpec {
|
|
54
|
+
/** Split `<spec>[#<member>]`, refusing a member that leaves the template. */
|
|
55
|
+
function splitMember(spec: string, where: string): { rest: string; member?: string } {
|
|
54
56
|
let rest = spec.trim();
|
|
55
57
|
let member: string | undefined;
|
|
56
58
|
const hash = rest.lastIndexOf("#");
|
|
57
59
|
if (hash >= 0) {
|
|
58
60
|
member = rest.slice(hash + 1).replace(/^\/+|\/+$/g, "");
|
|
59
61
|
rest = rest.slice(0, hash);
|
|
60
|
-
if (!member || posix.normalize(member).startsWith("..")) throw new LockError(`--from ${spec}: "#${member}" is not a directory in
|
|
62
|
+
if (!member || posix.normalize(member).startsWith("..")) throw new LockError(`--from ${spec}: "#${member}" is not a directory in ${where}`);
|
|
61
63
|
member = posix.normalize(member);
|
|
62
64
|
}
|
|
65
|
+
return { rest, member };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Parse `<repo>@<ref>[#<member>]`. `<repo>` is a git URL, an scp-style
|
|
70
|
+
* `git@host:path`, a local path, or `owner/name` for a GitHub repository.
|
|
71
|
+
*/
|
|
72
|
+
export function parseTemplateSpec(spec: string, cwd: string = process.cwd()): TemplateSpec {
|
|
73
|
+
const { rest, member } = splitMember(spec, "the repository");
|
|
63
74
|
const at = rest.lastIndexOf("@");
|
|
64
75
|
const lastSep = Math.max(rest.lastIndexOf("/"), rest.lastIndexOf(":"));
|
|
65
76
|
if (at <= 0 || at < lastSep || at === rest.length - 1) {
|
|
66
|
-
throw new LockError(`--from ${spec}: expected <repo>@<ref>[#<member>], e.g. acme/starter@v1.2.0`);
|
|
77
|
+
throw new LockError(`--from ${spec}: expected <repo>@<ref>[#<member>], e.g. acme/starter@v1.2.0, or an existing directory`);
|
|
67
78
|
}
|
|
68
79
|
const repo = rest.slice(0, at);
|
|
69
80
|
const ref = rest.slice(at + 1);
|
|
@@ -181,9 +192,162 @@ export function fetchTemplate(spec: TemplateSpec): FetchedTemplate {
|
|
|
181
192
|
}
|
|
182
193
|
}
|
|
183
194
|
|
|
195
|
+
// ── A template directory on disk (#2647) ─────────────────────────────────────
|
|
196
|
+
|
|
197
|
+
export interface DirTemplateSpec {
|
|
198
|
+
kind: "dir";
|
|
199
|
+
/** The directory as written, without `#<member>`. */
|
|
200
|
+
path: string;
|
|
201
|
+
/** The directory, absolute. */
|
|
202
|
+
abs: string;
|
|
203
|
+
/** The directory inside it to instantiate (`#<member>`). */
|
|
204
|
+
member?: string;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Parse `<path>[#<member>]` when `<path>` is an existing directory, or return
|
|
209
|
+
* null for the git form. A spec without `@` that names no directory is
|
|
210
|
+
* refused here, since it cannot be a `<repo>@<ref>` either. A directory whose
|
|
211
|
+
* name happens to parse as `<repo>@<ref>` is a directory: what is on disk wins.
|
|
212
|
+
*/
|
|
213
|
+
export function parseDirSpec(spec: string, cwd: string = process.cwd()): DirTemplateSpec | null {
|
|
214
|
+
const { rest, member } = splitMember(spec, "the directory");
|
|
215
|
+
if (!rest) return null;
|
|
216
|
+
const abs = resolve(cwd, rest);
|
|
217
|
+
let isDir = false;
|
|
218
|
+
try {
|
|
219
|
+
isDir = statSync(abs).isDirectory();
|
|
220
|
+
} catch {
|
|
221
|
+
isDir = false;
|
|
222
|
+
}
|
|
223
|
+
if (isDir) return { kind: "dir", path: rest, abs, ...(member ? { member } : {}) };
|
|
224
|
+
if (rest.includes("@")) return null;
|
|
225
|
+
if (existsSync(abs)) throw new LockError(`--from ${spec}: ${rest} is not a directory`);
|
|
226
|
+
throw new LockError(`--from ${spec}: no directory ${rest}, and not <repo>@<ref>[#<member>] (e.g. acme/starter@v1.2.0)`);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** The template's directory as `#<member>` names it, relative to its root, for messages. */
|
|
230
|
+
export function dirLabel(path: string, member?: string): string {
|
|
231
|
+
return member ? `${path}#${member}` : path;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** The git work tree `dir` sits in, or null when it is in none, git is not installed, or the work tree ignores `dir`. */
|
|
235
|
+
function workTreeOf(dir: string): string | null {
|
|
236
|
+
try {
|
|
237
|
+
if (git(dir, ["rev-parse", "--is-inside-work-tree"]) !== "true") return null;
|
|
238
|
+
const top = git(dir, ["rev-parse", "--show-toplevel"]);
|
|
239
|
+
const rel = relative(top, dir).split(sep).join("/");
|
|
240
|
+
if (rel) {
|
|
241
|
+
try {
|
|
242
|
+
// Exit 0: ignored. A directory the checkout ignores is copied whole.
|
|
243
|
+
git(top, ["check-ignore", "-q", "--", `${rel}/`]);
|
|
244
|
+
return null;
|
|
245
|
+
} catch {
|
|
246
|
+
// Not ignored.
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return top;
|
|
250
|
+
} catch {
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** Every path under `dir`, relative and posix, except `.git`. Symbolic links are listed, not followed. */
|
|
256
|
+
function walk(dir: string, prefix = ""): string[] {
|
|
257
|
+
const out: string[] = [];
|
|
258
|
+
for (const entry of readdirSync(join(dir, prefix), { withFileTypes: true })) {
|
|
259
|
+
const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
260
|
+
if (entry.name === ".git") continue;
|
|
261
|
+
if (entry.isDirectory()) out.push(...walk(dir, rel));
|
|
262
|
+
else out.push(rel);
|
|
263
|
+
}
|
|
264
|
+
return out;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Read the files of a template directory on disk, the way
|
|
269
|
+
* {@link readTemplateTree} reads a commit: symbolic links, submodules and the
|
|
270
|
+
* template's own lineage lock are skipped with the reason, and so is
|
|
271
|
+
* `node_modules`. Inside a git checkout the files are the ones git would
|
|
272
|
+
* commit, tracked or untracked but not ignored. Elsewhere, or when git is not
|
|
273
|
+
* installed, every file is copied. The executable bit is the owner's.
|
|
274
|
+
*/
|
|
275
|
+
export function readTemplateDir(abs: string, member: string | undefined, label: string): Omit<FetchedTemplate, "commit" | "tree"> {
|
|
276
|
+
const dir = member ? join(abs, member) : abs;
|
|
277
|
+
let isDir = false;
|
|
278
|
+
try {
|
|
279
|
+
isDir = statSync(dir).isDirectory();
|
|
280
|
+
} catch {
|
|
281
|
+
isDir = false;
|
|
282
|
+
}
|
|
283
|
+
if (!isDir) throw new LockError(`${label} has no directory ${member ?? ""}`.trimEnd());
|
|
284
|
+
|
|
285
|
+
let paths: string[];
|
|
286
|
+
if (workTreeOf(dir)) {
|
|
287
|
+
const listing = execFileSync("git", ["ls-files", "-z", "--cached", "--others", "--exclude-standard", "--", "."], {
|
|
288
|
+
cwd: dir,
|
|
289
|
+
maxBuffer: 256 * 1024 * 1024,
|
|
290
|
+
}).toString("utf-8");
|
|
291
|
+
paths = [...new Set(listing.split("\0").filter(Boolean))];
|
|
292
|
+
} else {
|
|
293
|
+
paths = walk(dir);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const files: FetchedTemplate["files"] = new Map();
|
|
297
|
+
const skipped: FetchedTemplate["skipped"] = [];
|
|
298
|
+
const skippedModules = new Set<string>();
|
|
299
|
+
for (const path of paths.sort()) {
|
|
300
|
+
const segments = path.split("/");
|
|
301
|
+
const nm = segments.indexOf("node_modules");
|
|
302
|
+
if (nm >= 0) {
|
|
303
|
+
const at = segments.slice(0, nm + 1).join("/");
|
|
304
|
+
if (!skippedModules.has(at)) {
|
|
305
|
+
skippedModules.add(at);
|
|
306
|
+
skipped.push({ path: at, reason: "node_modules" });
|
|
307
|
+
}
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
if (path === LOCK_FILE) {
|
|
311
|
+
skipped.push({ path, reason: "the template's own lineage lock" });
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
let st;
|
|
315
|
+
try {
|
|
316
|
+
st = lstatSync(join(dir, path));
|
|
317
|
+
} catch {
|
|
318
|
+
// Tracked but deleted from the working files: not part of the template on disk.
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
if (st.isSymbolicLink()) {
|
|
322
|
+
skipped.push({ path, reason: "a symbolic link" });
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
if (st.isDirectory()) {
|
|
326
|
+
skipped.push({ path, reason: "a submodule" });
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
if (!st.isFile()) {
|
|
330
|
+
skipped.push({ path, reason: "not a regular file" });
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
files.set(path, { data: readFileSync(join(dir, path)), executable: (st.mode & 0o100) !== 0 });
|
|
334
|
+
}
|
|
335
|
+
return { files, skipped };
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* How a directory source is recorded: an absolute path as given, a relative
|
|
340
|
+
* one re-expressed from the project, so it resolves from the lock's directory.
|
|
341
|
+
*/
|
|
342
|
+
export function recordedDirPath(given: string, abs: string, projectDir: string): string {
|
|
343
|
+
if (isAbsolute(given)) return abs;
|
|
344
|
+
return portableUrl(abs, projectDir);
|
|
345
|
+
}
|
|
346
|
+
|
|
184
347
|
// ── init --from ──────────────────────────────────────────────────────────────
|
|
185
348
|
|
|
186
349
|
export interface InitFromOptions {
|
|
350
|
+
/** `<repo>@<ref>[#<member>]`, or `<dir>[#<member>]` for a directory on disk (#2647). */
|
|
187
351
|
from: string;
|
|
188
352
|
/** Target directory (defaults to cwd). */
|
|
189
353
|
path?: string;
|
|
@@ -197,21 +361,28 @@ export interface InitFromResult {
|
|
|
197
361
|
createdFiles: string[];
|
|
198
362
|
warnings: string[];
|
|
199
363
|
error?: string;
|
|
364
|
+
/** The git form's spec. */
|
|
200
365
|
spec?: TemplateSpec;
|
|
366
|
+
/** The directory form's spec (#2647). */
|
|
367
|
+
dir?: DirTemplateSpec;
|
|
201
368
|
commit?: string;
|
|
369
|
+
/** The lineage's `template` id. */
|
|
370
|
+
template?: string;
|
|
202
371
|
/** The parameter values used, recorded in the lock. */
|
|
203
372
|
parameters?: Record<string, string>;
|
|
204
373
|
}
|
|
205
374
|
|
|
206
|
-
/** `chant init --from <repo>@<ref>[#<member>] [path]`. */
|
|
375
|
+
/** `chant init --from <repo>@<ref>[#<member>] [path]`, or `--from <dir>[#<member>]`. */
|
|
207
376
|
export async function initFromCommand(options: InitFromOptions): Promise<InitFromResult> {
|
|
208
377
|
const targetDir = resolve(options.path ?? ".");
|
|
209
378
|
const warnings: string[] = [];
|
|
210
379
|
const createdFiles: string[] = [];
|
|
211
380
|
|
|
212
|
-
let spec: TemplateSpec;
|
|
381
|
+
let spec: TemplateSpec | undefined;
|
|
382
|
+
let dir: DirTemplateSpec | undefined;
|
|
213
383
|
try {
|
|
214
|
-
|
|
384
|
+
dir = parseDirSpec(options.from) ?? undefined;
|
|
385
|
+
if (!dir) spec = parseTemplateSpec(options.from);
|
|
215
386
|
} catch (err) {
|
|
216
387
|
return { success: false, createdFiles, warnings, error: (err as Error).message };
|
|
217
388
|
}
|
|
@@ -227,9 +398,10 @@ export async function initFromCommand(options: InitFromOptions): Promise<InitFro
|
|
|
227
398
|
return { success: false, createdFiles, warnings, error: `${LOCK_FILE} already exists; this directory already has a lineage` };
|
|
228
399
|
}
|
|
229
400
|
|
|
230
|
-
let fetched: FetchedTemplate;
|
|
401
|
+
let fetched: Omit<FetchedTemplate, "commit" | "tree"> & { commit?: string; tree?: string };
|
|
231
402
|
try {
|
|
232
|
-
|
|
403
|
+
// Every file is read before anything is written, so a target inside the template directory is safe.
|
|
404
|
+
fetched = dir ? readTemplateDir(dir.abs, dir.member, dir.path) : fetchTemplate(spec!);
|
|
233
405
|
} catch (err) {
|
|
234
406
|
return { success: false, createdFiles, warnings, error: (err as Error).message };
|
|
235
407
|
}
|
|
@@ -268,23 +440,46 @@ export async function initFromCommand(options: InitFromOptions): Promise<InitFro
|
|
|
268
440
|
createdFiles.push(path);
|
|
269
441
|
}
|
|
270
442
|
|
|
271
|
-
const
|
|
272
|
-
kind: "template",
|
|
273
|
-
template: spec.id,
|
|
274
|
-
source: { type: "git", repo: spec.repo, url: portableUrl(spec.url, targetDir), ...(spec.member ? { path: spec.member } : {}) },
|
|
275
|
-
ref: spec.ref,
|
|
276
|
-
address: { digest: contentDigest(written), commit: fetched.commit, tree: fetched.tree },
|
|
443
|
+
const common = {
|
|
277
444
|
parameters,
|
|
278
445
|
migrations: [],
|
|
279
446
|
files: fileEntries(written, declaredFilesAt(targetDir)),
|
|
280
447
|
manualSteps: [],
|
|
281
448
|
};
|
|
449
|
+
let lineage: Lineage;
|
|
450
|
+
if (dir) {
|
|
451
|
+
const path = recordedDirPath(dir.path, dir.abs, targetDir);
|
|
452
|
+
lineage = {
|
|
453
|
+
kind: "template",
|
|
454
|
+
template: `dir:${dirLabel(path, dir.member)}`,
|
|
455
|
+
source: { type: "dir", path, ...(dir.member ? { member: dir.member } : {}) },
|
|
456
|
+
address: { digest: contentDigest(written) },
|
|
457
|
+
...common,
|
|
458
|
+
};
|
|
459
|
+
} else {
|
|
460
|
+
lineage = {
|
|
461
|
+
kind: "template",
|
|
462
|
+
template: spec!.id,
|
|
463
|
+
source: { type: "git", repo: spec!.repo, url: portableUrl(spec!.url, targetDir), ...(spec!.member ? { path: spec!.member } : {}) },
|
|
464
|
+
ref: spec!.ref,
|
|
465
|
+
address: { digest: contentDigest(written), commit: fetched.commit, tree: fetched.tree },
|
|
466
|
+
...common,
|
|
467
|
+
};
|
|
468
|
+
}
|
|
282
469
|
const lock = emptyLock();
|
|
283
470
|
lock.scopes["."] = lineage;
|
|
284
471
|
writeLock(targetDir, lock);
|
|
285
472
|
createdFiles.push(LOCK_FILE);
|
|
286
473
|
|
|
287
|
-
return {
|
|
474
|
+
return {
|
|
475
|
+
success: true,
|
|
476
|
+
createdFiles,
|
|
477
|
+
warnings,
|
|
478
|
+
...(spec ? { spec, commit: fetched.commit } : {}),
|
|
479
|
+
...(dir ? { dir } : {}),
|
|
480
|
+
template: lineage.template,
|
|
481
|
+
parameters,
|
|
482
|
+
};
|
|
288
483
|
}
|
|
289
484
|
|
|
290
485
|
/** A local repository is recorded relative to the project, so the lock does not name this machine's paths. */
|
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
* The lock records where a project's files came from. It holds one lineage per
|
|
5
5
|
* scope, keyed by the scope's directory relative to the lock's root:
|
|
6
6
|
*
|
|
7
|
-
* - a project made by `chant init --from <repo>@<ref
|
|
8
|
-
* `chant init --template <name>` has one
|
|
7
|
+
* - a project made by `chant init --from <repo>@<ref>`,
|
|
8
|
+
* `chant init --from <dir>` or `chant init --template <name>` has one
|
|
9
|
+
* scope, `"."`;
|
|
9
10
|
* - each `chant vendor` target is a scope of kind `vendor` (copied, no
|
|
10
11
|
* parameters), which replaces its entry in `vendor.json` (ws-038).
|
|
11
12
|
*
|
|
@@ -65,6 +66,12 @@ const LockFileEntrySchema = z
|
|
|
65
66
|
const SourceSchema = z.discriminatedUnion("type", [
|
|
66
67
|
/** A git repository at a ref, optionally one directory of it (`#<member>`). */
|
|
67
68
|
z.object({ type: z.literal("git"), repo: z.string().min(1), url: z.string().min(1), path: z.string().optional() }).strict(),
|
|
69
|
+
/**
|
|
70
|
+
* A directory on disk (#2647): `path` as given when absolute, otherwise
|
|
71
|
+
* relative to the lock's directory, and `member` for `#<member>`. It has no
|
|
72
|
+
* history, so its address is the digest alone.
|
|
73
|
+
*/
|
|
74
|
+
z.object({ type: z.literal("dir"), path: z.string().min(1), member: z.string().optional() }).strict(),
|
|
68
75
|
/** A lexicon's `initTemplates`, as `chant init --lexicon <lexicon> --template <template>` renders them. */
|
|
69
76
|
z.object({ type: z.literal("lexicon"), lexicon: z.string().min(1), template: z.string().min(1) }).strict(),
|
|
70
77
|
/** `chant vendor` sources, unchanged from `vendor.json`. */
|
|
@@ -78,7 +85,8 @@ export type LineageSource = z.infer<typeof SourceSchema>;
|
|
|
78
85
|
* `digest` is always present: the content hash of the file set as chant wrote
|
|
79
86
|
* it (the same hash `vendor.json` called `checksum`). A git source adds the
|
|
80
87
|
* commit and the tree of the scope's directory; a lexicon template adds the
|
|
81
|
-
* package and chant versions that rendered it.
|
|
88
|
+
* package and chant versions that rendered it. A directory source has the
|
|
89
|
+
* digest only.
|
|
82
90
|
*/
|
|
83
91
|
const AddressSchema = z
|
|
84
92
|
.object({
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `chant workspace upgrade [<scope>] [--to <ref>] [--allow-code] [--dry-run]
|
|
2
|
+
* `chant workspace upgrade [<scope>] [--to <ref|dir>] [--allow-code] [--dry-run]
|
|
3
3
|
* [--output <file>] [--json]` (#2550).
|
|
4
4
|
*
|
|
5
5
|
* Stages the upgrade in a worktree (see ./lineage-upgrade.ts), then decides
|
|
@@ -20,7 +20,7 @@ import { WORKSPACE_UPGRADE_GATE_OP } from "../op/gate-name";
|
|
|
20
20
|
import { LockError } from "./lineage-lock";
|
|
21
21
|
import { applyStagedUpgrade, describeStaged, stageUpgrade, type ChantRunner, type StagedUpgrade } from "./lineage-upgrade";
|
|
22
22
|
|
|
23
|
-
const USAGE = "chant workspace upgrade [<scope>] [--to <ref>] [--allow-code] [--dry-run] [--output <patch file>] [--json]";
|
|
23
|
+
const USAGE = "chant workspace upgrade [<scope>] [--to <ref|dir>] [--allow-code] [--dry-run] [--output <patch file>] [--json]";
|
|
24
24
|
|
|
25
25
|
/** The exit code of a gated upgrade: the same as `chant run`'s gated run. */
|
|
26
26
|
export const UPGRADE_GATED_EXIT = 3;
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
* 1. Fetch the target version of the template. For a git source that is one
|
|
10
10
|
* `git fetch` of the target ref, plus the commit the scope was made from.
|
|
11
11
|
* This is the command's only network step, catalogued in
|
|
12
|
-
* `test/egress-catalogue.ts`. A local repository reaches nothing
|
|
12
|
+
* `test/egress-catalogue.ts`. A local repository reaches nothing, and
|
|
13
|
+
* neither does a directory source (#2647), which reads `--to <dir>`.
|
|
13
14
|
* 2. Rebuild the merge base offline: each recorded file's content at the base
|
|
14
15
|
* commit, kept only when it has the hash the lock recorded. A file the
|
|
15
16
|
* project never edited is its own merge base. Vendor sources keep no
|
|
@@ -39,8 +40,21 @@ import { basename, join, relative, resolve, sep } from "node:path";
|
|
|
39
40
|
import { computePlanDigest } from "../lifecycle/plan-digest";
|
|
40
41
|
import type { ResolvedGateApproval } from "../op/gate-approval";
|
|
41
42
|
import { checkLineage, findingKey, type CheckFinding } from "./lineage-check";
|
|
42
|
-
import { git, readTemplateTree } from "./lineage-init";
|
|
43
|
-
import {
|
|
43
|
+
import { dirLabel, git, parseDirSpec, readTemplateDir, readTemplateTree, recordedDirPath } from "./lineage-init";
|
|
44
|
+
import {
|
|
45
|
+
LOCK_FILE,
|
|
46
|
+
LockError,
|
|
47
|
+
contentDigest,
|
|
48
|
+
fileHash,
|
|
49
|
+
readLock,
|
|
50
|
+
renderLock,
|
|
51
|
+
scopeKey,
|
|
52
|
+
writeLock,
|
|
53
|
+
type Lineage,
|
|
54
|
+
type LineageLock,
|
|
55
|
+
type LineageSource,
|
|
56
|
+
type ManualStep,
|
|
57
|
+
} from "./lineage-lock";
|
|
44
58
|
import { mergeFile } from "./lineage-merge";
|
|
45
59
|
import { assertCodeAllowed, planMigrations, runMigration, splitMigrations, type LoadedMigration } from "./lineage-migrations";
|
|
46
60
|
import { applyUpstream, type UpdateResult } from "./lineage-update";
|
|
@@ -95,7 +109,12 @@ export interface UpgradeOptions {
|
|
|
95
109
|
root: string;
|
|
96
110
|
/** The scope to upgrade: `"."` or a vendor scope's directory. */
|
|
97
111
|
scope?: string;
|
|
98
|
-
/**
|
|
112
|
+
/**
|
|
113
|
+
* The target ref (git) or label (vendor). Defaults to the ref the scope is
|
|
114
|
+
* pinned at. For a directory source, the directory holding the new version,
|
|
115
|
+
* as `<dir>[#<member>]`, resolved from the current directory; the member
|
|
116
|
+
* defaults to the recorded one.
|
|
117
|
+
*/
|
|
99
118
|
to?: string;
|
|
100
119
|
/** Run migrations whose body is code. */
|
|
101
120
|
allowCode?: boolean;
|
|
@@ -163,8 +182,10 @@ interface Upstream {
|
|
|
163
182
|
modules: Map<string, Buffer>;
|
|
164
183
|
commit?: string;
|
|
165
184
|
tree?: string;
|
|
166
|
-
/** The parameter values substituted into `files` (#2627), for a git source. */
|
|
185
|
+
/** The parameter values substituted into `files` (#2627), for a git or directory source. */
|
|
167
186
|
parameters?: Record<string, string>;
|
|
187
|
+
/** The source the lock records after the upgrade, when it moves (a directory source). */
|
|
188
|
+
source?: LineageSource;
|
|
168
189
|
}
|
|
169
190
|
|
|
170
191
|
/**
|
|
@@ -249,6 +270,76 @@ function fetchGit(root: string, lineage: Lineage, ref: string): Upstream {
|
|
|
249
270
|
}
|
|
250
271
|
}
|
|
251
272
|
|
|
273
|
+
/**
|
|
274
|
+
* Why a directory scope cannot be upgraded, with the way out. A directory
|
|
275
|
+
* has no history, so the merge base exists only while the files the scope
|
|
276
|
+
* was made from are still on disk.
|
|
277
|
+
*/
|
|
278
|
+
function dirRefusal(lineage: Lineage, why: string): UpgradeError {
|
|
279
|
+
const source = lineage.source as Extract<LineageSource, { type: "dir" }>;
|
|
280
|
+
return new UpgradeError(
|
|
281
|
+
`${why}. The scope was made from the directory ${dirLabel(source.path, source.member)}, which has no git history to rebuild a merge base from. ` +
|
|
282
|
+
`Upgrade with --to <dir> while ${source.path} still holds the files the scope was made from, or adopt the scope into a git lineage with \`chant workspace adopt-lineage\` (#2551).`,
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* The upstream of a directory source (#2647): the new version from the
|
|
288
|
+
* directory `to` names, and the merge base from the recorded directory, used
|
|
289
|
+
* only when its files, instantiated with the recorded parameters, still have
|
|
290
|
+
* the recorded digest. Anything else is refused: a base that is not the one
|
|
291
|
+
* the scope was made from would turn the project's edits into upstream
|
|
292
|
+
* changes. Reaches no network.
|
|
293
|
+
*/
|
|
294
|
+
function readDir(root: string, lineage: Lineage, to: string | undefined): Upstream {
|
|
295
|
+
const source = lineage.source;
|
|
296
|
+
if (source.type !== "dir") throw new UpgradeError("not a directory source");
|
|
297
|
+
if (!to) throw dirRefusal(lineage, "no --to directory");
|
|
298
|
+
|
|
299
|
+
let target;
|
|
300
|
+
try {
|
|
301
|
+
target = parseDirSpec(to);
|
|
302
|
+
} catch {
|
|
303
|
+
target = null;
|
|
304
|
+
}
|
|
305
|
+
if (!target) throw new UpgradeError(`--to ${to}: a scope made from a directory upgrades from a directory, and ${to} is not one`);
|
|
306
|
+
const member = to.includes("#") ? target.member : source.member;
|
|
307
|
+
const label = dirLabel(target.path, member);
|
|
308
|
+
const read = readTemplateDir(target.abs, member, label);
|
|
309
|
+
const raw = new Map<string, Buffer>();
|
|
310
|
+
const executable = new Set<string>();
|
|
311
|
+
for (const [path, f] of read.files) {
|
|
312
|
+
raw.set(path, f.data);
|
|
313
|
+
if (f.executable) executable.add(path);
|
|
314
|
+
}
|
|
315
|
+
const instantiated = instantiate(raw, lineage.parameters, label);
|
|
316
|
+
|
|
317
|
+
// The merge base: the recorded directory, only while it holds exactly what the scope was made from.
|
|
318
|
+
const original = resolve(root, source.path);
|
|
319
|
+
const originalLabel = dirLabel(source.path, source.member);
|
|
320
|
+
let base: Map<string, Buffer>;
|
|
321
|
+
try {
|
|
322
|
+
const old = readTemplateDir(original, source.member, originalLabel);
|
|
323
|
+
base = splitMigrations(instantiate(new Map([...old.files].map(([path, f]) => [path, f.data])), lineage.parameters, originalLabel).files).files;
|
|
324
|
+
} catch {
|
|
325
|
+
throw dirRefusal(lineage, `${originalLabel} is gone`);
|
|
326
|
+
}
|
|
327
|
+
if (!lineage.address || contentDigest(base) !== lineage.address.digest) {
|
|
328
|
+
throw dirRefusal(lineage, `${originalLabel} no longer holds the files the scope was made from (its digest is not ${lineage.address?.digest ?? "recorded"})`);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const split = splitMigrations(instantiated.files);
|
|
332
|
+
return {
|
|
333
|
+
files: split.files,
|
|
334
|
+
executable,
|
|
335
|
+
base,
|
|
336
|
+
migrations: split.migrations,
|
|
337
|
+
modules: split.modules,
|
|
338
|
+
parameters: instantiated.parameters,
|
|
339
|
+
source: { type: "dir", path: recordedDirPath(target.path, target.abs, root), ...(member ? { member } : {}) },
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
|
|
252
343
|
async function fetchUpstream(root: string, lineage: Lineage, ref: string | undefined): Promise<Upstream> {
|
|
253
344
|
const source = lineage.source;
|
|
254
345
|
if (source.type === "git") {
|
|
@@ -260,6 +351,7 @@ async function fetchUpstream(root: string, lineage: Lineage, ref: string | undef
|
|
|
260
351
|
const files = await resolveVendorSource(source, root);
|
|
261
352
|
return { files, executable: new Set(), base: new Map(), migrations: [], modules: new Map() };
|
|
262
353
|
}
|
|
354
|
+
if (source.type === "dir") return readDir(root, lineage, ref);
|
|
263
355
|
throw new UpgradeError(
|
|
264
356
|
`scope made by \`chant init --template\` (${lineage.template}) cannot be upgraded yet: its merge base is the older lexicon's render, which is not available offline. Upgrade git-sourced and vendor scopes for now.`,
|
|
265
357
|
);
|
|
@@ -404,9 +496,10 @@ export async function stageUpgrade(options: UpgradeOptions): Promise<StagedUpgra
|
|
|
404
496
|
);
|
|
405
497
|
}
|
|
406
498
|
|
|
407
|
-
// 1. Fetch.
|
|
408
|
-
const
|
|
409
|
-
const
|
|
499
|
+
// 1. Fetch. A directory source has no ref: `--to` names the directory.
|
|
500
|
+
const fromDir = lineage.source.type === "dir";
|
|
501
|
+
const ref = fromDir ? undefined : options.to ?? lineage.ref;
|
|
502
|
+
const upstream = await fetchUpstream(root, lineage, fromDir ? options.to : ref);
|
|
410
503
|
|
|
411
504
|
// 2. The merge base, offline from here on.
|
|
412
505
|
const base = rebuildBase(join(root, scope), lineage, upstream.base);
|
|
@@ -469,6 +562,7 @@ export async function stageUpgrade(options: UpgradeOptions): Promise<StagedUpgra
|
|
|
469
562
|
if (ref !== undefined) staged.ref = ref;
|
|
470
563
|
if (upstream.parameters) staged.parameters = upstream.parameters;
|
|
471
564
|
if (upstream.commit) staged.address = { ...staged.address!, commit: upstream.commit, tree: upstream.tree };
|
|
565
|
+
if (upstream.source) staged.source = upstream.source;
|
|
472
566
|
writeLock(worktreeProject, next);
|
|
473
567
|
const lockText = renderLock(next);
|
|
474
568
|
|
|
@@ -512,8 +606,8 @@ export async function stageUpgrade(options: UpgradeOptions): Promise<StagedUpgra
|
|
|
512
606
|
scope,
|
|
513
607
|
template: lineage.template,
|
|
514
608
|
kind: lineage.kind,
|
|
515
|
-
from: lineage.ref ?? null,
|
|
516
|
-
to: staged.ref ?? null,
|
|
609
|
+
from: fromDir ? sourcePin(lineage.source) : lineage.ref ?? null,
|
|
610
|
+
to: fromDir ? sourcePin(staged.source) : staged.ref ?? null,
|
|
517
611
|
...(upstream.commit ? { commit: { ...(lineage.address?.commit ? { from: lineage.address.commit } : {}), to: upstream.commit } } : {}),
|
|
518
612
|
migrations: plan.chain.map((m) => m.migration.id),
|
|
519
613
|
written: result.written,
|
|
@@ -542,6 +636,11 @@ export async function stageUpgrade(options: UpgradeOptions): Promise<StagedUpgra
|
|
|
542
636
|
}
|
|
543
637
|
}
|
|
544
638
|
|
|
639
|
+
/** A directory source's pin, for the summary: the directory it reads. */
|
|
640
|
+
function sourcePin(source: LineageSource): string | null {
|
|
641
|
+
return source.type === "dir" ? dirLabel(source.path, source.member) : null;
|
|
642
|
+
}
|
|
643
|
+
|
|
545
644
|
/**
|
|
546
645
|
* `chant workspace check` in the worktree. A finding already present at HEAD
|
|
547
646
|
* is not the upgrade's doing, and the upgrade's own manual steps are what the
|