@grimoire-rs/indexer 0.2.1 → 0.3.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/README.md +51 -18
- package/dist/ci.d.ts +115 -0
- package/dist/ci.d.ts.map +1 -0
- package/dist/ci.js +286 -0
- package/dist/ci.js.map +1 -0
- package/dist/cli/build.d.ts.map +1 -1
- package/dist/cli/build.js +3 -9
- package/dist/cli/build.js.map +1 -1
- package/dist/cli/ci.d.ts +6 -0
- package/dist/cli/ci.d.ts.map +1 -0
- package/dist/cli/ci.js +43 -0
- package/dist/cli/ci.js.map +1 -0
- package/dist/cli/dev.d.ts +7 -0
- package/dist/cli/dev.d.ts.map +1 -0
- package/dist/cli/dev.js +57 -0
- package/dist/cli/dev.js.map +1 -0
- package/dist/cli/init.d.ts +6 -1
- package/dist/cli/init.d.ts.map +1 -1
- package/dist/cli/init.js +298 -74
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/main.js +61 -8
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/out_dir.d.ts +11 -0
- package/dist/cli/out_dir.d.ts.map +1 -0
- package/dist/cli/out_dir.js +30 -0
- package/dist/cli/out_dir.js.map +1 -0
- package/dist/cli/validate.d.ts +1 -0
- package/dist/cli/validate.d.ts.map +1 -1
- package/dist/cli/validate.js +47 -1
- package/dist/cli/validate.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +12 -2
- package/dist/config.js.map +1 -1
- package/dist/templates.d.ts +12 -0
- package/dist/templates.d.ts.map +1 -0
- package/dist/templates.js +40 -0
- package/dist/templates.js.map +1 -0
- package/dist/validate/adapters/files.d.ts.map +1 -1
- package/dist/validate/adapters/files.js +13 -3
- package/dist/validate/adapters/files.js.map +1 -1
- package/dist/validate/index.js +17 -2
- package/dist/validate/index.js.map +1 -1
- package/package.json +1 -1
- package/templates/README.md +115 -19
- package/templates/ci/github-automerge.yml +36 -0
- package/templates/ci/github-enrich.yml +27 -0
- package/templates/ci/github-pages.yml +64 -0
- package/templates/ci/github-validate.yml +157 -0
- package/templates/ci/github-verify-ci.yml +47 -0
- package/templates/ci/gitlab-ci.yml +148 -0
- package/templates/ci/gitlab-enrich.sh +26 -0
- package/templates/ci/gitlab-verify.yml +16 -0
- package/templates/ci/header.txt +19 -0
- package/templates/gitattributes +4 -0
- package/templates/gitignore +7 -2
- package/templates/package.json +18 -0
- package/templates/publish.toml +2 -2
- package/templates/github-pages.yml +0 -24
- package/templates/github-validate.yml +0 -51
- package/templates/gitlab-ci.yml +0 -13
- package/templates/reusable/gitlab-index-ci.yml +0 -125
package/dist/cli/dev.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright 2026 The Grimoire Authors
|
|
3
|
+
// `grim-indexer dev` — serve this index locally, so a change to an entry, the
|
|
4
|
+
// branding, or the custom CSS can be looked at before it is pushed. It renders
|
|
5
|
+
// through the same code path `build` does, so what is on screen is what the
|
|
6
|
+
// deploy will publish.
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { CliError, EXIT } from "./exit.js";
|
|
9
|
+
import { resolveOutDir } from "./out_dir.js";
|
|
10
|
+
/**
|
|
11
|
+
* A bad `--port` is a usage error (64), like every other bad flag. Passed
|
|
12
|
+
* through unchecked it reached Astro as `NaN` and surfaced as a raw zod issue
|
|
13
|
+
* dump naming no flag at all, exiting 1.
|
|
14
|
+
*/
|
|
15
|
+
function resolvePort(port) {
|
|
16
|
+
if (port === undefined)
|
|
17
|
+
return undefined;
|
|
18
|
+
const value = Number(port);
|
|
19
|
+
if (!Number.isInteger(value) || value < 1 || value > 65535) {
|
|
20
|
+
throw new CliError(`--port ${JSON.stringify(port)}: must be a port number (1-65535)`, EXIT.usage);
|
|
21
|
+
}
|
|
22
|
+
return value;
|
|
23
|
+
}
|
|
24
|
+
export async function dev(root, flags) {
|
|
25
|
+
const rootDir = path.resolve(root);
|
|
26
|
+
const outDir = resolveOutDir(rootDir, flags.outDir);
|
|
27
|
+
const port = resolvePort(flags.port);
|
|
28
|
+
const [{ loadConfig }, { compileIndex }, { devSite }] = await Promise.all([
|
|
29
|
+
import("../config.js"),
|
|
30
|
+
import("../data/index.js"),
|
|
31
|
+
import("../renderer/index.js"),
|
|
32
|
+
]);
|
|
33
|
+
const config = await loadConfig(rootDir);
|
|
34
|
+
const { count, namespaces } = await compileIndex({ root: rootDir, outDir });
|
|
35
|
+
const server = await devSite({ root: rootDir, outDir, config, port });
|
|
36
|
+
console.log(`\n ${server.url}\n`);
|
|
37
|
+
console.log(` ${count} package(s) across ${namespaces.length} namespace(s)`);
|
|
38
|
+
// `index.config.json` is read once, at boot, and baked into the bundle — the
|
|
39
|
+
// same thing a build does. Say so, or an edit that appears to do nothing
|
|
40
|
+
// reads as a bug in the renderer.
|
|
41
|
+
console.log(" editing index/ or index.config.json? restart to pick it up\n");
|
|
42
|
+
// The server owns the process from here. Astro's dev server keeps the event
|
|
43
|
+
// loop alive on its own, but the promise is what stops `run` from returning
|
|
44
|
+
// and letting the CLI set an exit code out from under it.
|
|
45
|
+
await new Promise((resolve) => {
|
|
46
|
+
for (const signal of ["SIGINT", "SIGTERM"]) {
|
|
47
|
+
process.on(signal, () => {
|
|
48
|
+
void server
|
|
49
|
+
.stop()
|
|
50
|
+
.catch(() => { })
|
|
51
|
+
.finally(resolve);
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
return EXIT.ok;
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=dev.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dev.js","sourceRoot":"","sources":["../../src/cli/dev.ts"],"names":[],"mappings":"AAAA,sCAAsC;AACtC,sCAAsC;AAEtC,8EAA8E;AAC9E,+EAA+E;AAC/E,4EAA4E;AAC5E,uBAAuB;AACvB,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAiB,MAAM,WAAW,CAAC;AAC1D,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAO7C;;;;GAIG;AACH,SAAS,WAAW,CAAC,IAAwB;IAC3C,IAAI,IAAI,KAAK,SAAS;QAAE,OAAO,SAAS,CAAC;IACzC,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC;IAC3B,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,KAAK,GAAG,CAAC,IAAI,KAAK,GAAG,KAAK,EAAE,CAAC;QAC3D,MAAM,IAAI,QAAQ,CAAC,UAAU,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,mCAAmC,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC;IACpG,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,GAAG,CAAC,IAAY,EAAE,KAAe;IACrD,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IACnC,MAAM,MAAM,GAAG,aAAa,CAAC,OAAO,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IACpD,MAAM,IAAI,GAAG,WAAW,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAErC,MAAM,CAAC,EAAE,UAAU,EAAE,EAAE,EAAE,YAAY,EAAE,EAAE,EAAE,OAAO,EAAE,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC;QACxE,MAAM,CAAC,cAAc,CAAC;QACtB,MAAM,CAAC,kBAAkB,CAAC;QAC1B,MAAM,CAAC,sBAAsB,CAAC;KAC/B,CAAC,CAAC;IAEH,MAAM,MAAM,GAAG,MAAM,UAAU,CAAC,OAAO,CAAC,CAAC;IACzC,MAAM,EAAE,KAAK,EAAE,UAAU,EAAE,GAAG,MAAM,YAAY,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,CAAC;IAC5E,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC;IAEtE,OAAO,CAAC,GAAG,CAAC,OAAO,MAAM,CAAC,GAAG,IAAI,CAAC,CAAC;IACnC,OAAO,CAAC,GAAG,CAAC,KAAK,KAAK,sBAAsB,UAAU,CAAC,MAAM,eAAe,CAAC,CAAC;IAC9E,6EAA6E;IAC7E,yEAAyE;IACzE,kCAAkC;IAClC,OAAO,CAAC,GAAG,CAAC,gEAAgE,CAAC,CAAC;IAE9E,4EAA4E;IAC5E,4EAA4E;IAC5E,0DAA0D;IAC1D,MAAM,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,EAAE;QAClC,KAAK,MAAM,MAAM,IAAI,CAAC,QAAQ,EAAE,SAAS,CAAU,EAAE,CAAC;YACpD,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,GAAG,EAAE;gBACtB,KAAK,MAAM;qBACR,IAAI,EAAE;qBACN,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC;qBACf,OAAO,CAAC,OAAO,CAAC,CAAC;YACtB,CAAC,CAAC,CAAC;QACL,CAAC;IACH,CAAC,CAAC,CAAC;IACH,OAAO,IAAI,CAAC,EAAE,CAAC;AACjB,CAAC"}
|
package/dist/cli/init.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { type Forge } from "../ci.js";
|
|
1
2
|
import { type ExitCode } from "./exit.js";
|
|
2
|
-
export type Forge
|
|
3
|
+
export type { Forge };
|
|
3
4
|
/** Everything `init` needs to render the scaffold. Flags and prompts both resolve into this. */
|
|
4
5
|
export interface InitAnswers {
|
|
5
6
|
name: string;
|
|
@@ -10,6 +11,7 @@ export interface InitAnswers {
|
|
|
10
11
|
logo: string;
|
|
11
12
|
forge: Forge;
|
|
12
13
|
git: boolean;
|
|
14
|
+
install: boolean;
|
|
13
15
|
withSkills: boolean;
|
|
14
16
|
/** This repo's own https URL — the announce target. `""` when nothing could derive it. */
|
|
15
17
|
repoUrl: string;
|
|
@@ -25,6 +27,7 @@ export interface InitFlags {
|
|
|
25
27
|
logo?: string;
|
|
26
28
|
forge?: Forge;
|
|
27
29
|
git?: boolean;
|
|
30
|
+
install?: boolean;
|
|
28
31
|
withSkills?: boolean;
|
|
29
32
|
repoUrl?: string;
|
|
30
33
|
force?: boolean;
|
|
@@ -38,6 +41,8 @@ export interface InitResult {
|
|
|
38
41
|
outcome: FileOutcome;
|
|
39
42
|
}>;
|
|
40
43
|
gitInitialized: boolean;
|
|
44
|
+
/** Whether `npm install` ran and wrote a lock. */
|
|
45
|
+
installed: boolean;
|
|
41
46
|
}
|
|
42
47
|
export declare function init(dir: string, flags: InitFlags, version: string): Promise<ExitCode>;
|
|
43
48
|
//# sourceMappingURL=init.d.ts.map
|
package/dist/cli/init.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/cli/init.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/cli/init.ts"],"names":[],"mappings":"AAYA,OAAO,EAA6D,KAAK,KAAK,EAAE,MAAM,UAAU,CAAC;AAGjG,OAAO,EAAkB,KAAK,QAAQ,EAAE,MAAM,WAAW,CAAC;AAkB1D,YAAY,EAAE,KAAK,EAAE,CAAC;AAEtB,gGAAgG;AAChG,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,KAAK,CAAC;IACb,GAAG,EAAE,OAAO,CAAC;IACb,OAAO,EAAE,OAAO,CAAC;IACjB,UAAU,EAAE,OAAO,CAAC;IACpB,0FAA0F;IAC1F,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,gDAAgD;AAChD,MAAM,WAAW,SAAS;IACxB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,KAAK,CAAC;IACd,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,4CAA4C;AAC5C,MAAM,MAAM,WAAW,GAAG,SAAS,GAAG,aAAa,GAAG,WAAW,GAAG,SAAS,CAAC;AAE9E,MAAM,WAAW,UAAU;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,WAAW,CAAA;KAAE,CAAC,CAAC;IACrD,cAAc,EAAE,OAAO,CAAC;IACxB,kDAAkD;IAClD,SAAS,EAAE,OAAO,CAAC;CACpB;AAopBD,wBAAsB,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC,CAmD5F"}
|
package/dist/cli/init.js
CHANGED
|
@@ -6,13 +6,11 @@
|
|
|
6
6
|
import { execFileSync } from "node:child_process";
|
|
7
7
|
import fs from "node:fs";
|
|
8
8
|
import path from "node:path";
|
|
9
|
-
import { fileURLToPath } from "node:url";
|
|
10
9
|
import * as prompts from "@clack/prompts";
|
|
10
|
+
import { loadCiConfig, renderCi, resolveCi, staleCi } from "../ci.js";
|
|
11
|
+
import { CONFIG_FILE } from "../config.js";
|
|
12
|
+
import { fromTemplate } from "../templates.js";
|
|
11
13
|
import { CliError, EXIT } from "./exit.js";
|
|
12
|
-
/** Scaffold templates ship beside `dist/`, so this resolves identically from `src/cli/` and `dist/cli/`. */
|
|
13
|
-
const TEMPLATE_DIR = fileURLToPath(new URL("../../templates/", import.meta.url));
|
|
14
|
-
/** `{{name}}`, but never GitHub Actions' `${{ ... }}` — hence the `$` lookbehind. */
|
|
15
|
-
const PLACEHOLDER = /(?<!\$)\{\{\s*([A-Za-z_]\w*)\s*\}\}/g;
|
|
16
14
|
const NAME_RE = /^[a-z0-9][a-z0-9._-]*$/;
|
|
17
15
|
/**
|
|
18
16
|
* What `[announce]` gets when nothing knew this repo's URL. It is not a
|
|
@@ -39,23 +37,6 @@ function titleCase(name) {
|
|
|
39
37
|
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
40
38
|
.join(" ");
|
|
41
39
|
}
|
|
42
|
-
function readTemplate(rel) {
|
|
43
|
-
const file = path.join(TEMPLATE_DIR, rel);
|
|
44
|
-
try {
|
|
45
|
-
return fs.readFileSync(file, "utf8");
|
|
46
|
-
}
|
|
47
|
-
catch {
|
|
48
|
-
throw new CliError(`scaffold template ${rel} is missing from ${TEMPLATE_DIR} — the installed package is incomplete`, EXIT.unavailable);
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
function render(template, vars, rel) {
|
|
52
|
-
return template.replace(PLACEHOLDER, (match, key) => {
|
|
53
|
-
if (!(key in vars)) {
|
|
54
|
-
throw new CliError(`scaffold template ${rel} references unknown placeholder {{${key}}}`);
|
|
55
|
-
}
|
|
56
|
-
return vars[key];
|
|
57
|
-
});
|
|
58
|
-
}
|
|
59
40
|
/** Validate a URL the user typed. Returns the reason it is bad, or `null`. */
|
|
60
41
|
function badUrl(value) {
|
|
61
42
|
let url;
|
|
@@ -76,6 +57,12 @@ function badName(value) {
|
|
|
76
57
|
}
|
|
77
58
|
return null;
|
|
78
59
|
}
|
|
60
|
+
/** What `SiteConfig.logo` accepts — checked here so a typo fails now, not at build time. */
|
|
61
|
+
function badLogo(value) {
|
|
62
|
+
if (value === "" || /^(\/|https?:\/\/)/i.test(value))
|
|
63
|
+
return null;
|
|
64
|
+
return "must be a site-root path (/logo.svg) or an http(s) URL";
|
|
65
|
+
}
|
|
79
66
|
/**
|
|
80
67
|
* The target dir's `origin` remote as an https URL — set when the repo was
|
|
81
68
|
* created on the forge and cloned before scaffolding into it. Any remote
|
|
@@ -92,6 +79,19 @@ function gitRemoteUrl(dir) {
|
|
|
92
79
|
catch {
|
|
93
80
|
return undefined; // no git, no repo, or no `origin`
|
|
94
81
|
}
|
|
82
|
+
return normalizeRepoUrl(remote);
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Reduce any repository URL to the https form the rest of this file derives
|
|
86
|
+
* from: Pages URL, forge, announce target and the site's own header link.
|
|
87
|
+
*
|
|
88
|
+
* Applied to `--repo-url` as well as to the git remote, because the obvious
|
|
89
|
+
* thing to paste into it is GitHub's clone box — `https://github.com/acme/idx.git`
|
|
90
|
+
* — and used verbatim that trailing `.git` became `https://acme.github.io/idx.git`
|
|
91
|
+
* as `site`, hence `/idx.git` as Astro's `base`, on a Pages deployment that
|
|
92
|
+
* serves `/idx`. Every link on the built site pointed one path segment wrong.
|
|
93
|
+
*/
|
|
94
|
+
function normalizeRepoUrl(remote) {
|
|
95
95
|
const bare = remote.replace(/\.git$/, "");
|
|
96
96
|
const scp = SCP_REMOTE.exec(bare);
|
|
97
97
|
if (scp)
|
|
@@ -128,6 +128,89 @@ function repoUrlFromPages(baseUrl) {
|
|
|
128
128
|
const project = url.pathname.split("/").filter(Boolean)[0] ?? url.hostname;
|
|
129
129
|
return `https://${pages[2]}.com/${pages[1]}/${project}`;
|
|
130
130
|
}
|
|
131
|
+
/**
|
|
132
|
+
* Say out loud what was read off the git remote, and what could not be.
|
|
133
|
+
*
|
|
134
|
+
* Everything below is inferred rather than asked, and an inference nobody
|
|
135
|
+
* sees is one nobody checks — a wrong `site` is only noticed once the deploy
|
|
136
|
+
* serves 404s from a subpath that does not exist. The silences matter as much
|
|
137
|
+
* as the hits: a self-hosted forge can serve Pages from anywhere, so there is
|
|
138
|
+
* no URL to guess, and an unrecognised host is not evidence of GitHub.
|
|
139
|
+
*/
|
|
140
|
+
function reportDerivation(what) {
|
|
141
|
+
const say = (label, value) => console.log(` ${label.padEnd(12)}${value}`);
|
|
142
|
+
if (!what.remoteUrl) {
|
|
143
|
+
say("no remote", "nothing to derive - set `site` in index.config.json yourself");
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
say(what.fromRemote ? "from origin" : "repository", what.remoteUrl);
|
|
147
|
+
if (what.forge)
|
|
148
|
+
say("forge", what.forge);
|
|
149
|
+
if (what.forgeFallback) {
|
|
150
|
+
say("forge", "github (the host is neither github nor gitlab - pass --forge)");
|
|
151
|
+
}
|
|
152
|
+
if (what.baseUrl) {
|
|
153
|
+
say("site", what.baseUrl);
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
say("site", "not derivable from this host - set `site` in index.config.json");
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Which forge hosts `repoUrl`. Read off the host, so a self-hosted
|
|
161
|
+
* `gitlab.example.com` or `github.acme.internal` lands on the right pipeline
|
|
162
|
+
* without anyone having to answer a question about it. Anything unrecognised
|
|
163
|
+
* returns `undefined` and the prompt asks.
|
|
164
|
+
*/
|
|
165
|
+
function forgeFromRepoUrl(repoUrl) {
|
|
166
|
+
let host;
|
|
167
|
+
try {
|
|
168
|
+
host = new URL(repoUrl).hostname.toLowerCase();
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
return undefined;
|
|
172
|
+
}
|
|
173
|
+
if (host === "github.com" || host.startsWith("github."))
|
|
174
|
+
return "github";
|
|
175
|
+
if (host === "gitlab.com" || host.startsWith("gitlab."))
|
|
176
|
+
return "gitlab";
|
|
177
|
+
return undefined;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* The Pages URL a forge serves `repoUrl` from — the inverse of
|
|
181
|
+
* [`repoUrlFromPages`], and the reason nobody has to know their own Pages URL
|
|
182
|
+
* to scaffold an index.
|
|
183
|
+
*
|
|
184
|
+
* `github.com/acme/idx` -> `https://acme.github.io/idx`, and the repository
|
|
185
|
+
* *named after* the Pages host is served from its root rather than a
|
|
186
|
+
* subpath. GitLab keeps everything below the top-level group in the path, so
|
|
187
|
+
* `gitlab.com/acme/team/idx` -> `https://acme.gitlab.io/team/idx`.
|
|
188
|
+
*
|
|
189
|
+
* Only the two public hosts are answered. A self-hosted instance can serve
|
|
190
|
+
* Pages from anywhere, and a custom domain is a fact this command cannot
|
|
191
|
+
* observe — both are why the value is a prompt default and not a decision.
|
|
192
|
+
*/
|
|
193
|
+
function pagesUrlFromRepo(repoUrl) {
|
|
194
|
+
let url;
|
|
195
|
+
try {
|
|
196
|
+
url = new URL(repoUrl);
|
|
197
|
+
}
|
|
198
|
+
catch {
|
|
199
|
+
return undefined;
|
|
200
|
+
}
|
|
201
|
+
const host = url.hostname.toLowerCase();
|
|
202
|
+
const forge = host === "github.com" ? "github" : host === "gitlab.com" ? "gitlab" : undefined;
|
|
203
|
+
if (!forge)
|
|
204
|
+
return undefined;
|
|
205
|
+
const segments = url.pathname.split("/").filter(Boolean);
|
|
206
|
+
if (segments.length < 2)
|
|
207
|
+
return undefined;
|
|
208
|
+
const pagesHost = `${segments[0]}.${forge}.io`.toLowerCase();
|
|
209
|
+
const project = segments.slice(1).join("/");
|
|
210
|
+
return project.toLowerCase() === pagesHost
|
|
211
|
+
? `https://${pagesHost}`
|
|
212
|
+
: `https://${pagesHost}/${project}`;
|
|
213
|
+
}
|
|
131
214
|
/**
|
|
132
215
|
* The `index/<host>/<namespace>/` this repo's entries land under: its path
|
|
133
216
|
* minus the repo itself — one segment on GitHub, possibly nested on GitLab.
|
|
@@ -156,6 +239,7 @@ async function resolveAnswers(dir, flags) {
|
|
|
156
239
|
["--registry", flags.registry, badName],
|
|
157
240
|
["--base-url", flags.baseUrl, badUrl],
|
|
158
241
|
["--repo-url", flags.repoUrl, badUrl],
|
|
242
|
+
["--logo", flags.logo, badLogo],
|
|
159
243
|
]) {
|
|
160
244
|
if (value !== undefined) {
|
|
161
245
|
const reason = check(value);
|
|
@@ -163,10 +247,30 @@ async function resolveAnswers(dir, flags) {
|
|
|
163
247
|
throw new CliError(`${flag} ${JSON.stringify(value)}: ${reason}`, EXIT.data);
|
|
164
248
|
}
|
|
165
249
|
}
|
|
250
|
+
// The git remote is the one thing a cloned-then-scaffolded repo already
|
|
251
|
+
// knows about itself, and everything else falls out of it: which forge runs
|
|
252
|
+
// the CI, where Pages will serve the site, and what `--announce` targets. A
|
|
253
|
+
// `--repo-url` overrides it, for scaffolding before the remote exists.
|
|
254
|
+
const remoteUrl = flags.repoUrl ? normalizeRepoUrl(flags.repoUrl) : gitRemoteUrl(dir);
|
|
255
|
+
const derivedBaseUrl = remoteUrl ? pagesUrlFromRepo(remoteUrl) : undefined;
|
|
256
|
+
const derivedForge = remoteUrl ? forgeFromRepoUrl(remoteUrl) : undefined;
|
|
166
257
|
if (flags.quick) {
|
|
167
258
|
const name = flags.name ?? defaultName;
|
|
168
|
-
|
|
169
|
-
|
|
259
|
+
// Falls back to localhost rather than a guess: `--quick` is the
|
|
260
|
+
// non-interactive path, and a wrong absolute URL is worse than an obvious
|
|
261
|
+
// placeholder the next step tells you to replace.
|
|
262
|
+
const baseUrl = flags.baseUrl ?? derivedBaseUrl ?? "http://localhost:4321";
|
|
263
|
+
// `--quick` asks nothing, so every one of these decisions is otherwise
|
|
264
|
+
// invisible — including the two that quietly did not happen: a self-hosted
|
|
265
|
+
// forge has no predictable Pages URL, and an unrecognised host falls back
|
|
266
|
+
// to GitHub Actions rather than guessing.
|
|
267
|
+
reportDerivation({
|
|
268
|
+
remoteUrl,
|
|
269
|
+
fromRemote: flags.repoUrl === undefined && remoteUrl !== undefined,
|
|
270
|
+
baseUrl: flags.baseUrl === undefined ? derivedBaseUrl : undefined,
|
|
271
|
+
forge: flags.forge === undefined ? derivedForge : undefined,
|
|
272
|
+
forgeFallback: flags.forge === undefined && derivedForge === undefined,
|
|
273
|
+
});
|
|
170
274
|
return {
|
|
171
275
|
name,
|
|
172
276
|
title: flags.title ?? titleCase(name),
|
|
@@ -174,15 +278,15 @@ async function resolveAnswers(dir, flags) {
|
|
|
174
278
|
registryAlias: flags.registry ?? name,
|
|
175
279
|
registryHost: flags.registryHost ?? "ghcr.io",
|
|
176
280
|
logo: flags.logo ?? "",
|
|
177
|
-
forge: flags.forge ?? "github",
|
|
281
|
+
forge: flags.forge ?? derivedForge ?? "github",
|
|
178
282
|
git: flags.git ?? true,
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
//
|
|
182
|
-
//
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
283
|
+
install: flags.install ?? true,
|
|
284
|
+
withSkills: flags.withSkills ?? false,
|
|
285
|
+
// The combined layout needs an announce target, and every layout wants
|
|
286
|
+
// the header's repository link, which has no default to fall back on.
|
|
287
|
+
// Empty stays empty — nothing is guessed, and a `publish.toml` with no
|
|
288
|
+
// target refuses to publish.
|
|
289
|
+
repoUrl: remoteUrl ?? repoUrlFromPages(baseUrl) ?? "",
|
|
186
290
|
};
|
|
187
291
|
}
|
|
188
292
|
prompts.intro("grim-indexer — new package index");
|
|
@@ -199,26 +303,36 @@ async function resolveAnswers(dir, flags) {
|
|
|
199
303
|
placeholder: titleCase(name),
|
|
200
304
|
defaultValue: titleCase(name),
|
|
201
305
|
})));
|
|
306
|
+
if (remoteUrl) {
|
|
307
|
+
prompts.log.step(`Read from ${flags.repoUrl ? "--repo-url" : "the `origin` remote"}: ${remoteUrl}`);
|
|
308
|
+
}
|
|
309
|
+
// Asked before the base URL, because it is what the base URL defaults from.
|
|
310
|
+
// A repo cloned from its forge answers this without the user typing.
|
|
311
|
+
const repoUrl = remoteUrl ??
|
|
312
|
+
(await ask(prompts.text({
|
|
313
|
+
message: "Repository URL this index lives in",
|
|
314
|
+
placeholder: "https://github.com/you/your-index",
|
|
315
|
+
defaultValue: "",
|
|
316
|
+
// Blank is allowed: it writes a placeholder that refuses to publish,
|
|
317
|
+
// which beats forcing a URL the user does not have yet.
|
|
318
|
+
validate: (value) => (value ? (badUrl(value) ?? undefined) : undefined),
|
|
319
|
+
})));
|
|
320
|
+
// The whole point of asking for the repository first: on github.com and
|
|
321
|
+
// gitlab.com the Pages URL follows from it, so the answer is usually Enter.
|
|
322
|
+
// It stays a prompt because a custom domain (a CNAME on GitHub Pages, a
|
|
323
|
+
// GitLab Pages domain) is a fact this command cannot observe. The message
|
|
324
|
+
// names where the default came from, so accepting it is a decision rather
|
|
325
|
+
// than a shrug.
|
|
326
|
+
const pagesUrl = repoUrl === remoteUrl ? derivedBaseUrl : pagesUrlFromRepo(repoUrl);
|
|
202
327
|
const baseUrl = flags.baseUrl ??
|
|
203
328
|
(await ask(prompts.text({
|
|
204
|
-
message:
|
|
205
|
-
|
|
206
|
-
|
|
329
|
+
message: pagesUrl
|
|
330
|
+
? `Base URL the index is served from (Enter for ${pagesUrl}, or type a custom domain)`
|
|
331
|
+
: "Base URL the index is served from",
|
|
332
|
+
placeholder: pagesUrl ?? "https://index.example.com",
|
|
333
|
+
defaultValue: pagesUrl ?? "",
|
|
334
|
+
validate: (value) => badUrl(value || (pagesUrl ?? "")) ?? undefined,
|
|
207
335
|
})));
|
|
208
|
-
// Asked only for the combined layout — the standalone one writes no
|
|
209
|
-
// `publish.toml` and so announces nothing.
|
|
210
|
-
const derivedRepoUrl = gitRemoteUrl(dir) ?? repoUrlFromPages(baseUrl);
|
|
211
|
-
const repoUrl = !flags.withSkills
|
|
212
|
-
? ""
|
|
213
|
-
: (flags.repoUrl ??
|
|
214
|
-
(await ask(prompts.text({
|
|
215
|
-
message: "Repository URL this index lives in (`grim publish --announce` targets it)",
|
|
216
|
-
placeholder: derivedRepoUrl ?? "https://github.com/you/your-index",
|
|
217
|
-
defaultValue: derivedRepoUrl ?? "",
|
|
218
|
-
// Blank is allowed: it writes a placeholder that refuses to publish,
|
|
219
|
-
// which beats forcing a URL the user does not have yet.
|
|
220
|
-
validate: (value) => (value ? (badUrl(value) ?? undefined) : undefined),
|
|
221
|
-
}))));
|
|
222
336
|
const registryAlias = flags.registry ??
|
|
223
337
|
(await ask(prompts.text({
|
|
224
338
|
message: "Registry alias packages are published under",
|
|
@@ -228,32 +342,62 @@ async function resolveAnswers(dir, flags) {
|
|
|
228
342
|
})));
|
|
229
343
|
const logo = flags.logo ??
|
|
230
344
|
(await ask(prompts.text({
|
|
231
|
-
message: "Brand logo (path or URL
|
|
345
|
+
message: "Brand logo (site-root path like /logo.svg, or a URL; blank for none)",
|
|
232
346
|
defaultValue: "",
|
|
347
|
+
validate: (value) => badLogo(value ?? "") ?? undefined,
|
|
233
348
|
})));
|
|
349
|
+
// Prompted, not assumed. This is the committed allowlist the contribution
|
|
350
|
+
// gate bounds every entry's `ref` by, and defaulting it silently left every
|
|
351
|
+
// index refusing anything not on ghcr.io - including its own packages.
|
|
352
|
+
const registryHost = flags.registryHost ??
|
|
353
|
+
(await ask(prompts.text({
|
|
354
|
+
message: "OCI registry host packages are pulled from (the gate's allowlist)",
|
|
355
|
+
placeholder: "ghcr.io",
|
|
356
|
+
defaultValue: "ghcr.io",
|
|
357
|
+
})));
|
|
358
|
+
// Derived from the repository's host when that is recognisable, so the
|
|
359
|
+
// common case never sees this question. One repository runs on one forge —
|
|
360
|
+
// rendering both left every index carrying a pipeline it would never run.
|
|
361
|
+
const detectedForge = repoUrl === remoteUrl ? derivedForge : forgeFromRepoUrl(repoUrl);
|
|
362
|
+
if (detectedForge)
|
|
363
|
+
prompts.log.step(`CI: ${detectedForge}, from the repository host`);
|
|
234
364
|
const forge = flags.forge ??
|
|
365
|
+
detectedForge ??
|
|
235
366
|
(await ask(prompts.select({
|
|
236
367
|
message: "CI to scaffold",
|
|
237
368
|
options: [
|
|
238
369
|
{ value: "github", label: "GitHub Actions" },
|
|
239
370
|
{ value: "gitlab", label: "GitLab CI" },
|
|
240
|
-
{ value: "both", label: "Both" },
|
|
241
371
|
],
|
|
242
372
|
initialValue: "github",
|
|
243
373
|
})));
|
|
244
374
|
const git = flags.git ??
|
|
245
375
|
(await ask(prompts.confirm({ message: "Initialize a git repository?", initialValue: true })));
|
|
376
|
+
// The lock is what pins the renderer this index builds and validates with,
|
|
377
|
+
// and CI runs `npm ci` against it — so an index without one is an index
|
|
378
|
+
// whose first push fails. Declining is still allowed; the next steps say
|
|
379
|
+
// what to run.
|
|
380
|
+
const install = flags.install ??
|
|
381
|
+
(await ask(prompts.confirm({
|
|
382
|
+
message: "Install dependencies now? (writes package-lock.json)",
|
|
383
|
+
initialValue: true,
|
|
384
|
+
})));
|
|
246
385
|
return {
|
|
247
386
|
name,
|
|
248
387
|
title,
|
|
249
388
|
baseUrl,
|
|
250
389
|
registryAlias,
|
|
251
|
-
registryHost
|
|
390
|
+
registryHost,
|
|
252
391
|
logo,
|
|
253
392
|
forge,
|
|
254
393
|
git,
|
|
394
|
+
install,
|
|
255
395
|
withSkills: flags.withSkills ?? false,
|
|
256
|
-
|
|
396
|
+
// The Pages-URL fallback the `--quick` path has: someone who answered the
|
|
397
|
+
// base URL but left the repository blank has still said where this index
|
|
398
|
+
// lives, and dropping that left `repoUrl` empty — no header link, and a
|
|
399
|
+
// `publish.toml` that refuses to announce.
|
|
400
|
+
repoUrl: repoUrl || (repoUrlFromPages(baseUrl) ?? ""),
|
|
257
401
|
};
|
|
258
402
|
}
|
|
259
403
|
/** Unwrap a clack prompt, turning Ctrl-C into a clean non-error abort. */
|
|
@@ -278,8 +422,22 @@ function siteConfig(answers) {
|
|
|
278
422
|
site: answers.baseUrl,
|
|
279
423
|
brand: answers.title,
|
|
280
424
|
description: `${answers.title}, a Grimoire package index.`,
|
|
281
|
-
|
|
425
|
+
// `logo`, not `favicon`. They are deliberately different keys (see
|
|
426
|
+
// `SiteConfig`): a favicon is drawn to read at 16px, a logo goes in the
|
|
427
|
+
// header and becomes the default `og:image`. The prompt asks for a brand
|
|
428
|
+
// logo, so that is where the answer belongs.
|
|
429
|
+
...(answers.logo ? { logo: answers.logo } : {}),
|
|
430
|
+
// The header's "github" link. Written whenever it could be derived —
|
|
431
|
+
// it has no default, because the only sane one would be somebody
|
|
432
|
+
// else's repository.
|
|
433
|
+
...(answers.repoUrl ? { repoUrl: answers.repoUrl } : {}),
|
|
282
434
|
registry: { alias: answers.registryAlias, index: answers.baseUrl },
|
|
435
|
+
// What the committed CI is rendered from. The remaining knobs
|
|
436
|
+
// (`nodeVersion`, `enrich`, `grimVersion`, `allowManualEdits`) are left
|
|
437
|
+
// to their defaults rather than written out — `grim-indexer ci` resolves
|
|
438
|
+
// them the same way, so an absent key and its default render identically.
|
|
439
|
+
// Which renderer runs is not here at all: that is `package-lock.json`.
|
|
440
|
+
ci: { forge: answers.forge },
|
|
283
441
|
});
|
|
284
442
|
}
|
|
285
443
|
/**
|
|
@@ -298,8 +456,19 @@ function indexPolicy(answers) {
|
|
|
298
456
|
trustedBots: [],
|
|
299
457
|
});
|
|
300
458
|
}
|
|
301
|
-
/**
|
|
302
|
-
|
|
459
|
+
/**
|
|
460
|
+
* The complete scaffold as (destination path, content) pairs — nothing
|
|
461
|
+
* touches disk yet.
|
|
462
|
+
*
|
|
463
|
+
* `ci` is the `ci` block that will be on disk when this returns, which is not
|
|
464
|
+
* always the one derived from the answers: `write` leaves an existing
|
|
465
|
+
* `index.config.json` alone unless `--force`. Rendering the workflows from
|
|
466
|
+
* the answers while the config kept its own values produced a tree whose
|
|
467
|
+
* committed CI did not match its committed config — and `ci --check`, which
|
|
468
|
+
* reads only the config, then failed on a tree `init` had just reported as
|
|
469
|
+
* successful.
|
|
470
|
+
*/
|
|
471
|
+
function plan(answers, version, ci) {
|
|
303
472
|
const vars = {
|
|
304
473
|
name: answers.name,
|
|
305
474
|
title: answers.title,
|
|
@@ -310,27 +479,26 @@ function plan(answers, version) {
|
|
|
310
479
|
announceRepo: answers.repoUrl || UNDERIVED,
|
|
311
480
|
announceNamespace: (answers.repoUrl && announceNamespace(answers.repoUrl)) || UNDERIVED,
|
|
312
481
|
version,
|
|
313
|
-
//
|
|
314
|
-
//
|
|
315
|
-
|
|
482
|
+
// `title` is free text and lands inside a JSON string literal in
|
|
483
|
+
// package.json. A quote in it would close that string and produce a
|
|
484
|
+
// manifest npm refuses to read. Every other template puts it in prose.
|
|
485
|
+
titleJson: JSON.stringify(answers.title).slice(1, -1),
|
|
316
486
|
};
|
|
317
|
-
const from = (rel, dest) => ({
|
|
318
|
-
path: dest,
|
|
319
|
-
content: render(readTemplate(rel), vars, rel),
|
|
320
|
-
});
|
|
487
|
+
const from = (rel, dest) => ({ path: dest, content: fromTemplate(rel, vars) });
|
|
321
488
|
const files = [
|
|
322
489
|
{ path: "index/.gitkeep", content: "" },
|
|
323
490
|
{ path: "index.config.json", content: siteConfig(answers) },
|
|
324
491
|
{ path: "index-policy.json", content: indexPolicy(answers) },
|
|
325
492
|
from("gitignore", ".gitignore"),
|
|
493
|
+
from("gitattributes", ".gitattributes"),
|
|
494
|
+
from("package.json", "package.json"),
|
|
326
495
|
from("README.md", "README.md"),
|
|
327
496
|
];
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
files.push(from("gitlab-ci.yml", ".gitlab-ci.yml"));
|
|
497
|
+
// The same renderer `grim-indexer ci` uses, driven by the `ci` block that
|
|
498
|
+
// will be on disk — so the scaffold is drift-free by construction and the
|
|
499
|
+
// guard it emits passes on the first push.
|
|
500
|
+
for (const [dest, content] of renderCi(resolveCi(ci))) {
|
|
501
|
+
files.push({ path: dest, content });
|
|
334
502
|
}
|
|
335
503
|
if (answers.withSkills) {
|
|
336
504
|
files.push({ path: "skills/.gitkeep", content: "" });
|
|
@@ -380,6 +548,32 @@ function initGit(dir) {
|
|
|
380
548
|
return false;
|
|
381
549
|
}
|
|
382
550
|
}
|
|
551
|
+
/**
|
|
552
|
+
* Generate `package-lock.json` by installing. The lock is the load-bearing
|
|
553
|
+
* part: CI runs `npm ci` against it, so a repository without one cannot build,
|
|
554
|
+
* and which renderer runs would otherwise be resolved fresh on every runner.
|
|
555
|
+
*
|
|
556
|
+
* A failure is reported and survived — the scaffold is complete either way,
|
|
557
|
+
* and the next steps say what to run.
|
|
558
|
+
*/
|
|
559
|
+
function npmInstall(dir) {
|
|
560
|
+
try {
|
|
561
|
+
// `shell` on Windows: npm ships as `npm.cmd`, libuv's non-shell PATH
|
|
562
|
+
// search only tries `.com`/`.exe`, and Node refuses to spawn a `.cmd`
|
|
563
|
+
// without it — so without this branch the install could never succeed
|
|
564
|
+
// there, and every Windows scaffold silently shipped without a lockfile.
|
|
565
|
+
execFileSync("npm", ["install"], {
|
|
566
|
+
cwd: dir,
|
|
567
|
+
stdio: "ignore",
|
|
568
|
+
shell: process.platform === "win32",
|
|
569
|
+
});
|
|
570
|
+
return true;
|
|
571
|
+
}
|
|
572
|
+
catch {
|
|
573
|
+
console.error("warning: `npm install` failed - run it yourself to write package-lock.json");
|
|
574
|
+
return false;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
383
577
|
function nextSteps(result, answers) {
|
|
384
578
|
const rel = path.relative(process.cwd(), result.dir);
|
|
385
579
|
// A relative path that climbs out of the working directory is worse than
|
|
@@ -387,18 +581,26 @@ function nextSteps(result, answers) {
|
|
|
387
581
|
const target = rel === "" ? "" : rel.startsWith("..") ? result.dir : rel;
|
|
388
582
|
const cd = target === "" ? "" : `cd ${target}\n`;
|
|
389
583
|
const steps = [
|
|
390
|
-
|
|
584
|
+
// The forge-host segment is mandatory: the gate refuses anything outside
|
|
585
|
+
// `index/<host>/<namespace>/<package>/metadata.json`, and a shallow first
|
|
586
|
+
// commit builds locally without complaint before failing every review.
|
|
587
|
+
"Add packages under index/<host>/<namespace>/<package>/metadata.json",
|
|
588
|
+
"Commit package-lock.json - CI installs from it, so it is not optional",
|
|
391
589
|
"Push to your forge — CI builds and publishes the site",
|
|
392
590
|
];
|
|
393
591
|
if (answers.baseUrl === "http://localhost:4321") {
|
|
394
|
-
|
|
592
|
+
// Both keys, because the placeholder lands in both: `site` drives the
|
|
593
|
+
// deployment URL, `registry.index` is the address this index hands its
|
|
594
|
+
// visitors to add it with. Naming only the first left the copy-paste
|
|
595
|
+
// block on the published site pointing at localhost.
|
|
596
|
+
steps.push("Set site and registry.index in index.config.json to the real site URL");
|
|
395
597
|
}
|
|
396
598
|
if (answers.withSkills && !answers.repoUrl) {
|
|
397
599
|
steps.push(`Set [announce] repository + namespace in publish.toml — both are ${UNDERIVED}, ` +
|
|
398
600
|
"and `grim publish --announce` refuses to run until they name this repo");
|
|
399
601
|
}
|
|
400
602
|
return [
|
|
401
|
-
`${cd}
|
|
603
|
+
`${cd}${result.installed ? "" : "npm install\n"}npm run dev # preview it locally`,
|
|
402
604
|
"",
|
|
403
605
|
"Then:",
|
|
404
606
|
...steps.map((step, i) => ` ${i + 1}. ${step}`),
|
|
@@ -408,9 +610,19 @@ export async function init(dir, flags, version) {
|
|
|
408
610
|
const target = path.resolve(dir);
|
|
409
611
|
const answers = await resolveAnswers(target, flags);
|
|
410
612
|
fs.mkdirSync(target, { recursive: true });
|
|
411
|
-
|
|
613
|
+
// Which `ci` block the workflows are rendered from depends on which one
|
|
614
|
+
// survives `write`: an existing `index.config.json` is kept unless
|
|
615
|
+
// `--force`, and its settings — not the answers — are what the committed
|
|
616
|
+
// CI has to match. A malformed one is a data error either way, and
|
|
617
|
+
// `loadCiConfig` raises it rather than papering over it with defaults.
|
|
618
|
+
const keepsExistingConfig = fs.existsSync(path.join(target, CONFIG_FILE)) && !(flags.force ?? false);
|
|
619
|
+
const ci = keepsExistingConfig
|
|
620
|
+
? await loadCiConfig(target)
|
|
621
|
+
: { forge: answers.forge };
|
|
622
|
+
const files = write(target, plan(answers, version, ci), flags.force ?? false);
|
|
412
623
|
const gitInitialized = answers.git ? initGit(target) : false;
|
|
413
|
-
const
|
|
624
|
+
const installed = answers.install ? npmInstall(target) : false;
|
|
625
|
+
const result = { dir: target, files, gitInitialized, installed };
|
|
414
626
|
for (const file of files) {
|
|
415
627
|
console.log(` ${file.outcome.padEnd(12)}${file.path}`);
|
|
416
628
|
}
|
|
@@ -419,6 +631,18 @@ export async function init(dir, flags, version) {
|
|
|
419
631
|
console.error(`\n${skipped.length} file(s) differ from the scaffold and were left alone. ` +
|
|
420
632
|
`Re-run with --force to overwrite them.`);
|
|
421
633
|
}
|
|
634
|
+
// Same contract `grim-indexer ci` has: generated CI this config no longer
|
|
635
|
+
// renders is reported, never deleted. Silence here left an orphaned
|
|
636
|
+
// pipeline behind — and the drift guard failing on the next push with no
|
|
637
|
+
// hint of where it came from.
|
|
638
|
+
const orphaned = await staleCi(target, renderCi(resolveCi(ci)));
|
|
639
|
+
for (const relative of orphaned) {
|
|
640
|
+
console.log(` ${"stale".padEnd(12)}${relative}`);
|
|
641
|
+
}
|
|
642
|
+
if (orphaned.length > 0) {
|
|
643
|
+
console.error(`\n${orphaned.length} generated file(s) are no longer rendered by ${CONFIG_FILE} — ` +
|
|
644
|
+
`delete them, or \`npm run ci:check\` keeps failing`);
|
|
645
|
+
}
|
|
422
646
|
prompts.note(nextSteps(result, answers), "Next steps");
|
|
423
647
|
prompts.outro(`Index "${answers.name}" ready in ${target}`);
|
|
424
648
|
return EXIT.ok;
|