create-kedge 0.10.1 → 0.11.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-kedge",
3
- "version": "0.10.1",
3
+ "version": "0.11.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
package/src/create.mjs CHANGED
@@ -80,6 +80,31 @@ export function render(template, { name, suffix: sfx, packs }) {
80
80
  return files;
81
81
  }
82
82
 
83
+ /** The folder's entries, none when it doesn't exist yet. */
84
+ const existing = (dir) => (existsSync(dir) ? readdirSync(dir) : []);
85
+
86
+ /** The top-level names the template writes. */
87
+ const ownedTop = (files) => new Set(Object.keys(files).map((p) => p.split("/")[0]));
88
+
89
+ /** An existing .gitignore with each of the starter's lines it lacks appended, its own lines as they were. */
90
+ /** Where the CLI and kedge dev write credentials and keys: never tracked (p0.10 §4, §6.2). */
91
+ const CREDENTIAL_PATHS = [".env.local", ".env.development.local", ".dev.vars", ".kedge/state.db"];
92
+
93
+ export function mergeIgnore(mine, starter) {
94
+ const lines = mine.split("\n").map((l) => l.trim());
95
+ // A rule counts as present only if no exception follows it: `.env*` then `!.env.local` would commit
96
+ // the credentials the starter's `.env*` keeps out (codex code r4), so such a rule is appended again, last.
97
+ const holds = (rule) => {
98
+ const at = lines.lastIndexOf(rule);
99
+ return at >= 0 && !lines.slice(at + 1).some((l) => l.startsWith("!"));
100
+ };
101
+ const add = starter
102
+ .split("\n")
103
+ .filter((l) => l.trim() && (l.trim().startsWith("#") ? !lines.includes(l.trim()) : !holds(l.trim())));
104
+ if (!add.length) return mine;
105
+ return `${mine.replace(/\n*$/, "\n")}# Kedge's starter (create-kedge):\n${add.join("\n")}\n`;
106
+ }
107
+
83
108
  export function write(dir, files) {
84
109
  for (const [path, text] of Object.entries(files)) {
85
110
  if (isAbsolute(path) || path.split("/").includes("..")) throw new Error(`template path ${path} escapes the store`);
@@ -130,10 +155,6 @@ export function main(
130
155
  const positional = argv.filter((a, i) => !a.startsWith("--") && !["--name", "--from-packs"].includes(argv[i - 1]));
131
156
  // No directory: the folder it's run in, as `npm create kedge@latest` alone leaves it (p0.10.1 §1, 1).
132
157
  const target = positional[0] ?? ".";
133
- if (positional[0] === undefined && readdirSync(resolve(target)).length > 0) {
134
- error(`this folder isn't empty: run it in an empty one, or name a new one: npm create kedge@latest my-store`);
135
- return 2;
136
- }
137
158
  // Before anything is written: a half-made store on a machine that can't install it helps no one.
138
159
  const pnpm = flags.has("--no-install") ? null : run("pnpm", ["--version"], { encoding: "utf8" });
139
160
  const problem = environmentProblem({
@@ -146,10 +167,6 @@ export function main(
146
167
  return 1;
147
168
  }
148
169
  const dir = resolve(target);
149
- if (existsSync(dir) && readdirSync(dir).length > 0) {
150
- error(`${dir} isn't empty`);
151
- return 1;
152
- }
153
170
  const chosen = value("--name");
154
171
  const name = chosen ?? storeNameOf(basename(dir));
155
172
  if (!chosen) log(`store name ${name}, from the folder; --name to choose another`);
@@ -162,10 +179,39 @@ export function main(
162
179
  error(e.message);
163
180
  return 1;
164
181
  }
182
+ // A workspace's own dot-entries (.git, .salve, .vscode) may already be here: a platform like salve
183
+ // loads AGENTS.md and skills only from its root (p0.11 row 11). Anything the template writes may not.
184
+ const clash = existing(dir).filter((e) => !e.startsWith(".") || (e !== ".gitignore" && ownedTop(files).has(e)));
185
+ if (clash.length) {
186
+ error(
187
+ `${dir} already holds ${clash.join(", ")}: run it in an empty folder, or one holding only its own dot-files, or name a new one: npm create kedge@latest my-store`,
188
+ );
189
+ return positional[0] === undefined ? 2 : 1;
190
+ }
165
191
  mkdirSync(dir, { recursive: true });
192
+ const ignore = join(dir, ".gitignore");
193
+ if (existsSync(ignore) && files[".gitignore"] !== undefined)
194
+ files[".gitignore"] = mergeIgnore(readFileSync(ignore, "utf8"), files[".gitignore"]);
166
195
  write(dir, files);
167
196
  log(`created ${name} in ${dir} (Kedge ${t.version})`);
168
- if (!flags.has("--no-git")) run("git", ["init", "-q"], { cwd: dir, stdio: "inherit" });
197
+ // Inside a repository already (a workspace's), the store is part of it: no repository inside another (p0.11 row 12).
198
+ const inRepo =
199
+ String(run("git", ["rev-parse", "--is-inside-work-tree"], { cwd: dir, encoding: "utf8" })?.stdout ?? "").trim() ===
200
+ "true";
201
+ if (!flags.has("--no-git") && !inRepo) run("git", ["init", "-q"], { cwd: dir, stdio: "inherit" });
202
+ // Whatever else the repository's ignore files say, the credentials the CLI saves stay out of Git.
203
+ if (inRepo || !flags.has("--no-git")) {
204
+ const kept = CREDENTIAL_PATHS.filter(
205
+ // 1 is "not ignored"; no git, or no repository (128), is no answer.
206
+ (p) => run("git", ["check-ignore", "-q", p], { cwd: dir, stdio: "ignore" })?.status === 1,
207
+ );
208
+ if (kept.length) {
209
+ error(
210
+ `Git here wouldn't ignore ${kept.join(", ")}, where the CLI saves credentials and keys: add them to .gitignore (an exception such as !.env.local overrides the starter's rule) before anything is committed`,
211
+ );
212
+ return 1;
213
+ }
214
+ }
169
215
  if (!flags.has("--no-install")) {
170
216
  const r = run("pnpm", ["install"], { cwd: dir, stdio: "inherit" });
171
217
  if (r.status !== 0) {