baselane 0.1.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/LICENSE +191 -0
- package/README.md +40 -0
- package/bin/baselane.mjs +44 -0
- package/dist/apply.d.ts +16 -0
- package/dist/apply.js +103 -0
- package/dist/audit.d.ts +8 -0
- package/dist/audit.js +30 -0
- package/dist/bin.d.ts +1 -0
- package/dist/bin.js +2 -0
- package/dist/cli.d.ts +5 -0
- package/dist/cli.js +231 -0
- package/dist/draft-pack.d.ts +3 -0
- package/dist/draft-pack.js +22 -0
- package/dist/drift-cmd.d.ts +28 -0
- package/dist/drift-cmd.js +98 -0
- package/dist/graph.d.ts +10 -0
- package/dist/graph.js +44 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/install.d.ts +4 -0
- package/dist/install.js +12 -0
- package/dist/map.d.ts +15 -0
- package/dist/map.js +63 -0
- package/dist/publish.d.ts +27 -0
- package/dist/publish.js +76 -0
- package/dist/report.d.ts +7 -0
- package/dist/report.js +13 -0
- package/dist/update.d.ts +25 -0
- package/dist/update.js +84 -0
- package/package.json +53 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { auditRepo, formatAuditReport } from "./audit.js";
|
|
3
|
+
import { applyResolvedPack } from "./apply.js";
|
|
4
|
+
import { distributePack, GithubClient } from "@baselane/distribute";
|
|
5
|
+
import { reportAdoption } from "./report.js";
|
|
6
|
+
import { loadBuiltinPack, validatePack } from "@baselane/packs";
|
|
7
|
+
import { formatGraphSummary, runGraph } from "./graph.js";
|
|
8
|
+
import { formatMapSummary, runMap } from "./map.js";
|
|
9
|
+
import { runDraftPack, formatDraftSummary } from "./draft-pack.js";
|
|
10
|
+
import { runInstall, formatInstallReport } from "./install.js";
|
|
11
|
+
import { runUpdate } from "./update.js";
|
|
12
|
+
import { runDrift, formatDriftReport } from "./drift-cmd.js";
|
|
13
|
+
import { runPublish } from "./publish.js";
|
|
14
|
+
import { LocalDirFileSource, analyze, analyzeConventions, analyzeDesignTokens, narrateArchitecture, narrateDesign, pickSamples, pickDesignSamples, } from "@baselane/analyze";
|
|
15
|
+
// Exported so docs-site/src/pages/cli.ts can embed it verbatim at build time — the CLI reference
|
|
16
|
+
// page can never drift into a syntax this CLI doesn't actually accept.
|
|
17
|
+
export const USAGE = [
|
|
18
|
+
"usage:",
|
|
19
|
+
" baselane audit <dir> [--json]",
|
|
20
|
+
" baselane apply <dir> (--pack <id> | --pack-file <path>) [--force] [--dry-run]",
|
|
21
|
+
" baselane distribute <owner/repo> (--pack <id> | --pack-file <path>) [--portal <url>]",
|
|
22
|
+
" baselane draft-pack <dir> [--json]",
|
|
23
|
+
" baselane graph <dir> [--json]",
|
|
24
|
+
" baselane map <dir> [--json] [--llm] [--dry-run]",
|
|
25
|
+
" baselane install [github:owner/repo@ref | @scope/name[@version] | owner/repo@skill] [-g] [--dir <d>] [--dry-run] [--json]",
|
|
26
|
+
" baselane update [github:owner/repo | @scope/name] [-g] [--dir <d>] [--dry-run]",
|
|
27
|
+
" baselane drift [-g] [--dir <d>] [--json]",
|
|
28
|
+
" baselane publish @scope/name --source github:owner/repo --ref <tag> [--registry URL] [--json]",
|
|
29
|
+
].join("\n");
|
|
30
|
+
function argValue(argv, flag) {
|
|
31
|
+
const i = argv.indexOf(flag);
|
|
32
|
+
return i !== -1 ? argv[i + 1] : undefined;
|
|
33
|
+
}
|
|
34
|
+
// #6: --pack-file is the escape hatch for a custom pack the CLI doesn't ship (a portal-authored
|
|
35
|
+
// or AI-drafted one saved to disk) — loadBuiltinPack only ever knows built-in ids. Same rule as
|
|
36
|
+
// every other pack entry point in this repo: validatePack is the sole authority, so a hand-edited
|
|
37
|
+
// file gets exactly the same validation a built-in pack.json does before it's ever rendered.
|
|
38
|
+
async function loadPackFile(path) {
|
|
39
|
+
const raw = await readFile(path, "utf8");
|
|
40
|
+
return validatePack(JSON.parse(raw));
|
|
41
|
+
}
|
|
42
|
+
/** Resolves --pack/--pack-file into a WorkflowPack, requiring exactly one of the two. */
|
|
43
|
+
async function resolvePackArg(packId, packFile) {
|
|
44
|
+
return packFile ? loadPackFile(packFile) : loadBuiltinPack(packId);
|
|
45
|
+
}
|
|
46
|
+
export async function main(argv, deps = {}) {
|
|
47
|
+
const [cmd, dir] = argv;
|
|
48
|
+
// #29: an explicit help request is not an error — print usage to stdout and exit 0. A genuinely
|
|
49
|
+
// unknown or missing command still falls through to the stderr + exit-1 path below.
|
|
50
|
+
if (cmd === "--help" || cmd === "-h" || cmd === "help") {
|
|
51
|
+
console.log(USAGE);
|
|
52
|
+
return 0;
|
|
53
|
+
}
|
|
54
|
+
try {
|
|
55
|
+
if (cmd === "audit" && dir) {
|
|
56
|
+
const result = await auditRepo(dir);
|
|
57
|
+
console.log(argv.includes("--json") ? JSON.stringify(result, null, 2) : formatAuditReport(result));
|
|
58
|
+
return 0;
|
|
59
|
+
}
|
|
60
|
+
if (cmd === "apply" && dir) {
|
|
61
|
+
const packId = argValue(argv, "--pack");
|
|
62
|
+
const packFile = argValue(argv, "--pack-file");
|
|
63
|
+
if ((!packId && !packFile) || (packId && packFile)) {
|
|
64
|
+
console.error(USAGE);
|
|
65
|
+
return 1;
|
|
66
|
+
}
|
|
67
|
+
const pack = await resolvePackArg(packId, packFile);
|
|
68
|
+
const r = await applyResolvedPack(dir, pack, {
|
|
69
|
+
force: argv.includes("--force"),
|
|
70
|
+
dryRun: argv.includes("--dry-run"),
|
|
71
|
+
});
|
|
72
|
+
const label = argv.includes("--dry-run") ? "would write" : "wrote";
|
|
73
|
+
console.log(`baselane apply ${pack.id}: ${label} ${r.written.length} file(s)` +
|
|
74
|
+
(r.skipped.length ? `, skipped ${r.skipped.length} existing (use --force): ${r.skipped.join(", ")}` : ""));
|
|
75
|
+
return 0;
|
|
76
|
+
}
|
|
77
|
+
if (cmd === "graph" && dir) {
|
|
78
|
+
const r = await runGraph(dir);
|
|
79
|
+
console.log(argv.includes("--json") ? JSON.stringify(r.graph, null, 2) : formatGraphSummary(r.graph, r.written));
|
|
80
|
+
return 0;
|
|
81
|
+
}
|
|
82
|
+
if (cmd === "map" && dir) {
|
|
83
|
+
let narration = null;
|
|
84
|
+
let designNarration = null;
|
|
85
|
+
if (argv.includes("--llm")) {
|
|
86
|
+
const apiKey = process.env.BASELANE_ANTHROPIC_KEY ?? process.env.ANTHROPIC_API_KEY;
|
|
87
|
+
if (!apiKey) {
|
|
88
|
+
console.error("baselane map error: --llm needs BASELANE_ANTHROPIC_KEY or ANTHROPIC_API_KEY. " +
|
|
89
|
+
"Run without --llm for the deterministic map.");
|
|
90
|
+
return 1;
|
|
91
|
+
}
|
|
92
|
+
const source = new LocalDirFileSource(dir);
|
|
93
|
+
const [profile, conventions, samples, tokens, designSamples] = await Promise.all([
|
|
94
|
+
analyze(source),
|
|
95
|
+
analyzeConventions(source),
|
|
96
|
+
pickSamples(source),
|
|
97
|
+
analyzeDesignTokens(source),
|
|
98
|
+
pickDesignSamples(source),
|
|
99
|
+
]);
|
|
100
|
+
try {
|
|
101
|
+
narration = await narrateArchitecture({ profile, conventions, samples }, { apiKey, fetchImpl: deps.fetchImpl });
|
|
102
|
+
if (tokens.hasUiSurface) {
|
|
103
|
+
designNarration = await narrateDesign({ tokens, samples: designSamples }, { apiKey, fetchImpl: deps.fetchImpl });
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
catch (err) {
|
|
107
|
+
console.error(`baselane map error: ${err instanceof Error ? err.message : String(err)}`);
|
|
108
|
+
return 1;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const r = await runMap(dir, { dryRun: argv.includes("--dry-run"), narration, designNarration });
|
|
112
|
+
console.log(argv.includes("--json")
|
|
113
|
+
? JSON.stringify({ profile: r.profile, conventions: r.conventions, tokens: r.tokens }, null, 2)
|
|
114
|
+
: argv.includes("--dry-run")
|
|
115
|
+
? [r.architectureMd, r.designMd].filter((s) => s !== null).join("\n")
|
|
116
|
+
: formatMapSummary(r));
|
|
117
|
+
return 0;
|
|
118
|
+
}
|
|
119
|
+
if (cmd === "draft-pack" && dir) {
|
|
120
|
+
const result = await runDraftPack(dir);
|
|
121
|
+
console.log(argv.includes("--json") ? JSON.stringify(result.pack, null, 2) : formatDraftSummary(result));
|
|
122
|
+
return 0;
|
|
123
|
+
}
|
|
124
|
+
if (cmd === "distribute" && dir) {
|
|
125
|
+
const packId = argValue(argv, "--pack");
|
|
126
|
+
const packFile = argValue(argv, "--pack-file");
|
|
127
|
+
if ((!packId && !packFile) || (packId && packFile)) {
|
|
128
|
+
console.error(USAGE);
|
|
129
|
+
return 1;
|
|
130
|
+
}
|
|
131
|
+
const token = process.env.GITHUB_TOKEN;
|
|
132
|
+
if (!token) {
|
|
133
|
+
console.error("baselane error: GITHUB_TOKEN is required for distribute");
|
|
134
|
+
return 1;
|
|
135
|
+
}
|
|
136
|
+
const portalUrl = argValue(argv, "--portal");
|
|
137
|
+
const portalToken = process.env.BASELANE_PORTAL_TOKEN;
|
|
138
|
+
if (portalUrl && !portalToken) {
|
|
139
|
+
console.error("baselane error: BASELANE_PORTAL_TOKEN is required with --portal");
|
|
140
|
+
return 1;
|
|
141
|
+
}
|
|
142
|
+
const pack = await resolvePackArg(packId, packFile);
|
|
143
|
+
const r = await distributePack(new GithubClient({ token }), dir, pack);
|
|
144
|
+
console.log(`baselane distribute ${pack.id}: opened ${r.prUrl} (${r.fileCount} file(s) on ${r.branch})`);
|
|
145
|
+
if (portalUrl && portalToken) {
|
|
146
|
+
try {
|
|
147
|
+
await reportAdoption(portalUrl, portalToken, {
|
|
148
|
+
repo: dir,
|
|
149
|
+
packId: pack.id,
|
|
150
|
+
version: pack.version,
|
|
151
|
+
prUrl: r.prUrl,
|
|
152
|
+
});
|
|
153
|
+
console.log(`baselane distribute: adoption recorded at ${portalUrl}`);
|
|
154
|
+
}
|
|
155
|
+
catch (reportErr) {
|
|
156
|
+
console.error(`baselane warning: PR opened but adoption report failed: ${reportErr instanceof Error ? reportErr.message : String(reportErr)}`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return 0;
|
|
160
|
+
}
|
|
161
|
+
if (cmd === "install") {
|
|
162
|
+
const source = argv[1] && !argv[1].startsWith("-") ? argv[1] : undefined;
|
|
163
|
+
const json = argv.includes("--json");
|
|
164
|
+
const report = await runInstall({
|
|
165
|
+
source,
|
|
166
|
+
global: argv.includes("-g") || argv.includes("--global"),
|
|
167
|
+
dir: argValue(argv, "--dir"),
|
|
168
|
+
dryRun: argv.includes("--dry-run"),
|
|
169
|
+
json,
|
|
170
|
+
token: process.env.GITHUB_TOKEN,
|
|
171
|
+
registryBase: process.env.BASELANE_REGISTRY,
|
|
172
|
+
fetchImpl: deps.fetchImpl,
|
|
173
|
+
// --json means stdout must be pure JSON — route notices to stderr instead of the default console.log
|
|
174
|
+
log: json ? (l) => console.error(l) : undefined,
|
|
175
|
+
});
|
|
176
|
+
console.log(json ? JSON.stringify(report, null, 2) : formatInstallReport(report));
|
|
177
|
+
return 0;
|
|
178
|
+
}
|
|
179
|
+
if (cmd === "update") {
|
|
180
|
+
const name = argv[1] && !argv[1].startsWith("-") ? argv[1] : undefined;
|
|
181
|
+
const json = argv.includes("--json");
|
|
182
|
+
await runUpdate({
|
|
183
|
+
name,
|
|
184
|
+
global: argv.includes("-g") || argv.includes("--global"),
|
|
185
|
+
dir: argValue(argv, "--dir"),
|
|
186
|
+
dryRun: argv.includes("--dry-run"),
|
|
187
|
+
token: process.env.GITHUB_TOKEN,
|
|
188
|
+
registryBase: process.env.BASELANE_REGISTRY,
|
|
189
|
+
fetchImpl: deps.fetchImpl,
|
|
190
|
+
log: json ? (l) => console.error(l) : undefined,
|
|
191
|
+
});
|
|
192
|
+
return 0;
|
|
193
|
+
}
|
|
194
|
+
if (cmd === "drift") {
|
|
195
|
+
const report = await runDrift({
|
|
196
|
+
global: argv.includes("-g") || argv.includes("--global"),
|
|
197
|
+
dir: argValue(argv, "--dir"),
|
|
198
|
+
});
|
|
199
|
+
console.log(argv.includes("--json") ? JSON.stringify(report, null, 2) : formatDriftReport(report));
|
|
200
|
+
return report.clean ? 0 : 1;
|
|
201
|
+
}
|
|
202
|
+
if (cmd === "publish") {
|
|
203
|
+
const name = argv[1] && !argv[1].startsWith("-") ? argv[1] : undefined;
|
|
204
|
+
const source = argValue(argv, "--source");
|
|
205
|
+
const ref = argValue(argv, "--ref");
|
|
206
|
+
if (!name || !source || !ref) {
|
|
207
|
+
console.error(USAGE);
|
|
208
|
+
return 1;
|
|
209
|
+
}
|
|
210
|
+
const json = argv.includes("--json");
|
|
211
|
+
const result = await runPublish({
|
|
212
|
+
name,
|
|
213
|
+
sourceName: source,
|
|
214
|
+
ref,
|
|
215
|
+
registry: argValue(argv, "--registry") ?? process.env.BASELANE_REGISTRY,
|
|
216
|
+
token: process.env.GITHUB_TOKEN,
|
|
217
|
+
fetchImpl: deps.fetchImpl,
|
|
218
|
+
// --json means stdout must be pure JSON — route notices to stderr, same idiom as install/update
|
|
219
|
+
log: json ? (l) => console.error(l) : undefined,
|
|
220
|
+
});
|
|
221
|
+
console.log(json ? JSON.stringify(result, null, 2) : result.message);
|
|
222
|
+
return result.ok ? 0 : 1;
|
|
223
|
+
}
|
|
224
|
+
console.error(USAGE);
|
|
225
|
+
return 1;
|
|
226
|
+
}
|
|
227
|
+
catch (err) {
|
|
228
|
+
console.error(`baselane error: ${err instanceof Error ? err.message : String(err)}`);
|
|
229
|
+
return 1;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { basename, resolve } from "node:path";
|
|
2
|
+
import { LocalDirFileSource, analyze, analyzeConventions } from "@baselane/analyze";
|
|
3
|
+
import { composeDraftPack } from "@baselane/packs";
|
|
4
|
+
export async function runDraftPack(dir) {
|
|
5
|
+
const source = new LocalDirFileSource(dir);
|
|
6
|
+
const [profile, conventions] = await Promise.all([analyze(source), analyzeConventions(source)]);
|
|
7
|
+
return composeDraftPack({ profile, conventions, repoName: basename(resolve(dir)) });
|
|
8
|
+
}
|
|
9
|
+
export function formatDraftSummary(result) {
|
|
10
|
+
const { pack } = result;
|
|
11
|
+
const caps = (pack.capabilities ?? []).map((c) => c.type).join(", ") || "none";
|
|
12
|
+
return [
|
|
13
|
+
"baselane draft-pack",
|
|
14
|
+
"",
|
|
15
|
+
`Pack: ${pack.id} — ${pack.title}`,
|
|
16
|
+
`Commands: ${pack.commands.map((c) => c.name).join(", ") || "none"}`,
|
|
17
|
+
`Capabilities: ${caps}`,
|
|
18
|
+
"",
|
|
19
|
+
"Paste this JSON into the pack builder (or save it) — it is an editable draft, not published:",
|
|
20
|
+
JSON.stringify(pack, null, 2),
|
|
21
|
+
].join("\n");
|
|
22
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export type PathDriftStatus = "ok" | "modified" | "missing" | "no-hash-recorded";
|
|
2
|
+
export type RegionDriftStatus = "ok" | "region-missing" | "version-drift" | "content-drift" | "file-missing";
|
|
3
|
+
export interface DriftReport {
|
|
4
|
+
manifestPath: string;
|
|
5
|
+
vendored: Array<{
|
|
6
|
+
path: string;
|
|
7
|
+
status: PathDriftStatus;
|
|
8
|
+
}>;
|
|
9
|
+
regions: Array<{
|
|
10
|
+
path: string;
|
|
11
|
+
packId: string;
|
|
12
|
+
pinnedVersion: string;
|
|
13
|
+
status: RegionDriftStatus;
|
|
14
|
+
}>;
|
|
15
|
+
capabilities: Array<{
|
|
16
|
+
capability: string;
|
|
17
|
+
path: string;
|
|
18
|
+
status: PathDriftStatus;
|
|
19
|
+
}>;
|
|
20
|
+
unreadable: string[];
|
|
21
|
+
clean: boolean;
|
|
22
|
+
}
|
|
23
|
+
export declare function runDrift(opts: {
|
|
24
|
+
global: boolean;
|
|
25
|
+
dir?: string;
|
|
26
|
+
homeDir?: string;
|
|
27
|
+
}): Promise<DriftReport>;
|
|
28
|
+
export declare function formatDriftReport(r: DriftReport): string;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { extractManagedRegions, sha256Hex } from "@baselane/packs";
|
|
4
|
+
import { readManifestWithFallback, targetLocation } from "@baselane/materialize";
|
|
5
|
+
/** null = absent (ENOENT/ENOTDIR); "unreadable" recorded by the caller on any other error. */
|
|
6
|
+
async function readOrClassify(abs, rel, unreadable) {
|
|
7
|
+
try {
|
|
8
|
+
return await readFile(abs, "utf8");
|
|
9
|
+
}
|
|
10
|
+
catch (err) {
|
|
11
|
+
const code = err.code;
|
|
12
|
+
if (code === "ENOENT" || code === "ENOTDIR")
|
|
13
|
+
return null;
|
|
14
|
+
unreadable.push(rel);
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export async function runDrift(opts) {
|
|
19
|
+
const loc = targetLocation({ global: opts.global, dir: opts.dir, homeDir: opts.homeDir });
|
|
20
|
+
const { manifest } = await readManifestWithFallback(loc);
|
|
21
|
+
if (manifest === null)
|
|
22
|
+
throw new Error(`drift: no harness.json at ${loc.manifestPath} — run baselane install first`);
|
|
23
|
+
const unreadable = [];
|
|
24
|
+
const vendored = [];
|
|
25
|
+
for (const entry of manifest.materialized.vendored) {
|
|
26
|
+
const before = unreadable.length;
|
|
27
|
+
const current = await readOrClassify(join(loc.targetDir, entry.path), entry.path, unreadable);
|
|
28
|
+
if (unreadable.length > before)
|
|
29
|
+
continue; // reported, not statused
|
|
30
|
+
const status = current === null ? "missing"
|
|
31
|
+
: entry.sha256 === null ? "no-hash-recorded"
|
|
32
|
+
: sha256Hex(current) === entry.sha256 ? "ok"
|
|
33
|
+
: "modified";
|
|
34
|
+
vendored.push({ path: entry.path, status });
|
|
35
|
+
}
|
|
36
|
+
const regions = [];
|
|
37
|
+
for (const entry of manifest.materialized.managedRegions) {
|
|
38
|
+
const before = unreadable.length;
|
|
39
|
+
const current = await readOrClassify(join(loc.targetDir, entry.path), entry.path, unreadable);
|
|
40
|
+
if (unreadable.length > before)
|
|
41
|
+
continue;
|
|
42
|
+
const found = current === null ? [] : extractManagedRegions(current);
|
|
43
|
+
for (const receipt of entry.regions) {
|
|
44
|
+
const region = found.find((r) => r.packId === receipt.packId);
|
|
45
|
+
const status = current === null ? "file-missing"
|
|
46
|
+
: !region ? "region-missing"
|
|
47
|
+
: region.version !== receipt.version ? "version-drift"
|
|
48
|
+
: sha256Hex(region.body) !== receipt.sha256 ? "content-drift"
|
|
49
|
+
: "ok";
|
|
50
|
+
regions.push({ path: entry.path, packId: receipt.packId, pinnedVersion: receipt.version, status });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// Capability receipts (M4: committed-vs-cache split) — same hash-compare as vendored, reported
|
|
54
|
+
// under the capability's name. sha256 is always recorded on a receipt path (unlike legacy
|
|
55
|
+
// vendored entries), so no "no-hash-recorded" case here. Legacy manifests normalize
|
|
56
|
+
// materialized.capabilities to {} (validateManifest), so this loop is a no-op for them.
|
|
57
|
+
const capabilities = [];
|
|
58
|
+
for (const [capability, receipt] of Object.entries(manifest.materialized.capabilities)) {
|
|
59
|
+
for (const entry of receipt.paths) {
|
|
60
|
+
const before = unreadable.length;
|
|
61
|
+
const current = await readOrClassify(join(loc.targetDir, entry.path), entry.path, unreadable);
|
|
62
|
+
if (unreadable.length > before)
|
|
63
|
+
continue;
|
|
64
|
+
const status = current === null ? "missing" : sha256Hex(current) === entry.sha256 ? "ok" : "modified";
|
|
65
|
+
capabilities.push({ capability, path: entry.path, status });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const clean = unreadable.length === 0 &&
|
|
69
|
+
vendored.every((v) => v.status === "ok") &&
|
|
70
|
+
regions.every((r) => r.status === "ok") &&
|
|
71
|
+
capabilities.every((c) => c.status === "ok");
|
|
72
|
+
return { manifestPath: loc.manifestPath, vendored, regions, capabilities, unreadable, clean };
|
|
73
|
+
}
|
|
74
|
+
export function formatDriftReport(r) {
|
|
75
|
+
if (r.clean)
|
|
76
|
+
return `baselane drift: clean (${r.vendored.length} vendored file(s), ${r.regions.length} region(s) verified)`;
|
|
77
|
+
const lines = [`baselane drift: DRIFT DETECTED (${r.manifestPath})`];
|
|
78
|
+
for (const v of r.vendored) {
|
|
79
|
+
if (v.status === "ok")
|
|
80
|
+
continue;
|
|
81
|
+
lines.push(v.status === "no-hash-recorded"
|
|
82
|
+
? ` ~ ${v.path}: no hash recorded (pre-M2 install) — reinstall to record hashes`
|
|
83
|
+
: ` ! ${v.path}: ${v.status}`);
|
|
84
|
+
}
|
|
85
|
+
for (const g of r.regions) {
|
|
86
|
+
if (g.status === "ok")
|
|
87
|
+
continue;
|
|
88
|
+
lines.push(` ! ${g.path} [${g.packId}@${g.pinnedVersion}]: ${g.status}`);
|
|
89
|
+
}
|
|
90
|
+
for (const c of r.capabilities) {
|
|
91
|
+
if (c.status === "ok")
|
|
92
|
+
continue;
|
|
93
|
+
lines.push(` ! ${c.path} [capability:${c.capability}]: ${c.status}`);
|
|
94
|
+
}
|
|
95
|
+
for (const u of r.unreadable)
|
|
96
|
+
lines.push(` ? ${u}: unreadable — skipped (fix permissions and re-run)`);
|
|
97
|
+
return lines.join("\n");
|
|
98
|
+
}
|
package/dist/graph.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type ImportGraph } from "@baselane/analyze";
|
|
2
|
+
export interface GraphResult {
|
|
3
|
+
graph: ImportGraph;
|
|
4
|
+
/** Repo-relative paths written, in write order. */
|
|
5
|
+
written: string[];
|
|
6
|
+
}
|
|
7
|
+
/** Scans `dir` and writes `.baselane/graph.json` + `.baselane/graph/GRAPH.md` through the containment guard. */
|
|
8
|
+
export declare function runGraph(dir: string): Promise<GraphResult>;
|
|
9
|
+
/** One-screen human summary: totals + top fan-in + what was written. */
|
|
10
|
+
export declare function formatGraphSummary(graph: ImportGraph, written: string[]): string;
|
package/dist/graph.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
import { LocalDirFileSource, buildImportGraph, renderGraphMd } from "@baselane/analyze";
|
|
4
|
+
import { assertInsideDir } from "./apply.js";
|
|
5
|
+
/** Scans `dir` and writes `.baselane/graph.json` + `.baselane/graph/GRAPH.md` through the containment guard. */
|
|
6
|
+
export async function runGraph(dir) {
|
|
7
|
+
const graph = await buildImportGraph(new LocalDirFileSource(dir));
|
|
8
|
+
const outputs = [
|
|
9
|
+
[".baselane/graph.json", JSON.stringify(graph, null, 2) + "\n"],
|
|
10
|
+
[".baselane/graph/GRAPH.md", renderGraphMd(graph)],
|
|
11
|
+
];
|
|
12
|
+
const written = [];
|
|
13
|
+
for (const [rel, content] of outputs) {
|
|
14
|
+
const abs = assertInsideDir(dir, rel);
|
|
15
|
+
await mkdir(dirname(abs), { recursive: true });
|
|
16
|
+
await writeFile(abs, content, "utf8");
|
|
17
|
+
written.push(rel);
|
|
18
|
+
}
|
|
19
|
+
return { graph, written };
|
|
20
|
+
}
|
|
21
|
+
function topFanIn(graph, limit) {
|
|
22
|
+
const fanIn = new Map();
|
|
23
|
+
for (const e of graph.edges)
|
|
24
|
+
fanIn.set(e.to, (fanIn.get(e.to) ?? 0) + 1);
|
|
25
|
+
return [...fanIn.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).slice(0, limit);
|
|
26
|
+
}
|
|
27
|
+
/** One-screen human summary: totals + top fan-in + what was written. */
|
|
28
|
+
export function formatGraphSummary(graph, written) {
|
|
29
|
+
const modules = graph.nodes.filter((n) => n.kind === "module").length;
|
|
30
|
+
const externals = graph.nodes.filter((n) => n.kind === "external").length;
|
|
31
|
+
const top = topFanIn(graph, 5);
|
|
32
|
+
return [
|
|
33
|
+
"baselane graph",
|
|
34
|
+
"",
|
|
35
|
+
`Modules: ${modules}`,
|
|
36
|
+
`External packages: ${externals}`,
|
|
37
|
+
`Edges: ${graph.edges.length}`,
|
|
38
|
+
"",
|
|
39
|
+
"Top fan-in (most depended-on):",
|
|
40
|
+
...(top.length > 0 ? top.map(([id, count], i) => ` ${i + 1}. ${id} (${count})`) : [" (none)"]),
|
|
41
|
+
"",
|
|
42
|
+
`Wrote: ${written.join(", ")}`,
|
|
43
|
+
].join("\n");
|
|
44
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { auditRepo, formatAuditReport, type AuditResult } from "./audit.ts";
|
|
2
|
+
export { applyPack, type ApplyOptions, type ApplyResult } from "./apply.ts";
|
|
3
|
+
export { buildDistribution, distributePack, type Distribution, GithubApiError, GithubClient } from "@baselane/distribute";
|
|
4
|
+
export { main } from "./cli.ts";
|
|
5
|
+
export { reportAdoption, type AdoptionReport } from "./report.ts";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { auditRepo, formatAuditReport } from "./audit.js";
|
|
2
|
+
export { applyPack } from "./apply.js";
|
|
3
|
+
export { buildDistribution, distributePack, GithubApiError, GithubClient } from "@baselane/distribute";
|
|
4
|
+
export { main } from "./cli.js";
|
|
5
|
+
export { reportAdoption } from "./report.js";
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { runInstall, installFromManifest } from "@baselane/materialize";
|
|
2
|
+
export type { InstallOptions, InstallReport } from "@baselane/materialize";
|
|
3
|
+
import { type InstallReport } from "@baselane/materialize";
|
|
4
|
+
export declare function formatInstallReport(r: InstallReport): string;
|
package/dist/install.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export { runInstall, installFromManifest } from "@baselane/materialize";
|
|
2
|
+
import { decodeSkillPin } from "@baselane/materialize";
|
|
3
|
+
export function formatInstallReport(r) {
|
|
4
|
+
const head = `${r.dryRun ? "DRY RUN — " : ""}baselane install → ${r.targetDir}`;
|
|
5
|
+
const packs = Object.entries(r.packs).map(([n, p]) => {
|
|
6
|
+
const { ref, onlySkill } = decodeSkillPin(p);
|
|
7
|
+
return ` ${n}@${ref}${onlySkill ? ` (skill: ${onlySkill})` : ""}`;
|
|
8
|
+
});
|
|
9
|
+
const body = `${r.dryRun ? "would write" : "wrote"} ${r.writes.length} file(s), removed ${r.deletes.length}, ` +
|
|
10
|
+
`backed up ${r.driftedBackedUp.length} drifted (*.baselane-bak), left ${r.unchanged.length} unchanged`;
|
|
11
|
+
return [head, ...packs, body, ...r.notes.map((n) => ` note: ${n}`)].join("\n");
|
|
12
|
+
}
|
package/dist/map.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type AnalysisProfile, type ConventionsReport, type DesignNarration, type DesignTokensReport, type NarrationSections } from "@baselane/analyze";
|
|
2
|
+
export interface MapResult {
|
|
3
|
+
profile: AnalysisProfile;
|
|
4
|
+
conventions: ConventionsReport;
|
|
5
|
+
tokens: DesignTokensReport;
|
|
6
|
+
architectureMd: string;
|
|
7
|
+
designMd: string | null;
|
|
8
|
+
written: string[];
|
|
9
|
+
}
|
|
10
|
+
export declare function runMap(dir: string, opts?: {
|
|
11
|
+
dryRun?: boolean;
|
|
12
|
+
narration?: NarrationSections | null;
|
|
13
|
+
designNarration?: DesignNarration | null;
|
|
14
|
+
}): Promise<MapResult>;
|
|
15
|
+
export declare function formatMapSummary(result: MapResult): string;
|
package/dist/map.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { LocalDirFileSource, analyze, analyzeConventions, analyzeDesignTokens, buildImportGraph, renderArchitectureMd, renderDesignMd, DESIGN_REGION_ID, DESIGN_REGION_VERSION, SYSTEM_MAP_REGION_ID, SYSTEM_MAP_REGION_VERSION, } from "@baselane/analyze";
|
|
4
|
+
import { mergeManagedRegion } from "@baselane/packs";
|
|
5
|
+
import { assertInsideDir } from "./apply.js";
|
|
6
|
+
export async function runMap(dir, opts = {}) {
|
|
7
|
+
const source = new LocalDirFileSource(dir);
|
|
8
|
+
const [profile, graph, conventions, tokens] = await Promise.all([
|
|
9
|
+
analyze(source),
|
|
10
|
+
buildImportGraph(source),
|
|
11
|
+
analyzeConventions(source),
|
|
12
|
+
analyzeDesignTokens(source),
|
|
13
|
+
]);
|
|
14
|
+
const archBody = renderArchitectureMd({ profile, graph, conventions, narration: opts.narration ?? null });
|
|
15
|
+
const architectureMd = mergeManagedRegion(await readIfExists(join(dir, "ARCHITECTURE.md")), archBody, SYSTEM_MAP_REGION_ID, SYSTEM_MAP_REGION_VERSION);
|
|
16
|
+
let designMd = null;
|
|
17
|
+
if (tokens.hasUiSurface) {
|
|
18
|
+
const designBody = renderDesignMd({ tokens, narration: opts.designNarration ?? null });
|
|
19
|
+
designMd = mergeManagedRegion(await readIfExists(join(dir, "DESIGN.md")), designBody, DESIGN_REGION_ID, DESIGN_REGION_VERSION);
|
|
20
|
+
}
|
|
21
|
+
const written = [];
|
|
22
|
+
if (!opts.dryRun) {
|
|
23
|
+
const outputs = [
|
|
24
|
+
["ARCHITECTURE.md", architectureMd],
|
|
25
|
+
[".baselane/system-map/report.json", JSON.stringify({ profile, conventions, tokens }, null, 2) + "\n"],
|
|
26
|
+
];
|
|
27
|
+
if (designMd !== null)
|
|
28
|
+
outputs.push(["DESIGN.md", designMd]);
|
|
29
|
+
for (const [rel, content] of outputs) {
|
|
30
|
+
const abs = assertInsideDir(dir, rel);
|
|
31
|
+
await mkdir(dirname(abs), { recursive: true });
|
|
32
|
+
await writeFile(abs, content, "utf8");
|
|
33
|
+
written.push(rel);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return { profile, conventions, tokens, architectureMd, designMd, written };
|
|
37
|
+
}
|
|
38
|
+
/** Reads a file, returning null (not throwing) when it does not exist. */
|
|
39
|
+
async function readIfExists(path) {
|
|
40
|
+
try {
|
|
41
|
+
return await readFile(path, "utf8");
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
if (err.code !== "ENOENT")
|
|
45
|
+
throw err;
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
export function formatMapSummary(result) {
|
|
50
|
+
const hidden = result.conventions.rules.filter((r) => !r.enforced).length;
|
|
51
|
+
const design = result.tokens.hasUiSurface
|
|
52
|
+
? `Design tokens: ${result.tokens.colors.length} colors, ${result.tokens.fonts.length} fonts, ${result.tokens.radii.length} radii${result.written.includes("DESIGN.md") ? " — DESIGN.md written" : ""}`
|
|
53
|
+
: "Design: no UI surface detected — DESIGN.md skipped";
|
|
54
|
+
return [
|
|
55
|
+
"baselane map",
|
|
56
|
+
"",
|
|
57
|
+
`Languages: ${result.profile.languages.join(", ") || "(none)"}`,
|
|
58
|
+
`Conventions measured over ${result.conventions.sampledFiles} files — rules: ${result.conventions.rules.length} (${hidden} hidden), mixed: ${result.conventions.mixed.length}`,
|
|
59
|
+
design,
|
|
60
|
+
"",
|
|
61
|
+
result.written.length > 0 ? `Wrote: ${result.written.join(", ")}` : "(dry-run — nothing written)",
|
|
62
|
+
].join("\n");
|
|
63
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export interface PublishOptions {
|
|
2
|
+
/** `@scope/name` to publish under. */
|
|
3
|
+
name: string;
|
|
4
|
+
/** `github:owner/repo` source, per `isGitSourceName`. */
|
|
5
|
+
sourceName: string;
|
|
6
|
+
/** Version tag to resolve and publish from, e.g. "v1.2.0". */
|
|
7
|
+
ref: string;
|
|
8
|
+
registry?: string;
|
|
9
|
+
token?: string;
|
|
10
|
+
fetchImpl?: typeof fetch;
|
|
11
|
+
log?: (line: string) => void;
|
|
12
|
+
}
|
|
13
|
+
export interface PublishResult {
|
|
14
|
+
ok: boolean;
|
|
15
|
+
status: number;
|
|
16
|
+
name: string;
|
|
17
|
+
version: string;
|
|
18
|
+
sha: string;
|
|
19
|
+
message: string;
|
|
20
|
+
}
|
|
21
|
+
/** `baselane publish`: resolves `ref` → sha via `resolveGitSource`, validates the pack builds via
|
|
22
|
+
* `packFromFileset`, then POSTs to the registry's publish endpoint with the GitHub token as
|
|
23
|
+
* bearer auth. The token is read by the caller (cli.ts) from `GITHUB_TOKEN` and passed in here —
|
|
24
|
+
* this function never reads env itself (same seam as runInstall/runUpdate) — but it OWNS the
|
|
25
|
+
* "token required" check, since unlike install/update (which can proceed unauthenticated, just
|
|
26
|
+
* rate-limited), publish is inherently an authenticated action with no valid unauthenticated path. */
|
|
27
|
+
export declare function runPublish(opts: PublishOptions): Promise<PublishResult>;
|
package/dist/publish.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { parseGitSourceName, resolveGitSource } from "@baselane/distribute";
|
|
2
|
+
import { packFromFileset } from "@baselane/packs";
|
|
3
|
+
import { DEFAULT_REGISTRY } from "@baselane/materialize";
|
|
4
|
+
// Mirrors packages/packs/src/pack-from-fileset.ts's stampVersion tag grammar — kept in sync
|
|
5
|
+
// deliberately rather than exported/shared, since the two call sites need it for different
|
|
6
|
+
// reasons (stampVersion falls back to `0.0.0+sha` for a non-tag ref; publish instead REJECTS a
|
|
7
|
+
// non-tag ref outright, because the registry only accepts strict `X.Y.Z` semver — see
|
|
8
|
+
// apps/portal/src/registry-index.ts's VERSION_RE — and a `0.0.0+sha` publish would just bounce
|
|
9
|
+
// off the server as a 400 after already doing the network work).
|
|
10
|
+
const VERSION_TAG_RE = /^v?(\d+\.\d+\.\d+)$/;
|
|
11
|
+
/** `baselane publish`: resolves `ref` → sha via `resolveGitSource`, validates the pack builds via
|
|
12
|
+
* `packFromFileset`, then POSTs to the registry's publish endpoint with the GitHub token as
|
|
13
|
+
* bearer auth. The token is read by the caller (cli.ts) from `GITHUB_TOKEN` and passed in here —
|
|
14
|
+
* this function never reads env itself (same seam as runInstall/runUpdate) — but it OWNS the
|
|
15
|
+
* "token required" check, since unlike install/update (which can proceed unauthenticated, just
|
|
16
|
+
* rate-limited), publish is inherently an authenticated action with no valid unauthenticated path. */
|
|
17
|
+
export async function runPublish(opts) {
|
|
18
|
+
if (!opts.token) {
|
|
19
|
+
throw new Error("publish: GITHUB_TOKEN is required to publish");
|
|
20
|
+
}
|
|
21
|
+
const tagMatch = VERSION_TAG_RE.exec(opts.ref);
|
|
22
|
+
if (!tagMatch) {
|
|
23
|
+
throw new Error(`publish: "${opts.ref}" is not a semver version tag — registry versions are semver ` +
|
|
24
|
+
`(e.g. "v1.2.0"); publish from a version tag, not a branch, sha, or arbitrary ref`);
|
|
25
|
+
}
|
|
26
|
+
const version = tagMatch[1];
|
|
27
|
+
const registryBase = opts.registry ?? DEFAULT_REGISTRY;
|
|
28
|
+
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
29
|
+
const token = opts.token;
|
|
30
|
+
const { repo } = parseGitSourceName(opts.sourceName);
|
|
31
|
+
const resolved = await resolveGitSource({
|
|
32
|
+
name: opts.sourceName, ref: opts.ref, token, fetchImpl, onNotice: opts.log,
|
|
33
|
+
});
|
|
34
|
+
const { pack } = await packFromFileset(resolved.files, { repoName: repo, ref: opts.ref, sha: resolved.sha });
|
|
35
|
+
const res = await fetchImpl(`${registryBase}/registry/v1/publish`, {
|
|
36
|
+
method: "POST",
|
|
37
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
|
|
38
|
+
body: JSON.stringify({
|
|
39
|
+
name: opts.name,
|
|
40
|
+
version,
|
|
41
|
+
source: { name: opts.sourceName, ref: opts.ref, sha: resolved.sha },
|
|
42
|
+
packId: pack.id,
|
|
43
|
+
description: pack.summary.slice(0, 300),
|
|
44
|
+
}),
|
|
45
|
+
});
|
|
46
|
+
const shaShort = resolved.sha.slice(0, 7);
|
|
47
|
+
const base = { name: opts.name, version, sha: resolved.sha };
|
|
48
|
+
switch (res.status) {
|
|
49
|
+
case 201:
|
|
50
|
+
return { ...base, ok: true, status: 201, message: `published ${opts.name}@${version} (sha ${shaShort})` };
|
|
51
|
+
case 409:
|
|
52
|
+
return { ...base, ok: false, status: 409, message: `publish: ${opts.name}@${version} — version already published — bump the tag` };
|
|
53
|
+
case 403:
|
|
54
|
+
return {
|
|
55
|
+
...base, ok: false, status: 403,
|
|
56
|
+
message: `publish: "${opts.name}" does not match your authenticated GitHub user or org`,
|
|
57
|
+
};
|
|
58
|
+
case 401:
|
|
59
|
+
return { ...base, ok: false, status: 401, message: "publish: GITHUB_TOKEN was rejected — check it is valid and not expired" };
|
|
60
|
+
case 400: {
|
|
61
|
+
const detail = await readErrorDetail(res);
|
|
62
|
+
return { ...base, ok: false, status: 400, message: `publish: request rejected${detail ? ` (${detail})` : ""} — check the source/sha` };
|
|
63
|
+
}
|
|
64
|
+
default:
|
|
65
|
+
throw new Error(`publish: unexpected response HTTP ${res.status} from ${registryBase}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
async function readErrorDetail(res) {
|
|
69
|
+
try {
|
|
70
|
+
const body = (await res.json());
|
|
71
|
+
return typeof body.error === "string" ? body.error : "";
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return "";
|
|
75
|
+
}
|
|
76
|
+
}
|