@intentius/chant 0.124.48 → 0.124.50

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (167) hide show
  1. package/dist/cli/handlers/approve.d.ts.map +1 -1
  2. package/dist/cli/handlers/approve.js +20 -12
  3. package/dist/cli/handlers/graph-reader.d.ts +26 -0
  4. package/dist/cli/handlers/graph-reader.d.ts.map +1 -0
  5. package/dist/cli/handlers/graph-reader.js +41 -0
  6. package/dist/cli/handlers/graph.d.ts.map +1 -1
  7. package/dist/cli/handlers/graph.js +14 -0
  8. package/dist/cli/handlers/pr.d.ts.map +1 -1
  9. package/dist/cli/handlers/pr.js +40 -1
  10. package/dist/cli/main.d.ts.map +1 -1
  11. package/dist/cli/main.js +22 -0
  12. package/dist/cli/parse-args.d.ts.map +1 -1
  13. package/dist/cli/parse-args.js +11 -0
  14. package/dist/cli/registry.d.ts +8 -0
  15. package/dist/cli/registry.d.ts.map +1 -1
  16. package/dist/components/pr-run.d.ts +3 -0
  17. package/dist/components/pr-run.d.ts.map +1 -1
  18. package/dist/components/pr-run.js +1 -0
  19. package/dist/config.d.ts +16 -0
  20. package/dist/config.d.ts.map +1 -1
  21. package/dist/config.js +4 -0
  22. package/dist/gate-decision.d.ts +9 -0
  23. package/dist/gate-decision.d.ts.map +1 -1
  24. package/dist/gate-decision.js +22 -7
  25. package/dist/lexicon.d.ts +23 -0
  26. package/dist/lexicon.d.ts.map +1 -1
  27. package/dist/op/activities/activity-contracts.d.ts +4 -4
  28. package/dist/op/gate-approval-base.d.ts +25 -0
  29. package/dist/op/gate-approval-base.d.ts.map +1 -0
  30. package/dist/op/gate-approval-base.js +71 -0
  31. package/dist/op/gate-approval.d.ts +7 -4
  32. package/dist/op/gate-approval.d.ts.map +1 -1
  33. package/dist/op/gate-approval.js +2 -25
  34. package/dist/op/gate-name.d.ts +13 -0
  35. package/dist/op/gate-name.d.ts.map +1 -1
  36. package/dist/op/gate-name.js +14 -1
  37. package/dist/op/gate.d.ts +4 -3
  38. package/dist/op/gate.d.ts.map +1 -1
  39. package/dist/op/local-executor.d.ts +9 -2
  40. package/dist/op/local-executor.d.ts.map +1 -1
  41. package/dist/op/local-executor.js +24 -5
  42. package/dist/pr-loop.d.ts +10 -0
  43. package/dist/pr-loop.d.ts.map +1 -1
  44. package/dist/pr-loop.js +4 -0
  45. package/dist/pr-migrations.d.ts +62 -0
  46. package/dist/pr-migrations.d.ts.map +1 -0
  47. package/dist/pr-migrations.js +50 -0
  48. package/dist/pr-post.js +1 -1
  49. package/dist/wave-runtime.mjs +219 -218
  50. package/dist/workspace/check.schema.json +4 -2
  51. package/dist/workspace/checks/gate-approval.d.ts +58 -0
  52. package/dist/workspace/checks/gate-approval.d.ts.map +1 -0
  53. package/dist/workspace/checks/gate-approval.js +103 -0
  54. package/dist/workspace/checks.d.ts +1 -0
  55. package/dist/workspace/checks.d.ts.map +1 -1
  56. package/dist/workspace/checks.js +4 -0
  57. package/dist/workspace/declaration.d.ts +7 -0
  58. package/dist/workspace/declaration.d.ts.map +1 -1
  59. package/dist/workspace/declaration.js +13 -1
  60. package/dist/workspace/declaration.schema.json +33 -0
  61. package/dist/workspace/exit-cli.d.ts +44 -0
  62. package/dist/workspace/exit-cli.d.ts.map +1 -0
  63. package/dist/workspace/exit-cli.js +198 -0
  64. package/dist/workspace/exit.d.ts +159 -0
  65. package/dist/workspace/exit.d.ts.map +1 -0
  66. package/dist/workspace/exit.js +480 -0
  67. package/dist/workspace/exit.schema.json +257 -0
  68. package/dist/workspace/graph-cache.d.ts +3 -1
  69. package/dist/workspace/graph-cache.d.ts.map +1 -1
  70. package/dist/workspace/graph-cache.js +12 -1
  71. package/dist/workspace/import-fetch.d.ts +38 -0
  72. package/dist/workspace/import-fetch.d.ts.map +1 -0
  73. package/dist/workspace/import-fetch.js +99 -0
  74. package/dist/workspace/import-init.d.ts +26 -0
  75. package/dist/workspace/import-init.d.ts.map +1 -0
  76. package/dist/workspace/import-init.js +130 -0
  77. package/dist/workspace/import-source.d.ts +147 -0
  78. package/dist/workspace/import-source.d.ts.map +1 -0
  79. package/dist/workspace/import-source.js +370 -0
  80. package/dist/workspace/init.d.ts +4 -0
  81. package/dist/workspace/init.d.ts.map +1 -1
  82. package/dist/workspace/init.js +70 -3
  83. package/dist/workspace/kind-readers.d.ts +6 -0
  84. package/dist/workspace/kind-readers.d.ts.map +1 -1
  85. package/dist/workspace/kind-readers.js +18 -0
  86. package/dist/workspace/kinds.d.ts +24 -1
  87. package/dist/workspace/kinds.d.ts.map +1 -1
  88. package/dist/workspace/kinds.js +64 -4
  89. package/dist/workspace/lineage-upgrade.d.ts.map +1 -1
  90. package/dist/workspace/lineage-upgrade.js +13 -4
  91. package/dist/workspace/new-migrations.d.ts +47 -0
  92. package/dist/workspace/new-migrations.d.ts.map +1 -0
  93. package/dist/workspace/new-migrations.js +91 -0
  94. package/dist/workspace/pr-report.schema.json +46 -0
  95. package/dist/workspace/promote.d.ts +96 -0
  96. package/dist/workspace/promote.d.ts.map +1 -0
  97. package/dist/workspace/promote.js +263 -0
  98. package/dist/workspace/reason-codes.d.ts +13 -0
  99. package/dist/workspace/reason-codes.d.ts.map +1 -1
  100. package/dist/workspace/reason-codes.js +14 -0
  101. package/dist/workspace/search-contract.d.ts +1 -1
  102. package/dist/workspace/search-contract.d.ts.map +1 -1
  103. package/dist/workspace/workspace-kinds.schema.json +50 -7
  104. package/package.json +1 -1
  105. package/src/cli/handlers/approve.ts +10 -2
  106. package/src/cli/handlers/graph-reader.test.ts +31 -0
  107. package/src/cli/handlers/graph-reader.ts +51 -0
  108. package/src/cli/handlers/graph.ts +14 -0
  109. package/src/cli/handlers/pr.ts +38 -1
  110. package/src/cli/main.test.ts +2 -0
  111. package/src/cli/main.ts +22 -0
  112. package/src/cli/parse-args.ts +8 -0
  113. package/src/cli/registry.ts +8 -0
  114. package/src/components/pr-env-run-bundle.test.ts +1 -0
  115. package/src/components/pr-run.ts +4 -0
  116. package/src/config.ts +16 -0
  117. package/src/gate-decision-bundle.test.ts +4 -2
  118. package/src/gate-decision-identity.test.ts +11 -0
  119. package/src/gate-decision.ts +26 -6
  120. package/src/lexicon.ts +24 -0
  121. package/src/op/gate-approval-at-base.test.ts +97 -0
  122. package/src/op/gate-approval-base.ts +72 -0
  123. package/src/op/gate-approval.ts +10 -21
  124. package/src/op/gate-name.ts +16 -1
  125. package/src/op/gate.ts +4 -3
  126. package/src/op/local-executor.ts +25 -5
  127. package/src/pr-loop-bundle.test.ts +3 -2
  128. package/src/pr-loop.test.ts +52 -0
  129. package/src/pr-loop.ts +12 -0
  130. package/src/pr-migrations.ts +97 -0
  131. package/src/pr-post.ts +1 -1
  132. package/src/workspace/__fixtures__/import/spacelift.json +9 -0
  133. package/src/workspace/__fixtures__/import/tfc.json +13 -0
  134. package/src/workspace/behold-kinds.test.ts +1 -0
  135. package/src/workspace/check-contract.test.ts +3 -2
  136. package/src/workspace/check.schema.json +4 -2
  137. package/src/workspace/checks/gate-approval.test.ts +77 -0
  138. package/src/workspace/checks/gate-approval.ts +121 -0
  139. package/src/workspace/checks.ts +4 -0
  140. package/src/workspace/declaration.schema.json +33 -0
  141. package/src/workspace/declaration.ts +19 -2
  142. package/src/workspace/exit-cli.ts +219 -0
  143. package/src/workspace/exit.schema.json +257 -0
  144. package/src/workspace/exit.test.ts +185 -0
  145. package/src/workspace/exit.ts +560 -0
  146. package/src/workspace/graph-cache.ts +12 -2
  147. package/src/workspace/identity.test.ts +18 -3
  148. package/src/workspace/import-fetch.ts +151 -0
  149. package/src/workspace/import-init.ts +147 -0
  150. package/src/workspace/import-source.test.ts +183 -0
  151. package/src/workspace/import-source.ts +462 -0
  152. package/src/workspace/init.test.ts +55 -1
  153. package/src/workspace/init.ts +66 -4
  154. package/src/workspace/kind-readers.test.ts +36 -0
  155. package/src/workspace/kind-readers.ts +18 -1
  156. package/src/workspace/kinds.test.ts +23 -0
  157. package/src/workspace/kinds.ts +81 -5
  158. package/src/workspace/lineage-upgrade.test.ts +20 -0
  159. package/src/workspace/lineage-upgrade.ts +16 -7
  160. package/src/workspace/new-migrations.test.ts +98 -0
  161. package/src/workspace/new-migrations.ts +109 -0
  162. package/src/workspace/pr-report.schema.json +46 -0
  163. package/src/workspace/promote.test.ts +114 -0
  164. package/src/workspace/promote.ts +302 -0
  165. package/src/workspace/reason-codes.test.ts +4 -0
  166. package/src/workspace/reason-codes.ts +14 -0
  167. package/src/workspace/workspace-kinds.schema.json +50 -7
@@ -0,0 +1,147 @@
1
+ /**
2
+ * `chant workspace init --from tfc|spacelift|backstage` (#3178): propose a
3
+ * declaration from structure that lives outside the repository.
4
+ *
5
+ * Each source is read into a small input document (see the `*Input` types),
6
+ * and the mapping from that document to a proposal is a pure function. The
7
+ * Terraform Cloud and Spacelift documents come from their APIs
8
+ * (`import-fetch.ts`) or from an exported file given with `--input`; tests
9
+ * feed fixture JSON and never open a connection. Backstage is read from
10
+ * `catalog-info.yaml` files on disk.
11
+ *
12
+ * What maps:
13
+ *
14
+ * - a TFC workspace or a Spacelift stack becomes a member (`terraform` for a
15
+ * Terraform stack, `other` with a `because` for anything else);
16
+ * - a TFC run trigger, or a Spacelift stack dependency, becomes a link on the
17
+ * consumer to an output of the producer;
18
+ * - an environment is read from the `env:` or `environment:` tag or label, or
19
+ * the Backstage `spec.lifecycle`;
20
+ * - policy sets and policies become constraint records;
21
+ * - a Backstage component or resource becomes an `other` member, and its
22
+ * `dependsOn` becomes links.
23
+ *
24
+ * Everything else is listed in `leftOut`, with the reason, so the user sees
25
+ * what the import did not carry.
26
+ */
27
+ export declare const IMPORT_SOURCES: readonly ["tfc", "spacelift", "backstage"];
28
+ export type ImportSource = (typeof IMPORT_SOURCES)[number];
29
+ /** The output every imported producer exposes when its source names none: "this member was applied". */
30
+ export declare const APPLIED_OUTPUT = "applied";
31
+ /** The output a Backstage entity exposes to the entities that depend on it. */
32
+ export declare const CATALOG_OUTPUT = "catalog";
33
+ export interface TfcInput {
34
+ organization: string;
35
+ projects?: {
36
+ id: string;
37
+ name: string;
38
+ }[];
39
+ workspaces: {
40
+ id: string;
41
+ name: string;
42
+ project?: string;
43
+ workingDirectory?: string;
44
+ terraformVersion?: string;
45
+ vcsRepo?: string;
46
+ tags?: string[];
47
+ variables?: {
48
+ key: string;
49
+ category?: string;
50
+ sensitive?: boolean;
51
+ }[];
52
+ /** Workspace ids (or names) whose runs trigger this one. */
53
+ runTriggers?: string[];
54
+ notifications?: number;
55
+ }[];
56
+ policySets?: {
57
+ id?: string;
58
+ name: string;
59
+ kind?: string;
60
+ enforcement?: string;
61
+ workspaces?: string[];
62
+ global?: boolean;
63
+ }[];
64
+ }
65
+ export interface SpaceliftInput {
66
+ account: string;
67
+ stacks: {
68
+ id: string;
69
+ name: string;
70
+ space?: string;
71
+ repository?: string;
72
+ branch?: string;
73
+ projectRoot?: string;
74
+ vendor?: string;
75
+ labels?: string[];
76
+ dependsOn?: {
77
+ stack: string;
78
+ references?: {
79
+ outputName: string;
80
+ inputName?: string;
81
+ }[];
82
+ }[];
83
+ policies?: string[];
84
+ contexts?: string[];
85
+ }[];
86
+ policies?: {
87
+ id: string;
88
+ name: string;
89
+ type?: string;
90
+ }[];
91
+ }
92
+ export interface ImportedMember {
93
+ name: string;
94
+ dir: string;
95
+ kind: string;
96
+ because?: string;
97
+ outputs?: string[];
98
+ links?: {
99
+ member: string;
100
+ output: string;
101
+ }[];
102
+ }
103
+ export interface ConstraintRecord {
104
+ id: string;
105
+ kind: "constraint";
106
+ source: ImportSource;
107
+ title: string;
108
+ policyKind?: string;
109
+ enforcement?: string;
110
+ /** The members the constraint applies to; empty means all of them. */
111
+ appliesTo: string[];
112
+ }
113
+ export interface EnvironmentRecord {
114
+ name: string;
115
+ members: string[];
116
+ }
117
+ export interface LeftOut {
118
+ what: string;
119
+ count: number;
120
+ why: string;
121
+ }
122
+ export interface ImportProposal {
123
+ source: ImportSource;
124
+ declaration: {
125
+ name: string;
126
+ schema: 1;
127
+ members: ImportedMember[];
128
+ };
129
+ environments: EnvironmentRecord[];
130
+ records: ConstraintRecord[];
131
+ leftOut: LeftOut[];
132
+ /** Plain-language notes the user should read before writing, such as the kind pin the proposal needs. */
133
+ notes: string[];
134
+ }
135
+ export declare function proposeFromTfc(input: TfcInput, options?: {
136
+ name?: string;
137
+ }): ImportProposal;
138
+ export declare function proposeFromSpacelift(input: SpaceliftInput, options?: {
139
+ name?: string;
140
+ }): ImportProposal;
141
+ /** Every `catalog-info.yaml` under `root`, relative and `/`-separated. */
142
+ export declare function findCatalogFiles(root: string): string[];
143
+ export declare function proposeFromBackstage(root: string, options?: {
144
+ name?: string;
145
+ }): ImportProposal;
146
+ export declare function readInputFile(source: "tfc" | "spacelift", path: string): TfcInput | SpaceliftInput;
147
+ //# sourceMappingURL=import-source.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"import-source.d.ts","sourceRoot":"","sources":["../../src/workspace/import-source.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,370 @@
1
+ /**
2
+ * `chant workspace init --from tfc|spacelift|backstage` (#3178): propose a
3
+ * declaration from structure that lives outside the repository.
4
+ *
5
+ * Each source is read into a small input document (see the `*Input` types),
6
+ * and the mapping from that document to a proposal is a pure function. The
7
+ * Terraform Cloud and Spacelift documents come from their APIs
8
+ * (`import-fetch.ts`) or from an exported file given with `--input`; tests
9
+ * feed fixture JSON and never open a connection. Backstage is read from
10
+ * `catalog-info.yaml` files on disk.
11
+ *
12
+ * What maps:
13
+ *
14
+ * - a TFC workspace or a Spacelift stack becomes a member (`terraform` for a
15
+ * Terraform stack, `other` with a `because` for anything else);
16
+ * - a TFC run trigger, or a Spacelift stack dependency, becomes a link on the
17
+ * consumer to an output of the producer;
18
+ * - an environment is read from the `env:` or `environment:` tag or label, or
19
+ * the Backstage `spec.lifecycle`;
20
+ * - policy sets and policies become constraint records;
21
+ * - a Backstage component or resource becomes an `other` member, and its
22
+ * `dependsOn` becomes links.
23
+ *
24
+ * Everything else is listed in `leftOut`, with the reason, so the user sees
25
+ * what the import did not carry.
26
+ */
27
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
28
+ import { dirname, join, relative } from "node:path";
29
+ import yaml from "js-yaml";
30
+ import { NAME_PATTERN, parseDeclaration } from "./declaration.js";
31
+ import { sanitizeName } from "./init.js";
32
+ export const IMPORT_SOURCES = ["tfc", "spacelift", "backstage"];
33
+ /** The output every imported producer exposes when its source names none: "this member was applied". */
34
+ export const APPLIED_OUTPUT = "applied";
35
+ /** The output a Backstage entity exposes to the entities that depend on it. */
36
+ export const CATALOG_OUTPUT = "catalog";
37
+ // ── Helpers ──────────────────────────────────────────────────────────────────
38
+ function nameFor(raw, taken) {
39
+ let base = sanitizeName(raw) || "member";
40
+ if (!NAME_PATTERN.test(base))
41
+ base = "member";
42
+ let name = base;
43
+ for (let i = 2; taken.has(name); i++)
44
+ name = `${base.slice(0, 36)}-${i}`;
45
+ taken.add(name);
46
+ return name;
47
+ }
48
+ function cleanDir(raw, fallback) {
49
+ const d = (raw ?? "").trim().replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/+$/, "");
50
+ if (d === "" || d === "." || d.startsWith("/") || d.split("/").includes(".."))
51
+ return fallback;
52
+ return d;
53
+ }
54
+ const ENV_TAG = /^(?:env|environment)[:=](.+)$/i;
55
+ function envOf(tags) {
56
+ return (tags ?? []).map((t) => ENV_TAG.exec(t)?.[1]?.trim()).filter((e) => !!e);
57
+ }
58
+ function addEnv(map, env, member) {
59
+ const list = map.get(env) ?? [];
60
+ if (!list.includes(member))
61
+ list.push(member);
62
+ map.set(env, list);
63
+ }
64
+ function envList(map) {
65
+ return [...map].map(([name, members]) => ({ name, members })).sort((a, b) => a.name.localeCompare(b.name));
66
+ }
67
+ function addLink(member, producer, output) {
68
+ producer.outputs = [...new Set([...(producer.outputs ?? []), output])];
69
+ member.links = [...(member.links ?? [])];
70
+ if (!member.links.some((l) => l.member === producer.name && l.output === output))
71
+ member.links.push({ member: producer.name, output });
72
+ }
73
+ function finish(source, workspaceName, members, parts) {
74
+ const declaration = { name: nameFor(workspaceName, new Set()), schema: 1, members };
75
+ // The same check `init` makes: it must read back as a valid declaration.
76
+ parseDeclaration(JSON.stringify(declaration), "chant.workspace.json");
77
+ return { source, declaration, ...parts };
78
+ }
79
+ const TF_NOTE = "Members of kind terraform get their kind from @intentius/chant-lexicon-terraform: pin that package in the declaration's pins before chant workspace check will read them.";
80
+ // ── Terraform Cloud ──────────────────────────────────────────────────────────
81
+ export function proposeFromTfc(input, options = {}) {
82
+ const taken = new Set();
83
+ const byRef = new Map();
84
+ const members = [];
85
+ const projects = new Map((input.projects ?? []).map((p) => [p.id, p.name]));
86
+ const envs = new Map();
87
+ let variables = 0;
88
+ let sensitive = 0;
89
+ let vcs = 0;
90
+ let notifications = 0;
91
+ const byDir = new Map();
92
+ let shared = 0;
93
+ for (const w of input.workspaces) {
94
+ const dirKey = cleanDir(w.workingDirectory, `\0${w.name}`);
95
+ const first = byDir.get(dirKey);
96
+ if (first) {
97
+ // One member per directory: a workspace on the same configuration folds into the first.
98
+ byRef.set(w.id, first);
99
+ byRef.set(w.name, first);
100
+ for (const e of envOf(w.tags))
101
+ addEnv(envs, e, first.name);
102
+ shared++;
103
+ continue;
104
+ }
105
+ const name = nameFor(w.name, taken);
106
+ const project = w.project ? projects.get(w.project) : undefined;
107
+ const m = {
108
+ name,
109
+ dir: dirKey.startsWith("\0") ? name : dirKey,
110
+ kind: "terraform",
111
+ because: `Terraform Cloud workspace ${input.organization}/${w.name}${project ? ` in project ${project}` : ""}`,
112
+ };
113
+ members.push(m);
114
+ byDir.set(dirKey, m);
115
+ byRef.set(w.id, m);
116
+ byRef.set(w.name, m);
117
+ for (const e of envOf(w.tags))
118
+ addEnv(envs, e, name);
119
+ variables += w.variables?.length ?? 0;
120
+ sensitive += (w.variables ?? []).filter((v) => v.sensitive).length;
121
+ if (w.vcsRepo)
122
+ vcs++;
123
+ notifications += w.notifications ?? 0;
124
+ }
125
+ let triggers = 0;
126
+ let dangling = 0;
127
+ for (const w of input.workspaces) {
128
+ const consumer = byRef.get(w.id);
129
+ for (const src of w.runTriggers ?? []) {
130
+ const producer = byRef.get(src);
131
+ if (!producer || producer === consumer) {
132
+ dangling++;
133
+ continue;
134
+ }
135
+ addLink(consumer, producer, APPLIED_OUTPUT);
136
+ triggers++;
137
+ }
138
+ }
139
+ const records = (input.policySets ?? []).map((p, i) => ({
140
+ id: `tfc-policy-${sanitizeName(p.name) || i}`,
141
+ kind: "constraint",
142
+ source: "tfc",
143
+ title: p.name,
144
+ ...(p.kind ? { policyKind: p.kind } : {}),
145
+ ...(p.enforcement ? { enforcement: p.enforcement } : {}),
146
+ appliesTo: p.global ? [] : (p.workspaces ?? []).map((r) => byRef.get(r)?.name).filter((n) => !!n),
147
+ }));
148
+ const leftOut = [];
149
+ if (variables > 0)
150
+ leftOut.push({ what: "variables", count: variables, why: `${sensitive} are sensitive and their values are never read; the keys stay in Terraform Cloud` });
151
+ if (vcs > 0)
152
+ leftOut.push({ what: "VCS connections", count: vcs, why: "the repository link belongs to the CI that replaces Terraform Cloud's runs" });
153
+ if (notifications > 0)
154
+ leftOut.push({ what: "notification configurations", count: notifications, why: "a workspace has no notification record" });
155
+ if (shared > 0)
156
+ leftOut.push({ what: "workspaces sharing a directory", count: shared, why: "a member is one directory; each folds into the first workspace on it, with its environments and run triggers" });
157
+ if (dangling > 0)
158
+ leftOut.push({ what: "run triggers", count: dangling, why: "their source workspace is not in the input, or is the workspace itself" });
159
+ if ((input.projects ?? []).length > 0)
160
+ leftOut.push({ what: "projects", count: input.projects.length, why: "named in each member's because; a project is not a declared grouping" });
161
+ leftOut.push({ what: "state and run history", count: input.workspaces.length, why: "state is adopted when a member is promoted, not copied by init" });
162
+ void triggers;
163
+ return finish("tfc", options.name ?? input.organization, members, { environments: envList(envs), records, leftOut, notes: [TF_NOTE] });
164
+ }
165
+ // ── Spacelift ────────────────────────────────────────────────────────────────
166
+ export function proposeFromSpacelift(input, options = {}) {
167
+ const taken = new Set();
168
+ const byRef = new Map();
169
+ const members = [];
170
+ const envs = new Map();
171
+ let nonTerraform = 0;
172
+ let contexts = 0;
173
+ const byDir = new Map();
174
+ let shared = 0;
175
+ for (const s of input.stacks) {
176
+ const dirKey = cleanDir(s.projectRoot, `\0${s.name}`);
177
+ const first = byDir.get(dirKey);
178
+ if (first) {
179
+ byRef.set(s.id, first);
180
+ byRef.set(s.name, first);
181
+ for (const e of envOf(s.labels))
182
+ addEnv(envs, e, first.name);
183
+ shared++;
184
+ continue;
185
+ }
186
+ const name = nameFor(s.name, taken);
187
+ const vendor = (s.vendor ?? "terraform").toLowerCase();
188
+ const tf = vendor === "terraform" || vendor === "opentofu";
189
+ if (!tf)
190
+ nonTerraform++;
191
+ const m = {
192
+ name,
193
+ dir: dirKey.startsWith("\0") ? name : dirKey,
194
+ kind: tf ? "terraform" : "other",
195
+ because: `Spacelift stack ${s.id}${s.space ? ` in space ${s.space}` : ""}${tf ? "" : `, ${vendor}`}`,
196
+ };
197
+ members.push(m);
198
+ byDir.set(dirKey, m);
199
+ byRef.set(s.id, m);
200
+ byRef.set(s.name, m);
201
+ for (const e of envOf(s.labels))
202
+ addEnv(envs, e, name);
203
+ contexts += s.contexts?.length ?? 0;
204
+ }
205
+ let dangling = 0;
206
+ for (const s of input.stacks) {
207
+ const consumer = byRef.get(s.id);
208
+ for (const dep of s.dependsOn ?? []) {
209
+ const producer = byRef.get(dep.stack);
210
+ if (!producer || producer === consumer) {
211
+ dangling++;
212
+ continue;
213
+ }
214
+ const outs = (dep.references ?? []).map((r) => r.outputName);
215
+ for (const o of outs.length > 0 ? outs : [APPLIED_OUTPUT])
216
+ addLink(consumer, producer, o);
217
+ }
218
+ }
219
+ const attached = new Map();
220
+ for (const s of input.stacks)
221
+ for (const p of s.policies ?? [])
222
+ attached.set(p, [...(attached.get(p) ?? []), byRef.get(s.id).name]);
223
+ const records = (input.policies ?? []).map((p) => ({
224
+ id: `spacelift-policy-${sanitizeName(p.name) || sanitizeName(p.id)}`,
225
+ kind: "constraint",
226
+ source: "spacelift",
227
+ title: p.name,
228
+ ...(p.type ? { policyKind: p.type } : {}),
229
+ appliesTo: attached.get(p.id) ?? [],
230
+ }));
231
+ const leftOut = [];
232
+ if (contexts > 0)
233
+ leftOut.push({ what: "context attachments", count: contexts, why: "contexts hold variables and mounted files, which are never read" });
234
+ if (shared > 0)
235
+ leftOut.push({ what: "stacks sharing a project root", count: shared, why: "a member is one directory; each folds into the first stack on it" });
236
+ if (dangling > 0)
237
+ leftOut.push({ what: "stack dependencies", count: dangling, why: "the dependency's stack is not in the input, or is the stack itself" });
238
+ if (nonTerraform > 0)
239
+ leftOut.push({ what: "non-Terraform stacks kept as other", count: nonTerraform, why: "only Terraform and OpenTofu stacks take the terraform kind" });
240
+ leftOut.push({ what: "worker pools, cloud integrations and run history", count: input.stacks.length, why: "not structure of the workspace" });
241
+ return finish("spacelift", options.name ?? input.account, members, { environments: envList(envs), records, leftOut, notes: [TF_NOTE] });
242
+ }
243
+ const SKIP_DIRS = new Set(["node_modules", ".git", "dist", ".chant"]);
244
+ /** Every `catalog-info.yaml` under `root`, relative and `/`-separated. */
245
+ export function findCatalogFiles(root) {
246
+ const out = [];
247
+ const walk = (dir) => {
248
+ for (const e of readdirSync(dir, { withFileTypes: true })) {
249
+ if (e.isDirectory()) {
250
+ if (!SKIP_DIRS.has(e.name))
251
+ walk(join(dir, e.name));
252
+ }
253
+ else if (e.isFile() && /^catalog-info\.ya?ml$/.test(e.name)) {
254
+ out.push(relative(root, join(dir, e.name)).split("\\").join("/"));
255
+ }
256
+ }
257
+ };
258
+ if (existsSync(root))
259
+ walk(root);
260
+ return out.sort();
261
+ }
262
+ const refKey = (kind, ns, name) => `${kind.toLowerCase()}:${ns ?? "default"}/${name}`;
263
+ function asRefs(v) {
264
+ return Array.isArray(v) ? v.filter((x) => typeof x === "string") : [];
265
+ }
266
+ /** Normalize `name`, `kind:name` and `kind:ns/name` to `kind:ns/name`. */
267
+ function normalizeRef(ref, defaultKind) {
268
+ const m = /^(?:([A-Za-z]+):)?(?:([^/]+)\/)?(.+)$/.exec(ref.trim());
269
+ if (!m)
270
+ return ref;
271
+ return refKey(m[1] ?? defaultKind, m[2], m[3]);
272
+ }
273
+ export function proposeFromBackstage(root, options = {}) {
274
+ const files = findCatalogFiles(root);
275
+ const taken = new Set();
276
+ const byRef = new Map();
277
+ const byDir = new Map();
278
+ const members = [];
279
+ const entities = [];
280
+ const envs = new Map();
281
+ const skipped = new Map();
282
+ const bump = (k) => void skipped.set(k, (skipped.get(k) ?? 0) + 1);
283
+ for (const file of files) {
284
+ let docs;
285
+ try {
286
+ docs = yaml.loadAll(readFileSync(join(root, file), "utf-8"));
287
+ }
288
+ catch {
289
+ bump("unreadable catalog-info.yaml");
290
+ continue;
291
+ }
292
+ const dir = cleanDir(dirname(file), "");
293
+ for (const doc of docs) {
294
+ const entity = doc;
295
+ const kind = entity?.kind;
296
+ const name = entity?.metadata?.name;
297
+ if (typeof kind !== "string" || typeof name !== "string")
298
+ continue;
299
+ const ref = refKey(kind, entity.metadata?.namespace, name);
300
+ if (kind !== "Component" && kind !== "Resource") {
301
+ bump(kind);
302
+ entities.push({ entity, ref });
303
+ continue;
304
+ }
305
+ let member = byDir.get(dir);
306
+ if (member) {
307
+ // One member per directory: a second entity in the directory folds into the first.
308
+ byRef.set(ref, member);
309
+ bump(`${kind} sharing a directory with a member`);
310
+ }
311
+ else {
312
+ member = {
313
+ name: nameFor(name, taken),
314
+ dir: dir === "" ? "." : dir,
315
+ kind: "other",
316
+ because: `Backstage ${kind.toLowerCase()} ${name}${entity.spec?.system ? ` of system ${entity.spec.system}` : ""}`,
317
+ };
318
+ members.push(member);
319
+ byDir.set(dir, member);
320
+ byRef.set(ref, member);
321
+ const lifecycle = entity.spec?.lifecycle;
322
+ if (lifecycle)
323
+ addEnv(envs, lifecycle, member.name);
324
+ }
325
+ entities.push({ entity, member, ref });
326
+ }
327
+ }
328
+ const apiProviders = new Map();
329
+ for (const { entity, member } of entities) {
330
+ if (member)
331
+ for (const api of asRefs(entity.spec?.providesApis))
332
+ apiProviders.set(normalizeRef(api, "api"), member);
333
+ }
334
+ let dangling = 0;
335
+ for (const { entity, member } of entities) {
336
+ if (!member)
337
+ continue;
338
+ const wants = [
339
+ ...asRefs(entity.spec?.dependsOn).map((r) => ({ producer: byRef.get(normalizeRef(r, "component")) })),
340
+ ...asRefs(entity.spec?.consumesApis).map((r) => ({ producer: apiProviders.get(normalizeRef(r, "api")) })),
341
+ ];
342
+ for (const { producer } of wants) {
343
+ if (!producer || producer === member) {
344
+ dangling++;
345
+ continue;
346
+ }
347
+ addLink(member, producer, CATALOG_OUTPUT);
348
+ }
349
+ }
350
+ const leftOut = [...skipped].map(([what, count]) => ({
351
+ what,
352
+ count,
353
+ why: what.includes("sharing a directory") ? "a member is one directory; its dependsOn links fold into the first entity there" : "not a unit of code or infrastructure; its owner and system stay in the catalog",
354
+ }));
355
+ if (dangling > 0)
356
+ leftOut.push({ what: "dependsOn and consumesApis references", count: dangling, why: "they name an entity outside the files read, or the entity itself" });
357
+ if (files.length === 0)
358
+ leftOut.push({ what: "catalog-info.yaml files", count: 0, why: `none found under ${root}` });
359
+ const wsName = options.name ?? (root.split(/[\\/]/).filter(Boolean).pop() ?? "backstage");
360
+ return finish("backstage", wsName, members, { environments: envList(envs), records: [], leftOut, notes: [] });
361
+ }
362
+ // ── Reading an input file ────────────────────────────────────────────────────
363
+ export function readInputFile(source, path) {
364
+ const raw = JSON.parse(readFileSync(path, "utf-8"));
365
+ if (source === "tfc" && !Array.isArray(raw.workspaces))
366
+ throw new Error(`${path} has no "workspaces" array: it is not a Terraform Cloud export`);
367
+ if (source === "spacelift" && !Array.isArray(raw.stacks))
368
+ throw new Error(`${path} has no "stacks" array: it is not a Spacelift export`);
369
+ return raw;
370
+ }
@@ -74,6 +74,10 @@ export interface Proposal {
74
74
  declaration: {
75
75
  name: string;
76
76
  schema: 1;
77
+ pins?: {
78
+ package: string;
79
+ version: string;
80
+ }[];
77
81
  members: ProposedEntry[];
78
82
  };
79
83
  /** Directories leaving the root project, with the .ts source files each holds. */
@@ -1 +1 @@
1
- {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/workspace/init.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAMH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AActD,OAAO,EAAmC,KAAK,aAAa,EAAE,MAAM,QAAQ,CAAC;AAI7E,mHAAmH;AACnH,eAAO,MAAM,QAAQ,YAAI,UAAU,EAAE,KAAK,EAAE,OAAO,CAAU,CAAC;AAC9D,MAAM,MAAM,OAAO,GAAG,CAAC,OAAO,QAAQ,CAAC,CAAC,MAAM,CAAC,CAAC;AAEhD,uDAAuD;AACvD,eAAO,MAAM,YAAY,iCAAiC,CAAC;AAE3D,yFAAyF;AACzF,eAAO,MAAM,aAAa,uCAAuC,CAAC;AAElE;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,SAAS,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAGjG;AAoBD,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;CAC1B;AAED,mEAAmE;AACnE,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,iEAAiE;IACjE,MAAM,EAAE,MAAM,CAAC;IACf;;;OAGG;IACH,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,6EAA6E;IAC7E,QAAQ,EAAE,MAAM,CAAC;IACjB,uGAAuG;IACvG,MAAM,CAAC,EAAE,SAAS,GAAG,QAAQ,GAAG,UAAU,CAAC;CAC5C;AAED,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,8CAA8C;IAC9C,WAAW,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,CAAC,CAAC;QAAC,OAAO,EAAE,aAAa,EAAE,CAAA;KAAE,CAAC;IACnE,kFAAkF;IAClF,OAAO,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,EAAE,CAAA;KAAE,EAAE,CAAC;IAC3D,qEAAqE;IACrE,MAAM,EAAE,aAAa,EAAE,CAAC;CACzB;AAiDD,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAQhD;AAuDD;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,aAAa,EAAE,GAAG,EAAE,MAAM,GAAG;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,SAAS,CAkBvI;AASD;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,aAAa,EAAE,OAAO,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,EAAE,GAAG,aAAa,EAAE,CAuB1H;AAoFD,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAA;CAAO,GAAG,QAAQ,CAsFxF;AA4ED,wBAAsB,gBAAgB,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAoE3E"}
1
+ {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/workspace/init.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAMH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAetD,OAAO,EAAmC,KAAK,aAAa,EAAE,MAAM,QAAQ,CAAC;AAI7E,mHAAmH;AACnH,eAAO,MAAM,QAAQ,YAAI,UAAU,EAAE,KAAK,EAAE,OAAO,CAAU,CAAC;AAC9D,MAAM,MAAM,OAAO,GAAG,CAAC,OAAO,QAAQ,CAAC,CAAC,MAAM,CAAC,CAAC;AAEhD,uDAAuD;AACvD,eAAO,MAAM,YAAY,iCAAiC,CAAC;AAE3D,yFAAyF;AACzF,eAAO,MAAM,aAAa,uCAAuC,CAAC;AAElE;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,SAAS,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAGjG;AAoBD,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;CAC1B;AAED,mEAAmE;AACnE,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,iEAAiE;IACjE,MAAM,EAAE,MAAM,CAAC;IACf;;;OAGG;IACH,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,6EAA6E;IAC7E,QAAQ,EAAE,MAAM,CAAC;IACjB,uGAAuG;IACvG,MAAM,CAAC,EAAE,SAAS,GAAG,QAAQ,GAAG,UAAU,CAAC;CAC5C;AAED,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,8CAA8C;IAC9C,WAAW,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,CAAC,CAAC;QAAC,IAAI,CAAC,EAAE;YAAE,OAAO,EAAE,MAAM,CAAC;YAAC,OAAO,EAAE,MAAM,CAAA;SAAE,EAAE,CAAC;QAAC,OAAO,EAAE,aAAa,EAAE,CAAA;KAAE,CAAC;IAClH,kFAAkF;IAClF,OAAO,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,EAAE,CAAA;KAAE,EAAE,CAAC;IAC3D,qEAAqE;IACrE,MAAM,EAAE,aAAa,EAAE,CAAC;CACzB;AAiDD,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAQhD;AAuDD;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,aAAa,EAAE,GAAG,EAAE,MAAM,GAAG;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,SAAS,CAkBvI;AASD;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,aAAa,EAAE,OAAO,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,EAAE,GAAG,aAAa,EAAE,CAuB1H;AA+GD,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAA;CAAO,GAAG,QAAQ,CA+GxF;AA4ED,wBAAsB,gBAAgB,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CA6E3E"}
@@ -37,9 +37,10 @@ import { formatError, formatSuccess } from "../cli/format.js";
37
37
  import { DECLARATION_FILES, NAME_PATTERN, parseDeclaration, resolveGroups, rootExclusions, WorkspaceReadError, isInside, readerVersion, } from "./declaration.js";
38
38
  import { expandGlob } from "./glob.js";
39
39
  import { setProperty } from "./json-edit.js";
40
- import { holdsChantProject } from "./kinds.js";
40
+ import { dataExportTarget } from "./class-data.js";
41
+ import { holdsChantProject, loadKindRegistry, resolveKind } from "./kinds.js";
41
42
  import { gitTop, skippedDir, workingTree } from "./tree.js";
42
- const USAGE = "chant workspace init [dir] [--name <name>] [--yes] [--verbose]\n chant workspace init --profile ideation|app|infra [dir] [--name <name>] [--param name=<display name>] [--from <repo>@<ref>|<chant checkout>] [--yes]";
43
+ const USAGE = "chant workspace init [dir] [--name <name>] [--yes] [--verbose]\n chant workspace init --profile ideation|app|infra [dir] [--name <name>] [--param name=<display name>] [--from <repo>@<ref>|<chant checkout>] [--yes]\n chant workspace init --from tfc|spacelift|backstage [dir] [--org <organization>] [--endpoint <url>] [--input <export.json>] [--token <token>] [--yes]";
43
44
  /** The profiles `--profile` takes (#3174), each a directory under {@link PROFILES_DIR} in the chant repository. */
44
45
  export const PROFILES = ["ideation", "app", "infra"];
45
46
  /** Where the profiles live in the chant repository. */
@@ -335,6 +336,34 @@ function groupName(glob) {
335
336
  return sanitizeName([...words, base].join("-")) || base;
336
337
  }
337
338
  // ── The proposal ─────────────────────────────────────────────────────────────
339
+ /**
340
+ * The packages installed at `root` that supply member kinds, as pins at their
341
+ * installed versions. A declaration only reads kinds from its pins, so a
342
+ * proposal that names a package's kind has to pin the package too.
343
+ */
344
+ function installedKindPins(root) {
345
+ const scope = join(root, "node_modules", "@intentius");
346
+ let names;
347
+ try {
348
+ names = readdirSync(scope).sort();
349
+ }
350
+ catch {
351
+ return [];
352
+ }
353
+ const pins = [];
354
+ for (const n of names) {
355
+ try {
356
+ const pkg = JSON.parse(readFileSync(join(scope, n, "package.json"), "utf-8"));
357
+ if (typeof pkg.name === "string" && typeof pkg.version === "string" && typeof dataExportTarget(pkg, "./workspace-kinds") === "string") {
358
+ pins.push({ package: pkg.name, version: pkg.version });
359
+ }
360
+ }
361
+ catch {
362
+ // Not a package, or not readable: it supplies nothing.
363
+ }
364
+ }
365
+ return pins;
366
+ }
338
367
  export function proposeWorkspace(root, options = {}) {
339
368
  const tree = workingTree(root);
340
369
  const files = listFiles(root);
@@ -377,7 +406,30 @@ export function proposeWorkspace(root, options = {}) {
377
406
  [...matches].some((m) => isInside(d, m)) ||
378
407
  chantMembers.some((c) => isInside(d, c) || isInside(c, d)) ||
379
408
  exampleMatch(d) !== undefined;
380
- const packages = outermost([...pkgDirs].filter((d) => d !== "" && !claimed(d)));
409
+ // A directory that an installed package's kind claims is a typed member
410
+ // (#3179), taking the place of `other` for the same directory. Members
411
+ // don't nest, so a typed member is the outermost one, and never sits inside
412
+ // a project or package that is a member itself.
413
+ const packageDirs = outermost([...pkgDirs].filter((d) => d !== "" && !claimed(d)));
414
+ const kindPins = installedKindPins(root);
415
+ const kindRegistry = kindPins.length > 0 ? loadKindRegistry(kindPins.map((p) => ({ ...p, path: null, integrity: null })), root).registry : undefined;
416
+ const typed = new Map();
417
+ if (kindRegistry) {
418
+ const winners = new Map();
419
+ const others = [...configDirs, ...nested, ...packageDirs];
420
+ for (const d of new Set(files.map(dirOf))) {
421
+ if (d === "" || claimed(d) || configDirs.has(d) || nested.includes(d))
422
+ continue;
423
+ if (others.some((o) => o !== d && isInside(d, o)))
424
+ continue;
425
+ const winner = resolveKind(kindRegistry, tree, d).winner;
426
+ if (winner && winner.source !== "builtin")
427
+ winners.set(d, winner.name);
428
+ }
429
+ for (const d of outermost(winners.keys()))
430
+ typed.set(d, winners.get(d));
431
+ }
432
+ const packages = packageDirs.filter((d) => ![...typed.keys()].some((t) => isInside(d, t)));
381
433
  const taken = new Set([name]);
382
434
  const entries = [];
383
435
  if (configDirs.has(""))
@@ -395,6 +447,9 @@ export function proposeWorkspace(root, options = {}) {
395
447
  because: npmWorkspaces.has(d) ? "an npm workspace package with no chant project" : "an npm package with no chant project",
396
448
  });
397
449
  }
450
+ for (const [d, kind] of typed) {
451
+ entries.push({ sort: d, name: uniqueName(memberName(tree, d, name), taken), dir: d, kind });
452
+ }
398
453
  entries.sort((a, b) => (a.sort < b.sort ? -1 : a.sort > b.sort ? 1 : 0));
399
454
  taken.delete(name);
400
455
  const memberDirs = entries.map((e) => (e.dir === "." ? "" : e.dir));
@@ -406,9 +461,12 @@ export function proposeWorkspace(root, options = {}) {
406
461
  const groups = [...byGroup]
407
462
  .map(([n, globs]) => ({ name: uniqueName(n, taken), kind: "examples", glob: globs.length === 1 ? globs[0] : globs }))
408
463
  .sort((a, b) => ([a.glob].flat()[0] < [b.glob].flat()[0] ? -1 : 1));
464
+ const usedSources = new Set([...typed.values()].map((k) => kindRegistry.get(k).source));
465
+ const pins = kindPins.filter((p) => usedSources.has(p.package));
409
466
  const declaration = {
410
467
  name,
411
468
  schema: 1,
469
+ ...(pins.length > 0 ? { pins } : {}),
412
470
  members: [...entries.map(({ sort: _sort, ...e }) => e), ...groups],
413
471
  };
414
472
  // Read the proposal back through the real reader: it must be a valid
@@ -527,6 +585,15 @@ export async function runWorkspaceInit(ctx) {
527
585
  }
528
586
  if (args.profile !== undefined)
529
587
  return initProfile(args, root);
588
+ // #3178: --from tfc|spacelift|backstage proposes from structure held outside the repository.
589
+ const { isImportSource, runImportInit, IMPORT_USAGE } = await import("./import-init.js");
590
+ if (args.migrateFrom !== undefined) {
591
+ if (!isImportSource(args.migrateFrom)) {
592
+ console.error(formatError({ message: `--from ${JSON.stringify(args.migrateFrom)} is not a source; the sources are tfc, spacelift, backstage`, hint: IMPORT_USAGE }));
593
+ return 1;
594
+ }
595
+ return runImportInit(args.migrateFrom, root, args);
596
+ }
530
597
  let proposal;
531
598
  try {
532
599
  proposal = proposeWorkspace(root, { name: args.selectName });