@thebassclef/lite 0.0.1 → 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 +202 -0
- package/README.md +85 -4
- package/dist/cli.cjs +1610 -0
- package/dist/cli.js +1587 -0
- package/dist/index.cjs +4 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +4 -0
- package/package.json +51 -3
- package/substrate/.bassclef/lite-manifest.json +1794 -0
- package/substrate/.claude/agents/architect.md +70 -0
- package/substrate/.claude/agents/builder.md +114 -0
- package/substrate/.claude/agents/designer.md +156 -0
- package/substrate/.claude/agents/reviewer.md +88 -0
- package/substrate/.claude/hooks/artifact-ingestion-gate.sh +357 -0
- package/substrate/.claude/hooks/assert-verify-steering.sh +77 -0
- package/substrate/.claude/hooks/bassclef-source-config-validate.sh +215 -0
- package/substrate/.claude/hooks/bassclef-sync.sh +634 -0
- package/substrate/.claude/hooks/compound-noun-scrub.sh +292 -0
- package/substrate/.claude/hooks/kiss-expansion-inject.sh +69 -0
- package/substrate/.claude/hooks/plain-english-steering.sh +156 -0
- package/substrate/.claude/hooks/post-skill-friction-check.sh +177 -0
- package/substrate/.claude/hooks/post-skill-telemetry.sh +62 -0
- package/substrate/.claude/hooks/pre-build-gate.sh +511 -0
- package/substrate/.claude/hooks/pre-commit-gate.sh +452 -0
- package/substrate/.claude/hooks/session-end.sh +433 -0
- package/substrate/.claude/hooks/session-reflection.sh +303 -0
- package/substrate/.claude/hooks/skill-body-grade-gate.sh +219 -0
- package/substrate/.claude/hooks/skill-body-intent-drift.sh +107 -0
- package/substrate/.claude/hooks/skill-step-list-check.sh +171 -0
- package/substrate/.claude/hooks/state-validate.sh +271 -0
- package/substrate/.claude/hooks/substrate-clarity-gate.sh +1110 -0
- package/substrate/.claude/hooks/temperance-gate.sh +147 -0
- package/substrate/.claude/hooks/testing-tier-enforce.sh +233 -0
- package/substrate/.claude/hooks/turn-prose-grade-measure.sh +219 -0
- package/substrate/.claude/hooks/turn-prose-kiss-check.sh +434 -0
- package/substrate/.claude/hooks/vocabulary-migration-check.sh +171 -0
- package/substrate/.claude/hooks/whereami-utc-gate.sh +142 -0
- package/substrate/.claude/luminaries/alan-cooper.md +170 -0
- package/substrate/.claude/luminaries/alistair-cockburn.md +140 -0
- package/substrate/.claude/luminaries/amazon-pr-faq.md +34 -0
- package/substrate/.claude/luminaries/ash-maurya.md +121 -0
- package/substrate/.claude/luminaries/bill-buxton.md +210 -0
- package/substrate/.claude/luminaries/charles-sanders-peirce.md +150 -0
- package/substrate/.claude/luminaries/david-ogilvy.md +192 -0
- package/substrate/.claude/luminaries/don-norman.md +173 -0
- package/substrate/.claude/luminaries/edward-tufte.md +179 -0
- package/substrate/.claude/luminaries/eric-evans.md +160 -0
- package/substrate/.claude/luminaries/frederick-brooks.md +50 -0
- package/substrate/.claude/luminaries/gang-of-four.md +157 -0
- package/substrate/.claude/luminaries/jason-fried-dhh.md +46 -0
- package/substrate/.claude/luminaries/jesse-james-garrett.md +154 -0
- package/substrate/.claude/luminaries/john-ousterhout.md +94 -0
- package/substrate/.claude/luminaries/karl-popper.md +132 -0
- package/substrate/.claude/luminaries/kent-beck.md +168 -0
- package/substrate/.claude/luminaries/linus-torvalds.md +218 -0
- package/substrate/.claude/luminaries/martin-fowler.md +164 -0
- package/substrate/.claude/luminaries/michael-feathers.md +125 -0
- package/substrate/.claude/luminaries/michael-nygard.md +118 -0
- package/substrate/.claude/luminaries/robert-c-martin.md +164 -0
- package/substrate/.claude/luminaries/saltzer-schroeder.md +194 -0
- package/substrate/.claude/luminaries/sophia-prater.md +193 -0
- package/substrate/.claude/luminaries/stephen-toulmin.md +131 -0
- package/substrate/.claude/rules/artifact-ingestion.md +179 -0
- package/substrate/.claude/rules/assert-only-after-verify.md +137 -0
- package/substrate/.claude/rules/blocked-items.md +146 -0
- package/substrate/.claude/rules/branching.md +28 -0
- package/substrate/.claude/rules/commit-conventions.md +22 -0
- package/substrate/.claude/rules/context-engineering.md +202 -0
- package/substrate/.claude/rules/context-management.md +85 -0
- package/substrate/.claude/rules/destructive-operations.md +69 -0
- package/substrate/.claude/rules/diagnosis.md +38 -0
- package/substrate/.claude/rules/guardrails.md +73 -0
- package/substrate/.claude/rules/identifier-leak-prevention.md +104 -0
- package/substrate/.claude/rules/lite-manifest-schema-change-discipline.md +98 -0
- package/substrate/.claude/rules/oo-ad-entry-point.md +117 -0
- package/substrate/.claude/rules/operator-facing-prose.md +196 -0
- package/substrate/.claude/rules/plain-english-discipline.md +154 -0
- package/substrate/.claude/rules/pr-body-shape.md +317 -0
- package/substrate/.claude/rules/pr-strategy.md +167 -0
- package/substrate/.claude/rules/sdlc-gates.md +149 -0
- package/substrate/.claude/rules/session-artifacts.md +236 -0
- package/substrate/.claude/rules/state-schema-validation.md +162 -0
- package/substrate/.claude/rules/whereami-load-bearing.md +202 -0
- package/substrate/.claude/rules/writing-craft-discipline.md +92 -0
- package/substrate/.claude/skills/build/SKILL.md +640 -0
- package/substrate/.claude/skills/decompose/SKILL.md +280 -0
- package/substrate/.claude/skills/diagnose/SKILL.md +297 -0
- package/substrate/.claude/skills/howdoi/SKILL.md +105 -0
- package/substrate/.claude/skills/ia-model/SKILL.md +108 -0
- package/substrate/.claude/skills/interaction-design/SKILL.md +112 -0
- package/substrate/.claude/skills/interpret-input/SKILL.md +180 -0
- package/substrate/.claude/skills/kiss/SKILL.md +449 -0
- package/substrate/.claude/skills/launch/SKILL.md +924 -0
- package/substrate/.claude/skills/lean-canvas/SKILL.md +332 -0
- package/substrate/.claude/skills/longrun/SKILL.md +426 -0
- package/substrate/.claude/skills/luminary/SKILL.md +481 -0
- package/substrate/.claude/skills/ogilvy-writing-audit/SKILL.md +177 -0
- package/substrate/.claude/skills/onboard-repo/SKILL.md +2190 -0
- package/substrate/.claude/skills/pattern-review/SKILL.md +99 -0
- package/substrate/.claude/skills/personas/SKILL.md +207 -0
- package/substrate/.claude/skills/promote/SKILL.md +283 -0
- package/substrate/.claude/skills/requirement/SKILL.md +98 -0
- package/substrate/.claude/skills/retro/SKILL.md +117 -0
- package/substrate/.claude/skills/riff/SKILL.md +114 -0
- package/substrate/.claude/skills/roadmap-reconcile/SKILL.md +163 -0
- package/substrate/.claude/skills/session-end/SKILL.md +309 -0
- package/substrate/.claude/skills/session-log/SKILL.md +299 -0
- package/substrate/.claude/skills/skills/SKILL.md +228 -0
- package/substrate/.claude/skills/spec/SKILL.md +105 -0
- package/substrate/.claude/skills/sprint/SKILL.md +392 -0
- package/substrate/.claude/skills/stage/SKILL.md +384 -0
- package/substrate/.claude/skills/state-a-problem/SKILL.md +185 -0
- package/substrate/.claude/skills/temperance/SKILL.md +108 -0
- package/substrate/.claude/skills/use-case/SKILL.md +417 -0
- package/substrate/.claude/skills/user-stories/SKILL.md +268 -0
- package/substrate/.claude/skills/value-prop/SKILL.md +251 -0
- package/substrate/.claude/skills/verify/SKILL.md +160 -0
- package/substrate/.claude/skills/whereami/SKILL.md +307 -0
- package/substrate/AGENTS.md +79 -0
- package/substrate/CLAUDE-lite.md +85 -0
- package/substrate/CODE_OF_CONDUCT.md +28 -0
- package/substrate/CONTRIBUTING.md +177 -0
- package/substrate/README.md +173 -0
- package/substrate/SECURITY.md +19 -0
- package/substrate/architecture/decisions/ADR-029-release-pipeline.md +79 -0
- package/substrate/architecture/decisions/ADR-031-non-breaking-changes-adopter-discipline.md +139 -0
- package/substrate/architecture/decisions/ADR-032-adopter-sync-dispatcher-architecture.md +192 -0
- package/substrate/architecture/decisions/ADR-039-release-tagging-scheme.md +145 -0
- package/substrate/architecture/decisions/ADR-040-planning-skill-vocabulary-and-lite-profile.md +155 -0
- package/substrate/architecture/decisions/ADR-044-unified-skill-body-template.md +162 -0
- package/substrate/lib/clean-artifacts-sweep.sh +112 -0
- package/substrate/lib/code-comment-discipline.sh +144 -0
- package/substrate/lib/composer-preflight.sh +459 -0
- package/substrate/lib/hook-inject.sh +255 -0
- package/substrate/lib/luminary-pick.sh +96 -0
- package/substrate/lib/output-discipline.sh +143 -0
- package/substrate/lib/rewrite-check.sh +214 -0
- package/substrate/lib/state.sh +1372 -0
- package/substrate/lib/telemetry.sh +205 -0
- package/substrate/lib/tier-check.sh +138 -0
- package/substrate/lib/tier-dependency-audit.sh +933 -0
- package/substrate/presence/install/bassclef-hook-connect.sh +178 -0
- package/substrate/presence/install/bassclef-sync.dispatcher.template.sh +841 -0
- package/substrate/presence/install/bassclef-sync.template.sh +2076 -0
- package/substrate/presence/install/schedule-auto-save.cron.sh +88 -0
- package/substrate/presence/install/schedule-auto-save.taskscheduler.md +122 -0
- package/substrate/scripts/aggregate-telemetry.sh +217 -0
- package/substrate/scripts/analyze-tier-dependencies.sh +239 -0
- package/substrate/scripts/generate-lite-manifest.sh +489 -0
- package/substrate/scripts/generate-tier-manifest.sh +28 -0
- package/substrate/scripts/intent-drift-check.sh +456 -0
- package/substrate/scripts/lite-manifest-drift-check.sh +146 -0
- package/substrate/scripts/render-lite-manifest-doc.sh +150 -0
- package/substrate/standards/bassclef-configs-schema.md +232 -0
- package/substrate/standards/bassclef-evolution.md +143 -0
- package/substrate/standards/bassclef-internal-jargon.md +244 -0
- package/substrate/standards/bassclef-managed-sentinel.md +96 -0
- package/substrate/standards/bassclef-source-config.md +228 -0
- package/substrate/standards/deferred-actions-schema.md +204 -0
- package/substrate/standards/docs-sync-allowlist.md +76 -0
- package/substrate/standards/graceful-exit.md +227 -0
- package/substrate/standards/hook-idempotency.md +102 -0
- package/substrate/standards/lite-manifest-schema-changes.md +75 -0
- package/substrate/standards/luminary-problem-patterns.md +481 -0
- package/substrate/standards/ogilvy-writing-rules.md +225 -0
- package/substrate/standards/opener-discipline.md +96 -0
- package/substrate/standards/operator-facing-prose-discipline.md +201 -0
- package/substrate/standards/project-directory-layout.md +276 -0
- package/substrate/standards/release-tagging.md +137 -0
- package/substrate/standards/sdlc-compliance.md +286 -0
- package/substrate/standards/secrets-lifecycle.md +210 -0
- package/substrate/standards/security-scanner-adapter.md +145 -0
- package/substrate/standards/skill-output-discipline.md +90 -0
- package/substrate/standards/state-spine.md +511 -0
- package/substrate/standards/tech-stack-config.md +109 -0
- package/substrate/standards/tier-dependency-analysis.md +167 -0
- package/substrate/standards/tier-runtime-deps/lite.md +57 -0
- package/substrate/standards/tier-tag-schema.md +155 -0
- package/substrate/standards/whereami-schema.md +301 -0
- package/substrate/standards/writing-guide.md +213 -0
- package/substrate/templates/deferred-action-template.md +45 -0
- package/substrate/templates/whereami-template.md +101 -0
package/dist/cli.cjs
ADDED
|
@@ -0,0 +1,1610 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __copyProps = (to, from, except, desc) => {
|
|
9
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
10
|
+
for (let key of __getOwnPropNames(from))
|
|
11
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
12
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
13
|
+
}
|
|
14
|
+
return to;
|
|
15
|
+
};
|
|
16
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
17
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
18
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
19
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
20
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
21
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
22
|
+
mod
|
|
23
|
+
));
|
|
24
|
+
const index = require("./index.cjs");
|
|
25
|
+
const node_fs = require("node:fs");
|
|
26
|
+
const node_path = require("node:path");
|
|
27
|
+
const node_os = require("node:os");
|
|
28
|
+
const node_crypto = require("node:crypto");
|
|
29
|
+
const node_url = require("node:url");
|
|
30
|
+
const node_module = require("node:module");
|
|
31
|
+
const promises = require("node:readline/promises");
|
|
32
|
+
const node_process = require("node:process");
|
|
33
|
+
var _documentCurrentScript = typeof document !== "undefined" ? document.currentScript : null;
|
|
34
|
+
const DEFAULTS$2 = {
|
|
35
|
+
force: false,
|
|
36
|
+
dryRun: false,
|
|
37
|
+
verbose: false,
|
|
38
|
+
allowRoot: false,
|
|
39
|
+
allowAnyDir: false,
|
|
40
|
+
dir: void 0
|
|
41
|
+
};
|
|
42
|
+
let ArgvError$1 = class ArgvError extends Error {
|
|
43
|
+
name = "ArgvError";
|
|
44
|
+
};
|
|
45
|
+
function parseInitArgs(argv) {
|
|
46
|
+
const out = { ...DEFAULTS$2 };
|
|
47
|
+
let i = 0;
|
|
48
|
+
while (i < argv.length) {
|
|
49
|
+
const token = argv[i];
|
|
50
|
+
if (token === "--force") {
|
|
51
|
+
out.force = true;
|
|
52
|
+
i += 1;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (token === "--dry-run") {
|
|
56
|
+
out.dryRun = true;
|
|
57
|
+
i += 1;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (token === "--verbose") {
|
|
61
|
+
out.verbose = true;
|
|
62
|
+
i += 1;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (token === "--allow-root") {
|
|
66
|
+
out.allowRoot = true;
|
|
67
|
+
i += 1;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (token === "--allow-any-dir") {
|
|
71
|
+
out.allowAnyDir = true;
|
|
72
|
+
i += 1;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (token === "--dir") {
|
|
76
|
+
const value = argv[i + 1];
|
|
77
|
+
if (value === void 0 || value.startsWith("--")) {
|
|
78
|
+
throw new ArgvError$1("--dir requires a value");
|
|
79
|
+
}
|
|
80
|
+
out.dir = value;
|
|
81
|
+
i += 2;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (token.startsWith("--dir=")) {
|
|
85
|
+
out.dir = token.slice("--dir=".length);
|
|
86
|
+
i += 1;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (token.startsWith("--")) {
|
|
90
|
+
throw new ArgvError$1(`unknown flag: ${token}`);
|
|
91
|
+
}
|
|
92
|
+
throw new ArgvError$1(`unexpected argument: ${token}`);
|
|
93
|
+
}
|
|
94
|
+
return out;
|
|
95
|
+
}
|
|
96
|
+
class ResolveError extends Error {
|
|
97
|
+
name = "ResolveError";
|
|
98
|
+
}
|
|
99
|
+
function resolveTargetDir(input) {
|
|
100
|
+
const raw = input.cliArg ?? input.cwd;
|
|
101
|
+
const beforeReal = node_path.isAbsolute(raw) ? raw : node_path.resolve(input.cwd, raw);
|
|
102
|
+
let canonical;
|
|
103
|
+
try {
|
|
104
|
+
canonical = node_fs.realpathSync(beforeReal);
|
|
105
|
+
} catch (e) {
|
|
106
|
+
const err = e;
|
|
107
|
+
if (err.code === "ENOENT") {
|
|
108
|
+
throw new ResolveError(`target directory does not exist: ${beforeReal}`);
|
|
109
|
+
}
|
|
110
|
+
throw new ResolveError(`cannot resolve target directory: ${beforeReal} (${err.code ?? "unknown"})`);
|
|
111
|
+
}
|
|
112
|
+
if (!input.allowAnyDir) {
|
|
113
|
+
const home = node_os.homedir();
|
|
114
|
+
if (!isUnder(canonical, home)) {
|
|
115
|
+
throw new ResolveError(
|
|
116
|
+
`target directory is not under your home directory (${home}). Pass --allow-any-dir if this is intentional.`
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
if (typeof process.getuid === "function") {
|
|
120
|
+
const uid = process.getuid();
|
|
121
|
+
let stat;
|
|
122
|
+
try {
|
|
123
|
+
stat = node_fs.statSync(canonical);
|
|
124
|
+
} catch (e) {
|
|
125
|
+
const err = e;
|
|
126
|
+
throw new ResolveError(`cannot stat target directory (${err.code ?? "unknown"}): ${canonical}`);
|
|
127
|
+
}
|
|
128
|
+
if (stat.uid !== uid) {
|
|
129
|
+
throw new ResolveError(
|
|
130
|
+
`target directory is not owned by the current user. Pass --allow-any-dir if this is intentional.`
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return canonical;
|
|
136
|
+
}
|
|
137
|
+
function isUnder(child, parent) {
|
|
138
|
+
const rel = node_path.resolve(child);
|
|
139
|
+
const par = node_path.resolve(parent);
|
|
140
|
+
if (rel === par) return true;
|
|
141
|
+
return rel.startsWith(par + "/");
|
|
142
|
+
}
|
|
143
|
+
class WriteError extends Error {
|
|
144
|
+
name = "WriteError";
|
|
145
|
+
kind;
|
|
146
|
+
constructor(kind, message) {
|
|
147
|
+
super(message);
|
|
148
|
+
this.kind = kind;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
const FLAGS_CREATE = node_fs.constants.O_CREAT | node_fs.constants.O_EXCL | node_fs.constants.O_NOFOLLOW | node_fs.constants.O_WRONLY;
|
|
152
|
+
function writeSafely(path, content, opts = {}) {
|
|
153
|
+
const parent = node_path.dirname(path);
|
|
154
|
+
try {
|
|
155
|
+
node_fs.accessSync(parent, node_fs.constants.F_OK);
|
|
156
|
+
} catch {
|
|
157
|
+
throw new WriteError("ParentMissing", `parent directory does not exist: ${parent}`);
|
|
158
|
+
}
|
|
159
|
+
try {
|
|
160
|
+
node_fs.accessSync(parent, node_fs.constants.W_OK);
|
|
161
|
+
} catch {
|
|
162
|
+
throw new WriteError("ParentNotWritable", `parent directory is not writable: ${parent}`);
|
|
163
|
+
}
|
|
164
|
+
let existing;
|
|
165
|
+
try {
|
|
166
|
+
const st = node_fs.lstatSync(path);
|
|
167
|
+
if (st.isSymbolicLink()) existing = "symlink";
|
|
168
|
+
else if (st.isFile()) existing = "regular";
|
|
169
|
+
else existing = "other";
|
|
170
|
+
} catch (e) {
|
|
171
|
+
const err = e;
|
|
172
|
+
if (err.code === "ENOENT") existing = "none";
|
|
173
|
+
else throw new WriteError("Unknown", `cannot stat ${path}: ${err.code ?? "unknown"}`);
|
|
174
|
+
}
|
|
175
|
+
if (existing === "symlink") {
|
|
176
|
+
throw new WriteError("SymlinkRefused", `refusing to follow symlink at target path: ${path}`);
|
|
177
|
+
}
|
|
178
|
+
if (existing !== "none" && !opts.force) {
|
|
179
|
+
throw new WriteError("AlreadyExists", `file already exists (pass --force to overwrite): ${path}`);
|
|
180
|
+
}
|
|
181
|
+
if (existing !== "none" && opts.force) {
|
|
182
|
+
try {
|
|
183
|
+
node_fs.unlinkSync(path);
|
|
184
|
+
} catch (e) {
|
|
185
|
+
const err = e;
|
|
186
|
+
if (err.code !== "ENOENT") {
|
|
187
|
+
throw new WriteError("Unknown", `cannot remove existing entry at ${path}: ${err.code ?? "unknown"}`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
let fd;
|
|
192
|
+
const mode = opts.mode ?? 420;
|
|
193
|
+
try {
|
|
194
|
+
fd = node_fs.openSync(path, FLAGS_CREATE, mode);
|
|
195
|
+
} catch (e) {
|
|
196
|
+
const err = e;
|
|
197
|
+
if (err.code === "EEXIST") {
|
|
198
|
+
throw new WriteError("AlreadyExists", `file appeared during write (race): ${path}`);
|
|
199
|
+
}
|
|
200
|
+
if (err.code === "ELOOP" || err.code === "EMLINK" || err.code === "ENOTSUP") {
|
|
201
|
+
throw new WriteError("SymlinkRefused", `refusing to follow symlink at target path: ${path}`);
|
|
202
|
+
}
|
|
203
|
+
throw new WriteError("Unknown", `cannot open ${path}: ${err.code ?? "unknown"}`);
|
|
204
|
+
}
|
|
205
|
+
try {
|
|
206
|
+
node_fs.writeSync(fd, content);
|
|
207
|
+
} finally {
|
|
208
|
+
node_fs.closeSync(fd);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
function mkdirSafely(path) {
|
|
212
|
+
try {
|
|
213
|
+
const st = node_fs.lstatSync(path);
|
|
214
|
+
if (st.isSymbolicLink()) {
|
|
215
|
+
throw new WriteError("SymlinkRefused", `refusing to create over symlink: ${path}`);
|
|
216
|
+
}
|
|
217
|
+
if (st.isDirectory()) {
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
throw new WriteError("AlreadyExists", `cannot create directory: non-directory entry exists at ${path}`);
|
|
221
|
+
} catch (e) {
|
|
222
|
+
if (e instanceof WriteError) throw e;
|
|
223
|
+
const err = e;
|
|
224
|
+
if (err.code !== "ENOENT") {
|
|
225
|
+
throw new WriteError("Unknown", `cannot stat ${path}: ${err.code ?? "unknown"}`);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
try {
|
|
229
|
+
node_fs.mkdirSync(path, { recursive: true, mode: 493 });
|
|
230
|
+
} catch (e) {
|
|
231
|
+
const err = e;
|
|
232
|
+
if (err.code === "EACCES" || err.code === "EPERM") {
|
|
233
|
+
throw new WriteError("ParentNotWritable", `cannot create directory (permission): ${path}`);
|
|
234
|
+
}
|
|
235
|
+
throw new WriteError("Unknown", `cannot create directory: ${path} (${err.code ?? "unknown"})`);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
function hashContent(content) {
|
|
239
|
+
let normalized = content;
|
|
240
|
+
if (normalized.charCodeAt(0) === 65279) {
|
|
241
|
+
normalized = normalized.slice(1);
|
|
242
|
+
}
|
|
243
|
+
normalized = normalized.replace(/\r\n/g, "\n");
|
|
244
|
+
return node_crypto.createHash("sha256").update(normalized, "utf8").digest("hex");
|
|
245
|
+
}
|
|
246
|
+
const SETTINGS_TEMPLATE_VERSION = "0.0.1";
|
|
247
|
+
function settingsJsonTemplate(pkgVersion) {
|
|
248
|
+
const value = {
|
|
249
|
+
$bassclef: {
|
|
250
|
+
template: "settings.json",
|
|
251
|
+
template_version: SETTINGS_TEMPLATE_VERSION,
|
|
252
|
+
generated_by: "@thebassclef/core",
|
|
253
|
+
generated_by_version: pkgVersion
|
|
254
|
+
},
|
|
255
|
+
permissions: {
|
|
256
|
+
additionalDirectories: []
|
|
257
|
+
},
|
|
258
|
+
hooks: {}
|
|
259
|
+
};
|
|
260
|
+
return JSON.stringify(value, null, 2) + "\n";
|
|
261
|
+
}
|
|
262
|
+
const SUBSTRATE_CONFIG_TEMPLATE_VERSION = "0.0.1";
|
|
263
|
+
function substrateConfigMdTemplate(pkgVersion) {
|
|
264
|
+
return [
|
|
265
|
+
"---",
|
|
266
|
+
`bassclef_template: substrate.config.md`,
|
|
267
|
+
`bassclef_template_version: ${SUBSTRATE_CONFIG_TEMPLATE_VERSION}`,
|
|
268
|
+
`generated_by: "@thebassclef/core"`,
|
|
269
|
+
`generated_by_version: "${pkgVersion}"`,
|
|
270
|
+
"---",
|
|
271
|
+
"",
|
|
272
|
+
"# substrate.config.md",
|
|
273
|
+
"",
|
|
274
|
+
"## Configuration",
|
|
275
|
+
"gating: operator-gated",
|
|
276
|
+
"execution_mode: sequential",
|
|
277
|
+
"",
|
|
278
|
+
"## Active agents",
|
|
279
|
+
"agents: Shaper, PM, Designer, Architect, Builder, Reviewer, Deployer, Closer",
|
|
280
|
+
"",
|
|
281
|
+
"## Phases",
|
|
282
|
+
"phases: [Inception, Elaboration, Construction, Transition]",
|
|
283
|
+
"",
|
|
284
|
+
"## Iteration planning",
|
|
285
|
+
"iterations: informal",
|
|
286
|
+
"",
|
|
287
|
+
"## Risk register",
|
|
288
|
+
"risk-register: no",
|
|
289
|
+
"",
|
|
290
|
+
"## Budget",
|
|
291
|
+
"budget:",
|
|
292
|
+
" session_ceiling:",
|
|
293
|
+
" iteration_ceiling:",
|
|
294
|
+
"",
|
|
295
|
+
"## Hosting platform",
|
|
296
|
+
"# Leave `none` unless this project deploys a web UI.",
|
|
297
|
+
"# Enum: amplify | ec2-tailscale | vercel | netlify | cloudflare-pages | none",
|
|
298
|
+
"hosting_platform: none",
|
|
299
|
+
"",
|
|
300
|
+
"## Deploy targets",
|
|
301
|
+
"deploy_targets:",
|
|
302
|
+
" local: true",
|
|
303
|
+
" staging: false",
|
|
304
|
+
" prod: false",
|
|
305
|
+
""
|
|
306
|
+
].join("\n");
|
|
307
|
+
}
|
|
308
|
+
const MANIFEST_SCHEMA_VERSION = "0.1.0";
|
|
309
|
+
function manifestTemplate(input) {
|
|
310
|
+
const value = {
|
|
311
|
+
$bassclef: {
|
|
312
|
+
template: "init.manifest.json",
|
|
313
|
+
manifest_schema_version: MANIFEST_SCHEMA_VERSION,
|
|
314
|
+
generated_by: "@thebassclef/core",
|
|
315
|
+
generated_by_version: input.pkgVersion
|
|
316
|
+
},
|
|
317
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
318
|
+
target_dir: input.targetDir,
|
|
319
|
+
files: input.files
|
|
320
|
+
};
|
|
321
|
+
return JSON.stringify(value, null, 2) + "\n";
|
|
322
|
+
}
|
|
323
|
+
const MANIFEST_RELATIVE_PATH = ".bassclef/init.manifest.json";
|
|
324
|
+
class ManifestReadError extends Error {
|
|
325
|
+
name = "ManifestReadError";
|
|
326
|
+
kind;
|
|
327
|
+
constructor(kind, message) {
|
|
328
|
+
super(message);
|
|
329
|
+
this.kind = kind;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
function readManifest(targetDir) {
|
|
333
|
+
const path = node_path.join(targetDir, MANIFEST_RELATIVE_PATH);
|
|
334
|
+
let raw;
|
|
335
|
+
try {
|
|
336
|
+
raw = node_fs.readFileSync(path, "utf8");
|
|
337
|
+
} catch (e) {
|
|
338
|
+
const err = e;
|
|
339
|
+
if (err.code === "ENOENT") {
|
|
340
|
+
throw new ManifestReadError(
|
|
341
|
+
"Missing",
|
|
342
|
+
`not initialized — no manifest at ${path}. Run \`bassclef init\` (or \`bassclef init --force\` if managed files already exist).`
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
throw new ManifestReadError("Malformed", `cannot read manifest at ${path}: ${err.code ?? "unknown"}`);
|
|
346
|
+
}
|
|
347
|
+
let parsed;
|
|
348
|
+
try {
|
|
349
|
+
parsed = JSON.parse(raw);
|
|
350
|
+
} catch {
|
|
351
|
+
throw new ManifestReadError(
|
|
352
|
+
"Malformed",
|
|
353
|
+
`manifest is not valid JSON at ${path}. Repair it by hand or run \`bassclef init --force\` to re-baseline.`
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
if (typeof parsed !== "object" || parsed === null || !("$bassclef" in parsed) || typeof parsed.$bassclef !== "object" || parsed.$bassclef === null) {
|
|
357
|
+
throw new ManifestReadError(
|
|
358
|
+
"Malformed",
|
|
359
|
+
`manifest is missing the $bassclef block at ${path}. Repair or re-baseline via \`bassclef init --force\`.`
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
const marker = parsed.$bassclef;
|
|
363
|
+
const schema = marker.manifest_schema_version;
|
|
364
|
+
if (typeof schema !== "string") {
|
|
365
|
+
throw new ManifestReadError(
|
|
366
|
+
"Malformed",
|
|
367
|
+
`manifest lacks a $bassclef.manifest_schema_version field at ${path}.`
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
if (compareSchemaVersion(schema, MANIFEST_SCHEMA_VERSION) > 0) {
|
|
371
|
+
throw new ManifestReadError(
|
|
372
|
+
"SchemaTooNew",
|
|
373
|
+
`manifest schema version ${schema} is newer than this package understands (${MANIFEST_SCHEMA_VERSION}). Upgrade @thebassclef/core.`
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
const obj = parsed;
|
|
377
|
+
if (!Array.isArray(obj.files)) {
|
|
378
|
+
throw new ManifestReadError("Malformed", `manifest.files is not an array at ${path}.`);
|
|
379
|
+
}
|
|
380
|
+
return parsed;
|
|
381
|
+
}
|
|
382
|
+
function writeManifest$1(targetDir, manifest) {
|
|
383
|
+
const path = node_path.join(targetDir, MANIFEST_RELATIVE_PATH);
|
|
384
|
+
const body = JSON.stringify(manifest, null, 2) + "\n";
|
|
385
|
+
mkdirSafely(node_path.dirname(path));
|
|
386
|
+
writeSafely(path, body, { force: true });
|
|
387
|
+
}
|
|
388
|
+
function compareSchemaVersion(a, b) {
|
|
389
|
+
const A = a.split(".").map((x) => parseInt(x, 10));
|
|
390
|
+
const B = b.split(".").map((x) => parseInt(x, 10));
|
|
391
|
+
for (let i = 0; i < Math.max(A.length, B.length); i++) {
|
|
392
|
+
const av = A[i] ?? 0;
|
|
393
|
+
const bv = B[i] ?? 0;
|
|
394
|
+
if (av !== bv) return av - bv;
|
|
395
|
+
}
|
|
396
|
+
return 0;
|
|
397
|
+
}
|
|
398
|
+
async function computeConfigHashes(targetDir, paths) {
|
|
399
|
+
const { readFileSync: read } = await import("node:fs");
|
|
400
|
+
const { createHash } = await import("node:crypto");
|
|
401
|
+
const { existsSync } = await import("node:fs");
|
|
402
|
+
const out = {};
|
|
403
|
+
for (const rel of paths) {
|
|
404
|
+
const abs = node_path.join(targetDir, rel);
|
|
405
|
+
if (!existsSync(abs)) continue;
|
|
406
|
+
const raw = read(abs, "utf8");
|
|
407
|
+
const normalized = raw.replace(/\r\n/g, "\n");
|
|
408
|
+
out[rel] = createHash("sha256").update(normalized, "utf8").digest("hex");
|
|
409
|
+
}
|
|
410
|
+
return out;
|
|
411
|
+
}
|
|
412
|
+
const SUBSTRATE_ROOT = "substrate";
|
|
413
|
+
const CONFIG_FILES = [
|
|
414
|
+
".claude/settings.json",
|
|
415
|
+
"substrate.config.md",
|
|
416
|
+
"substrate.secrets.md"
|
|
417
|
+
];
|
|
418
|
+
const CURRENT_ENTRY_COUNT = 149;
|
|
419
|
+
function copySubstrate(targetDir, options = {}) {
|
|
420
|
+
const bundleRoot = resolveBundleRoot(options.bundleRoot);
|
|
421
|
+
const manifest = loadBundledManifest(bundleRoot);
|
|
422
|
+
const result = {
|
|
423
|
+
copied: [],
|
|
424
|
+
refused: [],
|
|
425
|
+
errored: [],
|
|
426
|
+
erroredMessages: []
|
|
427
|
+
};
|
|
428
|
+
if (options.dryRun) result.wouldCopy = [];
|
|
429
|
+
const groups = groupByTopDirectory(manifest.entries);
|
|
430
|
+
for (const [directory, entries] of groups) {
|
|
431
|
+
let completedInGroup = 0;
|
|
432
|
+
for (const entry of entries) {
|
|
433
|
+
const outcome = copyOne(entry, bundleRoot, targetDir, options, result);
|
|
434
|
+
if (outcome !== "skipped") completedInGroup += 1;
|
|
435
|
+
}
|
|
436
|
+
if (options.onProgress) options.onProgress(directory, completedInGroup);
|
|
437
|
+
}
|
|
438
|
+
return result;
|
|
439
|
+
}
|
|
440
|
+
function copyOne(entry, bundleRoot, targetDir, options, result) {
|
|
441
|
+
const sourcePath = node_path.join(bundleRoot, entry.path);
|
|
442
|
+
const targetPath = node_path.join(targetDir, entry.path);
|
|
443
|
+
let content;
|
|
444
|
+
try {
|
|
445
|
+
content = node_fs.readFileSync(sourcePath, "utf8");
|
|
446
|
+
} catch (e) {
|
|
447
|
+
const err = e;
|
|
448
|
+
const message = `${entry.path} — cannot read bundled source (${err.code ?? "unknown"}). Reinstall @thebassclef/core to restore the bundle.`;
|
|
449
|
+
result.errored.push(entry.path);
|
|
450
|
+
result.erroredMessages?.push(message);
|
|
451
|
+
return "errored";
|
|
452
|
+
}
|
|
453
|
+
const declaredHash = stripHashPrefix(entry.content_hash);
|
|
454
|
+
const actualHash = sha256Hex(content);
|
|
455
|
+
if (declaredHash !== actualHash) {
|
|
456
|
+
const message = `${entry.path} — bundled content hash does not match the manifest. Reinstall @thebassclef/core to repair the bundle, then rerun bassclef init --force.`;
|
|
457
|
+
result.errored.push(entry.path);
|
|
458
|
+
result.erroredMessages?.push(message);
|
|
459
|
+
return "errored";
|
|
460
|
+
}
|
|
461
|
+
if (options.dryRun) {
|
|
462
|
+
result.wouldCopy?.push(entry.path);
|
|
463
|
+
return "wouldCopy";
|
|
464
|
+
}
|
|
465
|
+
try {
|
|
466
|
+
mkdirSafely(node_path.dirname(targetPath));
|
|
467
|
+
writeSafely(targetPath, content, { force: options.force ?? false });
|
|
468
|
+
result.copied.push(entry.path);
|
|
469
|
+
return "copied";
|
|
470
|
+
} catch (e) {
|
|
471
|
+
if (e instanceof WriteError) {
|
|
472
|
+
if (e.kind === "AlreadyExists") {
|
|
473
|
+
result.refused.push(entry.path);
|
|
474
|
+
return "refused";
|
|
475
|
+
}
|
|
476
|
+
if (e.kind === "SymlinkRefused") {
|
|
477
|
+
result.refused.push(entry.path);
|
|
478
|
+
return "refused";
|
|
479
|
+
}
|
|
480
|
+
const message = `${entry.path} — write failed (${e.kind}): ${e.message}. Check the target directory exists and is writable, then rerun.`;
|
|
481
|
+
result.errored.push(entry.path);
|
|
482
|
+
result.erroredMessages?.push(message);
|
|
483
|
+
return "errored";
|
|
484
|
+
}
|
|
485
|
+
throw e;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
function loadBundledManifest(bundleRoot) {
|
|
489
|
+
const manifestPath = node_path.join(bundleRoot, ".bassclef", "lite-manifest.json");
|
|
490
|
+
const raw = node_fs.readFileSync(manifestPath, "utf8");
|
|
491
|
+
const parsed = JSON.parse(raw);
|
|
492
|
+
if (!Array.isArray(parsed.entries)) {
|
|
493
|
+
throw new Error(
|
|
494
|
+
`bundled manifest at ${manifestPath} has no entries[] array. Reinstall @thebassclef/core to restore the bundle.`
|
|
495
|
+
);
|
|
496
|
+
}
|
|
497
|
+
return parsed;
|
|
498
|
+
}
|
|
499
|
+
function resolveBundleRoot(explicit) {
|
|
500
|
+
if (explicit && explicit.length > 0) return node_path.resolve(explicit);
|
|
501
|
+
const thisFile = node_url.fileURLToPath(typeof document === "undefined" ? require("url").pathToFileURL(__filename).href : _documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === "SCRIPT" && _documentCurrentScript.src || new URL("cli.cjs", document.baseURI).href);
|
|
502
|
+
let dir = node_path.dirname(thisFile);
|
|
503
|
+
for (let i = 0; i < 5; i += 1) {
|
|
504
|
+
try {
|
|
505
|
+
node_fs.readFileSync(node_path.join(dir, "package.json"), "utf8");
|
|
506
|
+
return node_path.join(dir, SUBSTRATE_ROOT);
|
|
507
|
+
} catch {
|
|
508
|
+
const parent = node_path.dirname(dir);
|
|
509
|
+
if (parent === dir) break;
|
|
510
|
+
dir = parent;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
const require$1 = node_module.createRequire(typeof document === "undefined" ? require("url").pathToFileURL(__filename).href : _documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === "SCRIPT" && _documentCurrentScript.src || new URL("cli.cjs", document.baseURI).href);
|
|
514
|
+
const pkgJsonPath = require$1.resolve("@thebassclef/core/package.json");
|
|
515
|
+
const pkgRoot = node_path.dirname(pkgJsonPath);
|
|
516
|
+
return node_path.join(pkgRoot, SUBSTRATE_ROOT);
|
|
517
|
+
}
|
|
518
|
+
function groupByTopDirectory(entries) {
|
|
519
|
+
const groups = /* @__PURE__ */ new Map();
|
|
520
|
+
for (const entry of entries) {
|
|
521
|
+
const parts = entry.path.split("/");
|
|
522
|
+
const top = parts.length >= 2 ? parts.slice(0, 2).join("/") : parts[0] ?? "";
|
|
523
|
+
if (!groups.has(top)) groups.set(top, []);
|
|
524
|
+
groups.get(top).push(entry);
|
|
525
|
+
}
|
|
526
|
+
return groups;
|
|
527
|
+
}
|
|
528
|
+
function stripHashPrefix(value) {
|
|
529
|
+
const prefix = "sha256:";
|
|
530
|
+
return value.startsWith(prefix) ? value.slice(prefix.length) : value;
|
|
531
|
+
}
|
|
532
|
+
function sha256Hex(content) {
|
|
533
|
+
return node_crypto.createHash("sha256").update(content).digest("hex");
|
|
534
|
+
}
|
|
535
|
+
function runInit(argv) {
|
|
536
|
+
let args;
|
|
537
|
+
try {
|
|
538
|
+
args = parseInitArgs(argv);
|
|
539
|
+
} catch (e) {
|
|
540
|
+
if (e instanceof ArgvError$1) {
|
|
541
|
+
process.stderr.write(`bassclef init: ${e.message}
|
|
542
|
+
`);
|
|
543
|
+
process.stderr.write(usage$2());
|
|
544
|
+
return 3;
|
|
545
|
+
}
|
|
546
|
+
throw e;
|
|
547
|
+
}
|
|
548
|
+
const currentUid = typeof process.getuid === "function" ? process.getuid() : void 0;
|
|
549
|
+
if (shouldRefuseRoot(currentUid, args.allowRoot)) {
|
|
550
|
+
process.stderr.write(
|
|
551
|
+
"bassclef init: refusing to run as root. Pass --allow-root if this is intentional.\n"
|
|
552
|
+
);
|
|
553
|
+
return 1;
|
|
554
|
+
}
|
|
555
|
+
let targetDir;
|
|
556
|
+
try {
|
|
557
|
+
targetDir = resolveTargetDir({
|
|
558
|
+
cwd: process.cwd(),
|
|
559
|
+
cliArg: args.dir,
|
|
560
|
+
allowAnyDir: args.allowAnyDir
|
|
561
|
+
});
|
|
562
|
+
} catch (e) {
|
|
563
|
+
if (e instanceof ResolveError) {
|
|
564
|
+
process.stderr.write(`bassclef init: ${e.message}
|
|
565
|
+
`);
|
|
566
|
+
return 1;
|
|
567
|
+
}
|
|
568
|
+
throw e;
|
|
569
|
+
}
|
|
570
|
+
if (!args.force && !args.dryRun) {
|
|
571
|
+
const manifestPath = node_path.join(targetDir, MANIFEST_RELATIVE_PATH);
|
|
572
|
+
if (node_fs.existsSync(manifestPath)) {
|
|
573
|
+
process.stderr.write(
|
|
574
|
+
"bassclef init: already initialized (manifest exists). Run `bassclef sync` to apply updates, or `bassclef init --force` to re-baseline.\n"
|
|
575
|
+
);
|
|
576
|
+
return 1;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
const plans = [
|
|
580
|
+
{
|
|
581
|
+
label: "settings.json",
|
|
582
|
+
relativePath: ".claude/settings.json",
|
|
583
|
+
fullPath: node_path.join(targetDir, ".claude", "settings.json"),
|
|
584
|
+
content: settingsJsonTemplate(index.version),
|
|
585
|
+
templateName: "settings.json",
|
|
586
|
+
templateVersion: SETTINGS_TEMPLATE_VERSION
|
|
587
|
+
},
|
|
588
|
+
{
|
|
589
|
+
label: "substrate.config.md",
|
|
590
|
+
relativePath: "substrate.config.md",
|
|
591
|
+
fullPath: node_path.join(targetDir, "substrate.config.md"),
|
|
592
|
+
content: substrateConfigMdTemplate(index.version),
|
|
593
|
+
templateName: "substrate.config.md",
|
|
594
|
+
templateVersion: SUBSTRATE_CONFIG_TEMPLATE_VERSION
|
|
595
|
+
}
|
|
596
|
+
];
|
|
597
|
+
if (args.dryRun) {
|
|
598
|
+
return runDryRun$1(plans);
|
|
599
|
+
}
|
|
600
|
+
const exitCode = runReal$1(plans, args.force, args.verbose, targetDir);
|
|
601
|
+
if (exitCode === 0) {
|
|
602
|
+
dispatchSubstrateCopy(targetDir, args.force, args.verbose);
|
|
603
|
+
}
|
|
604
|
+
return exitCode;
|
|
605
|
+
}
|
|
606
|
+
function dispatchSubstrateCopy(targetDir, force, verbose) {
|
|
607
|
+
let result;
|
|
608
|
+
try {
|
|
609
|
+
result = copySubstrate(targetDir, { force });
|
|
610
|
+
} catch (e) {
|
|
611
|
+
const err = e;
|
|
612
|
+
process.stderr.write(
|
|
613
|
+
`bassclef init: substrate copy failed — ${err.message}
|
|
614
|
+
cause: the installed @thebassclef/core package is missing the bundled substrate manifest.
|
|
615
|
+
fix: reinstall with \`npm install -g @thebassclef/core@latest --force\`.
|
|
616
|
+
If the reinstall does not help, file an issue at
|
|
617
|
+
https://github.com/sunj-labs/bassclef-cli/issues with your package version.
|
|
618
|
+
`
|
|
619
|
+
);
|
|
620
|
+
process.exit(2);
|
|
621
|
+
}
|
|
622
|
+
if (result.copied.length === 0 && result.refused.length === 0 && result.errored.length === 0) {
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
const groupCounts = /* @__PURE__ */ new Map();
|
|
626
|
+
for (const path of result.copied) {
|
|
627
|
+
const parts2 = path.split("/");
|
|
628
|
+
const top = parts2.length >= 2 ? parts2.slice(0, 2).join("/") : parts2[0] ?? "";
|
|
629
|
+
groupCounts.set(top, (groupCounts.get(top) ?? 0) + 1);
|
|
630
|
+
}
|
|
631
|
+
for (const [directory, count] of groupCounts) {
|
|
632
|
+
process.stdout.write(` ${directory}: ${count} files copied
|
|
633
|
+
`);
|
|
634
|
+
}
|
|
635
|
+
const parts = [];
|
|
636
|
+
if (result.copied.length > 0) parts.push(`${result.copied.length} substrate files copied`);
|
|
637
|
+
if (result.refused.length > 0) parts.push(`${result.refused.length} refused`);
|
|
638
|
+
if (result.errored.length > 0) parts.push(`${result.errored.length} error(s)`);
|
|
639
|
+
process.stdout.write(`bassclef init: ${parts.join(", ")}.
|
|
640
|
+
`);
|
|
641
|
+
if (verbose && result.erroredMessages) {
|
|
642
|
+
for (const msg of result.erroredMessages) {
|
|
643
|
+
process.stderr.write(` substrate: ${msg}
|
|
644
|
+
`);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
function runDryRun$1(plans) {
|
|
649
|
+
for (const p of plans) {
|
|
650
|
+
let planned;
|
|
651
|
+
let extra = "";
|
|
652
|
+
if (!node_fs.existsSync(p.fullPath)) {
|
|
653
|
+
planned = "would create";
|
|
654
|
+
} else {
|
|
655
|
+
try {
|
|
656
|
+
const st = node_fs.lstatSync(p.fullPath);
|
|
657
|
+
if (st.isSymbolicLink()) {
|
|
658
|
+
planned = "would refuse";
|
|
659
|
+
extra = " (symlink at target)";
|
|
660
|
+
} else {
|
|
661
|
+
planned = "would skip";
|
|
662
|
+
extra = " (already exists — use --force)";
|
|
663
|
+
}
|
|
664
|
+
} catch {
|
|
665
|
+
planned = "would skip";
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
process.stdout.write(` ${planned.padEnd(14)} ${p.fullPath}${extra}
|
|
669
|
+
`);
|
|
670
|
+
}
|
|
671
|
+
return 0;
|
|
672
|
+
}
|
|
673
|
+
function runReal$1(plans, force, verbose, targetDir) {
|
|
674
|
+
const results = [];
|
|
675
|
+
let anyRefused = false;
|
|
676
|
+
let anyError = false;
|
|
677
|
+
for (const p of plans) {
|
|
678
|
+
const parent = node_path.dirname(p.fullPath);
|
|
679
|
+
try {
|
|
680
|
+
mkdirSafely(parent);
|
|
681
|
+
} catch (e) {
|
|
682
|
+
if (e instanceof WriteError) {
|
|
683
|
+
results.push({ plan: p, outcome: "error", message: e.message });
|
|
684
|
+
anyError = true;
|
|
685
|
+
continue;
|
|
686
|
+
}
|
|
687
|
+
throw e;
|
|
688
|
+
}
|
|
689
|
+
try {
|
|
690
|
+
writeSafely(p.fullPath, p.content, { force });
|
|
691
|
+
results.push({ plan: p, outcome: "created" });
|
|
692
|
+
} catch (e) {
|
|
693
|
+
if (e instanceof WriteError) {
|
|
694
|
+
if (e.kind === "AlreadyExists") {
|
|
695
|
+
results.push({ plan: p, outcome: "unchanged" });
|
|
696
|
+
anyRefused = true;
|
|
697
|
+
continue;
|
|
698
|
+
}
|
|
699
|
+
if (e.kind === "SymlinkRefused") {
|
|
700
|
+
results.push({ plan: p, outcome: "refused", message: e.message });
|
|
701
|
+
anyError = true;
|
|
702
|
+
continue;
|
|
703
|
+
}
|
|
704
|
+
results.push({ plan: p, outcome: "error", message: e.message });
|
|
705
|
+
anyError = true;
|
|
706
|
+
continue;
|
|
707
|
+
}
|
|
708
|
+
throw e;
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
const created = results.filter((r) => r.outcome === "created").length;
|
|
712
|
+
const unchanged = results.filter((r) => r.outcome === "unchanged").length;
|
|
713
|
+
const errored = results.filter((r) => r.outcome === "error");
|
|
714
|
+
const refused = results.filter((r) => r.outcome === "refused").length;
|
|
715
|
+
if (verbose) {
|
|
716
|
+
for (const r of results) {
|
|
717
|
+
let label;
|
|
718
|
+
if (r.outcome === "error") label = `error (${r.message ?? "unknown"})`;
|
|
719
|
+
else if (r.outcome === "refused") label = "refused";
|
|
720
|
+
else label = r.outcome;
|
|
721
|
+
process.stdout.write(` ${label.padEnd(10)} ${r.plan.fullPath}
|
|
722
|
+
`);
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
for (const r of errored) {
|
|
726
|
+
const tag = r.outcome === "refused" ? "refused" : "error";
|
|
727
|
+
process.stderr.write(`bassclef init: ${tag}: ${r.message ?? "write failed"}: ${r.plan.fullPath}
|
|
728
|
+
`);
|
|
729
|
+
}
|
|
730
|
+
writeManifest(targetDir, results);
|
|
731
|
+
if (anyError) {
|
|
732
|
+
const parts = [];
|
|
733
|
+
if (created > 0) parts.push(`${created} created`);
|
|
734
|
+
if (unchanged > 0) parts.push(`${unchanged} unchanged`);
|
|
735
|
+
if (refused > 0) parts.push(`${refused} refused`);
|
|
736
|
+
const otherErrors = errored.length - refused;
|
|
737
|
+
if (otherErrors > 0) parts.push(`${otherErrors} error(s)`);
|
|
738
|
+
process.stderr.write(`bassclef init: ${parts.join(", ")}.
|
|
739
|
+
`);
|
|
740
|
+
return 2;
|
|
741
|
+
}
|
|
742
|
+
if (created === 0 && unchanged === plans.length) {
|
|
743
|
+
process.stdout.write("bassclef init: already initialized. No changes.\n");
|
|
744
|
+
return 0;
|
|
745
|
+
}
|
|
746
|
+
if (anyRefused && created > 0) {
|
|
747
|
+
process.stdout.write(
|
|
748
|
+
`bassclef init: ${created} created, ${unchanged} unchanged. Pass --force to overwrite.
|
|
749
|
+
`
|
|
750
|
+
);
|
|
751
|
+
process.stdout.write(
|
|
752
|
+
`bassclef init: your substrate lives under .claude/. Add .claude/ to .gitignore if you have not.
|
|
753
|
+
`
|
|
754
|
+
);
|
|
755
|
+
return 0;
|
|
756
|
+
}
|
|
757
|
+
process.stdout.write(`bassclef init: ${created} created, ${unchanged} unchanged.
|
|
758
|
+
`);
|
|
759
|
+
process.stdout.write(
|
|
760
|
+
`bassclef init: your substrate lives under .claude/. Add .claude/ to .gitignore if you have not.
|
|
761
|
+
`
|
|
762
|
+
);
|
|
763
|
+
return 0;
|
|
764
|
+
}
|
|
765
|
+
function shouldRefuseRoot(currentUid, allowRoot) {
|
|
766
|
+
if (currentUid === void 0) return false;
|
|
767
|
+
if (currentUid !== 0) return false;
|
|
768
|
+
return !allowRoot;
|
|
769
|
+
}
|
|
770
|
+
function writeManifest(targetDir, results) {
|
|
771
|
+
const entries = results.map((r) => {
|
|
772
|
+
const entry = {
|
|
773
|
+
path: r.plan.relativePath,
|
|
774
|
+
template: r.plan.templateName,
|
|
775
|
+
template_version: r.plan.templateVersion,
|
|
776
|
+
outcome: r.outcome
|
|
777
|
+
};
|
|
778
|
+
if (r.outcome === "created") {
|
|
779
|
+
entry.content_hash_sha256 = hashContent(r.plan.content);
|
|
780
|
+
entry.updated_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
781
|
+
}
|
|
782
|
+
return entry;
|
|
783
|
+
});
|
|
784
|
+
const manifestDir = node_path.join(targetDir, ".bassclef");
|
|
785
|
+
const manifestPath = node_path.join(manifestDir, "init.manifest.json");
|
|
786
|
+
const content = manifestTemplate({
|
|
787
|
+
pkgVersion: index.version,
|
|
788
|
+
targetDir,
|
|
789
|
+
files: entries
|
|
790
|
+
});
|
|
791
|
+
try {
|
|
792
|
+
mkdirSafely(manifestDir);
|
|
793
|
+
writeSafely(manifestPath, content, { force: true });
|
|
794
|
+
} catch {
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
function usage$2() {
|
|
798
|
+
return [
|
|
799
|
+
"",
|
|
800
|
+
"Usage:",
|
|
801
|
+
" bassclef init [options]",
|
|
802
|
+
"",
|
|
803
|
+
"Options:",
|
|
804
|
+
" --dir <path> Target directory. Default: current working directory.",
|
|
805
|
+
" --force Overwrite existing files. Default: refuse.",
|
|
806
|
+
" --dry-run Print what would happen; write nothing.",
|
|
807
|
+
" --allow-root Allow running as root. Default: refuse.",
|
|
808
|
+
" --allow-any-dir Allow --dir outside your home directory. Default: refuse.",
|
|
809
|
+
" --verbose Print per-file result.",
|
|
810
|
+
"",
|
|
811
|
+
"Files written under <target>:",
|
|
812
|
+
" .claude/settings.json Claude Code settings (minimal, opt-in blocks)",
|
|
813
|
+
" substrate.config.md Bassclef project manifest",
|
|
814
|
+
" .bassclef/init.manifest.json Record of what init wrote (used by sync)",
|
|
815
|
+
""
|
|
816
|
+
].join("\n");
|
|
817
|
+
}
|
|
818
|
+
const DEFAULTS$1 = {
|
|
819
|
+
force: false,
|
|
820
|
+
replaceEdits: false,
|
|
821
|
+
dryRun: false,
|
|
822
|
+
diff: false,
|
|
823
|
+
verbose: false,
|
|
824
|
+
allowRoot: false,
|
|
825
|
+
allowAnyDir: false,
|
|
826
|
+
dir: void 0
|
|
827
|
+
};
|
|
828
|
+
class SyncArgvError extends Error {
|
|
829
|
+
name = "SyncArgvError";
|
|
830
|
+
}
|
|
831
|
+
function parseSyncArgs(argv) {
|
|
832
|
+
const out = { ...DEFAULTS$1 };
|
|
833
|
+
let i = 0;
|
|
834
|
+
while (i < argv.length) {
|
|
835
|
+
const token = argv[i];
|
|
836
|
+
if (token === "--force") {
|
|
837
|
+
out.force = true;
|
|
838
|
+
i += 1;
|
|
839
|
+
continue;
|
|
840
|
+
}
|
|
841
|
+
if (token === "--replace-edits") {
|
|
842
|
+
out.replaceEdits = true;
|
|
843
|
+
i += 1;
|
|
844
|
+
continue;
|
|
845
|
+
}
|
|
846
|
+
if (token === "--dry-run") {
|
|
847
|
+
out.dryRun = true;
|
|
848
|
+
i += 1;
|
|
849
|
+
continue;
|
|
850
|
+
}
|
|
851
|
+
if (token === "--diff") {
|
|
852
|
+
out.diff = true;
|
|
853
|
+
i += 1;
|
|
854
|
+
continue;
|
|
855
|
+
}
|
|
856
|
+
if (token === "--verbose") {
|
|
857
|
+
out.verbose = true;
|
|
858
|
+
i += 1;
|
|
859
|
+
continue;
|
|
860
|
+
}
|
|
861
|
+
if (token === "--allow-root") {
|
|
862
|
+
out.allowRoot = true;
|
|
863
|
+
i += 1;
|
|
864
|
+
continue;
|
|
865
|
+
}
|
|
866
|
+
if (token === "--allow-any-dir") {
|
|
867
|
+
out.allowAnyDir = true;
|
|
868
|
+
i += 1;
|
|
869
|
+
continue;
|
|
870
|
+
}
|
|
871
|
+
if (token === "--dir") {
|
|
872
|
+
const value = argv[i + 1];
|
|
873
|
+
if (value === void 0 || value.startsWith("--")) {
|
|
874
|
+
throw new SyncArgvError("--dir requires a value");
|
|
875
|
+
}
|
|
876
|
+
out.dir = value;
|
|
877
|
+
i += 2;
|
|
878
|
+
continue;
|
|
879
|
+
}
|
|
880
|
+
if (token.startsWith("--dir=")) {
|
|
881
|
+
out.dir = token.slice("--dir=".length);
|
|
882
|
+
i += 1;
|
|
883
|
+
continue;
|
|
884
|
+
}
|
|
885
|
+
if (token.startsWith("--")) {
|
|
886
|
+
throw new SyncArgvError(`unknown flag: ${token}`);
|
|
887
|
+
}
|
|
888
|
+
throw new SyncArgvError(`unexpected argument: ${token}`);
|
|
889
|
+
}
|
|
890
|
+
return out;
|
|
891
|
+
}
|
|
892
|
+
const TEMPLATES = [
|
|
893
|
+
{
|
|
894
|
+
templateName: "settings.json",
|
|
895
|
+
currentVersion: SETTINGS_TEMPLATE_VERSION,
|
|
896
|
+
render: settingsJsonTemplate,
|
|
897
|
+
hasMarker: (c) => c.includes('"$bassclef"')
|
|
898
|
+
},
|
|
899
|
+
{
|
|
900
|
+
templateName: "substrate.config.md",
|
|
901
|
+
currentVersion: SUBSTRATE_CONFIG_TEMPLATE_VERSION,
|
|
902
|
+
render: substrateConfigMdTemplate,
|
|
903
|
+
hasMarker: (c) => c.includes("bassclef_template:")
|
|
904
|
+
}
|
|
905
|
+
];
|
|
906
|
+
function runSync(argv) {
|
|
907
|
+
let args;
|
|
908
|
+
try {
|
|
909
|
+
args = parseSyncArgs(argv);
|
|
910
|
+
} catch (e) {
|
|
911
|
+
if (e instanceof SyncArgvError) {
|
|
912
|
+
process.stderr.write(`bassclef sync: ${e.message}
|
|
913
|
+
`);
|
|
914
|
+
process.stderr.write(usage$1());
|
|
915
|
+
return 3;
|
|
916
|
+
}
|
|
917
|
+
throw e;
|
|
918
|
+
}
|
|
919
|
+
const currentUid = typeof process.getuid === "function" ? process.getuid() : void 0;
|
|
920
|
+
if (shouldRefuseRoot(currentUid, args.allowRoot)) {
|
|
921
|
+
process.stderr.write(
|
|
922
|
+
"bassclef sync: refusing to run as root. Pass --allow-root if this is intentional.\n"
|
|
923
|
+
);
|
|
924
|
+
return 1;
|
|
925
|
+
}
|
|
926
|
+
let targetDir;
|
|
927
|
+
try {
|
|
928
|
+
targetDir = resolveTargetDir({
|
|
929
|
+
cwd: process.cwd(),
|
|
930
|
+
cliArg: args.dir,
|
|
931
|
+
allowAnyDir: args.allowAnyDir
|
|
932
|
+
});
|
|
933
|
+
} catch (e) {
|
|
934
|
+
if (e instanceof ResolveError) {
|
|
935
|
+
process.stderr.write(`bassclef sync: ${e.message}
|
|
936
|
+
`);
|
|
937
|
+
return 1;
|
|
938
|
+
}
|
|
939
|
+
throw e;
|
|
940
|
+
}
|
|
941
|
+
let manifest;
|
|
942
|
+
try {
|
|
943
|
+
manifest = readManifest(targetDir);
|
|
944
|
+
} catch (e) {
|
|
945
|
+
if (e instanceof ManifestReadError) {
|
|
946
|
+
process.stderr.write(`bassclef sync: ${e.message}
|
|
947
|
+
`);
|
|
948
|
+
if (e.kind === "SchemaTooNew") return 4;
|
|
949
|
+
return 1;
|
|
950
|
+
}
|
|
951
|
+
throw e;
|
|
952
|
+
}
|
|
953
|
+
const decisions = manifest.files.map(
|
|
954
|
+
(entry) => classify(entry, targetDir, args)
|
|
955
|
+
);
|
|
956
|
+
if (args.dryRun) {
|
|
957
|
+
return runDryRun(decisions, args.diff);
|
|
958
|
+
}
|
|
959
|
+
return runReal(decisions, targetDir, manifest, args.verbose);
|
|
960
|
+
}
|
|
961
|
+
function classify(entry, targetDir, args) {
|
|
962
|
+
const fullPath = node_path.join(targetDir, entry.path);
|
|
963
|
+
const spec = TEMPLATES.find((t) => t.templateName === entry.template);
|
|
964
|
+
if (spec === void 0) {
|
|
965
|
+
return {
|
|
966
|
+
entry,
|
|
967
|
+
fullPath,
|
|
968
|
+
spec: void 0,
|
|
969
|
+
case: { kind: "Current" },
|
|
970
|
+
// unknown template — treat as no-op with a note
|
|
971
|
+
action: "no-op",
|
|
972
|
+
refusalReason: `unknown template: ${entry.template}`
|
|
973
|
+
};
|
|
974
|
+
}
|
|
975
|
+
if (!node_fs.existsSync(fullPath)) {
|
|
976
|
+
const canRestore = args.force && args.replaceEdits;
|
|
977
|
+
return {
|
|
978
|
+
entry,
|
|
979
|
+
fullPath,
|
|
980
|
+
spec,
|
|
981
|
+
case: { kind: "Deleted" },
|
|
982
|
+
action: canRestore ? "update" : "refuse",
|
|
983
|
+
refusalReason: canRestore ? void 0 : `${entry.path} — you have deleted this file since init. Sync will not recreate it without both --force and --replace-edits.`,
|
|
984
|
+
nextStep: canRestore ? void 0 : "bassclef sync --force --replace-edits"
|
|
985
|
+
};
|
|
986
|
+
}
|
|
987
|
+
let currentContent;
|
|
988
|
+
try {
|
|
989
|
+
currentContent = node_fs.readFileSync(fullPath, "utf8");
|
|
990
|
+
} catch (e) {
|
|
991
|
+
const err = e;
|
|
992
|
+
return {
|
|
993
|
+
entry,
|
|
994
|
+
fullPath,
|
|
995
|
+
spec,
|
|
996
|
+
case: { kind: "Edited", manifestUpdatedAt: entry.updated_at, currentMtime: "unknown" },
|
|
997
|
+
action: "refuse",
|
|
998
|
+
refusalReason: `${entry.path} — cannot read (${err.code ?? "unknown"}).`
|
|
999
|
+
};
|
|
1000
|
+
}
|
|
1001
|
+
if (!spec.hasMarker(currentContent)) {
|
|
1002
|
+
return {
|
|
1003
|
+
entry,
|
|
1004
|
+
fullPath,
|
|
1005
|
+
spec,
|
|
1006
|
+
case: { kind: "NoMarker" },
|
|
1007
|
+
action: "refuse",
|
|
1008
|
+
refusalReason: `${entry.path} — file is not managed by bassclef (no $bassclef marker). Sync will not overwrite it.`
|
|
1009
|
+
};
|
|
1010
|
+
}
|
|
1011
|
+
if (entry.content_hash_sha256 === void 0) {
|
|
1012
|
+
return {
|
|
1013
|
+
entry,
|
|
1014
|
+
fullPath,
|
|
1015
|
+
spec,
|
|
1016
|
+
case: { kind: "UnknownHash" },
|
|
1017
|
+
action: "refuse",
|
|
1018
|
+
refusalReason: `${entry.path} — manifest lacks content hash. This can happen on manifests written before content-hash tracking. Repair via \`bassclef init --force\` (drops any adopter edits) or add the current hash by hand.`,
|
|
1019
|
+
nextStep: "bassclef init --force"
|
|
1020
|
+
};
|
|
1021
|
+
}
|
|
1022
|
+
const currentHash = hashContent(currentContent);
|
|
1023
|
+
const templateOutput = spec.render(index.version);
|
|
1024
|
+
const templateOutputHash = hashContent(templateOutput);
|
|
1025
|
+
const currentTemplateVersion = spec.currentVersion;
|
|
1026
|
+
const versionOutdated = currentTemplateVersion !== entry.template_version;
|
|
1027
|
+
const adopterEdited = currentHash !== entry.content_hash_sha256;
|
|
1028
|
+
const wouldChange = templateOutputHash !== currentHash;
|
|
1029
|
+
if (!wouldChange) {
|
|
1030
|
+
return { entry, fullPath, spec, case: { kind: "Current" }, action: "no-op" };
|
|
1031
|
+
}
|
|
1032
|
+
if (adopterEdited) {
|
|
1033
|
+
const mtime = safeMtimeIso(fullPath);
|
|
1034
|
+
const versionNote = versionOutdated ? `
|
|
1035
|
+
a template update is also pending: ${entry.template_version} → ${currentTemplateVersion}` : "";
|
|
1036
|
+
return {
|
|
1037
|
+
entry,
|
|
1038
|
+
fullPath,
|
|
1039
|
+
spec,
|
|
1040
|
+
case: {
|
|
1041
|
+
kind: "Edited",
|
|
1042
|
+
manifestUpdatedAt: entry.updated_at,
|
|
1043
|
+
currentMtime: mtime
|
|
1044
|
+
},
|
|
1045
|
+
action: args.replaceEdits ? "update" : "refuse",
|
|
1046
|
+
refusalReason: args.replaceEdits ? void 0 : `${entry.path} — content differs from what init/sync last recorded.
|
|
1047
|
+
either you edited the file, or a prior sync did not finish.
|
|
1048
|
+
last sync recorded: ${entry.updated_at ?? "(unknown)"}
|
|
1049
|
+
file mtime is now: ${mtime}` + versionNote + `
|
|
1050
|
+
Run \`bassclef sync --diff\` to see the change.
|
|
1051
|
+
Run \`bassclef sync --replace-edits\` to replace your edits.`,
|
|
1052
|
+
nextStep: args.replaceEdits ? void 0 : "bassclef sync --replace-edits"
|
|
1053
|
+
};
|
|
1054
|
+
}
|
|
1055
|
+
if (versionOutdated) {
|
|
1056
|
+
return {
|
|
1057
|
+
entry,
|
|
1058
|
+
fullPath,
|
|
1059
|
+
spec,
|
|
1060
|
+
case: {
|
|
1061
|
+
kind: "NeedsUpdate",
|
|
1062
|
+
fromVersion: entry.template_version,
|
|
1063
|
+
toVersion: currentTemplateVersion
|
|
1064
|
+
},
|
|
1065
|
+
action: args.force ? "update" : "refuse",
|
|
1066
|
+
refusalReason: args.force ? void 0 : `${entry.path} — update available (template ${entry.template_version} → ${currentTemplateVersion}). Run \`bassclef sync --force\` to apply.`,
|
|
1067
|
+
nextStep: args.force ? void 0 : "bassclef sync --force"
|
|
1068
|
+
};
|
|
1069
|
+
}
|
|
1070
|
+
return { entry, fullPath, spec, case: { kind: "Current" }, action: "no-op" };
|
|
1071
|
+
}
|
|
1072
|
+
function safeMtimeIso(path) {
|
|
1073
|
+
try {
|
|
1074
|
+
return node_fs.statSync(path).mtime.toISOString();
|
|
1075
|
+
} catch {
|
|
1076
|
+
return "unknown";
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
function runDryRun(decisions, showDiff) {
|
|
1080
|
+
let anyChange = false;
|
|
1081
|
+
for (const d of decisions) {
|
|
1082
|
+
const label = describeCase(d.case);
|
|
1083
|
+
process.stdout.write(` ${label.padEnd(18)} ${d.entry.path}
|
|
1084
|
+
`);
|
|
1085
|
+
if (d.action === "update" || d.action === "refuse") anyChange = true;
|
|
1086
|
+
if (showDiff && d.spec !== void 0 && d.case.kind !== "Current") {
|
|
1087
|
+
process.stdout.write(` template current: ${d.spec.currentVersion}
|
|
1088
|
+
`);
|
|
1089
|
+
process.stdout.write(` manifest recorded: ${d.entry.template_version}
|
|
1090
|
+
`);
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
if (!anyChange) {
|
|
1094
|
+
process.stdout.write("bassclef sync: all up to date.\n");
|
|
1095
|
+
}
|
|
1096
|
+
return 0;
|
|
1097
|
+
}
|
|
1098
|
+
function runReal(decisions, targetDir, manifest, verbose) {
|
|
1099
|
+
let updated = 0;
|
|
1100
|
+
let refused = 0;
|
|
1101
|
+
let errors = 0;
|
|
1102
|
+
let noChange = 0;
|
|
1103
|
+
for (const d of decisions) {
|
|
1104
|
+
if (d.action === "no-op") {
|
|
1105
|
+
noChange += 1;
|
|
1106
|
+
continue;
|
|
1107
|
+
}
|
|
1108
|
+
if (d.action === "refuse") {
|
|
1109
|
+
refused += 1;
|
|
1110
|
+
process.stderr.write(`bassclef sync: refused: ${d.refusalReason ?? d.entry.path}
|
|
1111
|
+
`);
|
|
1112
|
+
continue;
|
|
1113
|
+
}
|
|
1114
|
+
if (d.spec === void 0) {
|
|
1115
|
+
noChange += 1;
|
|
1116
|
+
continue;
|
|
1117
|
+
}
|
|
1118
|
+
const newContent = d.spec.render(index.version);
|
|
1119
|
+
const newHash = hashContent(newContent);
|
|
1120
|
+
const parent = d.fullPath.substring(0, d.fullPath.lastIndexOf("/"));
|
|
1121
|
+
let preservedMode = 420;
|
|
1122
|
+
try {
|
|
1123
|
+
preservedMode = node_fs.lstatSync(d.fullPath).mode & 511;
|
|
1124
|
+
} catch {
|
|
1125
|
+
}
|
|
1126
|
+
try {
|
|
1127
|
+
mkdirSafely(parent);
|
|
1128
|
+
writeSafely(d.fullPath, newContent, { force: true, mode: preservedMode });
|
|
1129
|
+
} catch (e) {
|
|
1130
|
+
errors += 1;
|
|
1131
|
+
const msg = e instanceof WriteError ? e.message : e.message;
|
|
1132
|
+
process.stderr.write(`bassclef sync: write failed: ${d.entry.path}: ${msg}
|
|
1133
|
+
`);
|
|
1134
|
+
continue;
|
|
1135
|
+
}
|
|
1136
|
+
updateManifestEntry(manifest, d.entry.path, {
|
|
1137
|
+
content_hash_sha256: newHash,
|
|
1138
|
+
template_version: d.spec.currentVersion,
|
|
1139
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1140
|
+
outcome: "updated"
|
|
1141
|
+
});
|
|
1142
|
+
try {
|
|
1143
|
+
writeManifest$1(targetDir, manifest);
|
|
1144
|
+
} catch (e) {
|
|
1145
|
+
errors += 1;
|
|
1146
|
+
process.stderr.write(`bassclef sync: manifest update failed after writing ${d.entry.path}: ${e.message}
|
|
1147
|
+
`);
|
|
1148
|
+
continue;
|
|
1149
|
+
}
|
|
1150
|
+
updated += 1;
|
|
1151
|
+
process.stdout.write(` updated ${d.entry.path}
|
|
1152
|
+
`);
|
|
1153
|
+
}
|
|
1154
|
+
emitL2Output(decisions, verbose);
|
|
1155
|
+
const parts = [];
|
|
1156
|
+
if (updated > 0) parts.push(`${updated} updated`);
|
|
1157
|
+
if (noChange > 0) parts.push(`${noChange} up to date`);
|
|
1158
|
+
if (refused > 0) parts.push(`${refused} refused`);
|
|
1159
|
+
if (errors > 0) parts.push(`${errors} error(s)`);
|
|
1160
|
+
if (parts.length === 0) parts.push("nothing to do");
|
|
1161
|
+
const summary = parts.join(", ");
|
|
1162
|
+
if (errors > 0) {
|
|
1163
|
+
process.stderr.write(`bassclef sync: ${summary}.
|
|
1164
|
+
`);
|
|
1165
|
+
return 2;
|
|
1166
|
+
}
|
|
1167
|
+
if (refused > 0) {
|
|
1168
|
+
process.stderr.write(`bassclef sync: ${summary}.
|
|
1169
|
+
`);
|
|
1170
|
+
return 1;
|
|
1171
|
+
}
|
|
1172
|
+
process.stdout.write(`bassclef sync: ${summary}.
|
|
1173
|
+
`);
|
|
1174
|
+
return 0;
|
|
1175
|
+
}
|
|
1176
|
+
function emitL2Output(decisions, verbose) {
|
|
1177
|
+
if (decisions.length === 0) return;
|
|
1178
|
+
if (verbose) {
|
|
1179
|
+
for (const d of decisions) {
|
|
1180
|
+
const status = describeCase(d.case);
|
|
1181
|
+
process.stdout.write(` ${d.entry.path}: ${status}
|
|
1182
|
+
`);
|
|
1183
|
+
}
|
|
1184
|
+
return;
|
|
1185
|
+
}
|
|
1186
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1187
|
+
for (const d of decisions) {
|
|
1188
|
+
const parts = d.entry.path.split("/");
|
|
1189
|
+
const top = parts.length >= 2 ? parts.slice(0, 2).join("/") : parts[0] ?? "";
|
|
1190
|
+
const status = describeCase(d.case);
|
|
1191
|
+
let group = groups.get(top);
|
|
1192
|
+
if (group === void 0) {
|
|
1193
|
+
group = { count: 0, statuses: /* @__PURE__ */ new Map() };
|
|
1194
|
+
groups.set(top, group);
|
|
1195
|
+
}
|
|
1196
|
+
group.count += 1;
|
|
1197
|
+
group.statuses.set(status, (group.statuses.get(status) ?? 0) + 1);
|
|
1198
|
+
}
|
|
1199
|
+
for (const [directory, group] of groups) {
|
|
1200
|
+
let statusSummary;
|
|
1201
|
+
if (group.statuses.size === 1) {
|
|
1202
|
+
statusSummary = Array.from(group.statuses.keys())[0];
|
|
1203
|
+
} else {
|
|
1204
|
+
const bits = [];
|
|
1205
|
+
for (const [status, count] of group.statuses) {
|
|
1206
|
+
bits.push(`${count} ${status}`);
|
|
1207
|
+
}
|
|
1208
|
+
statusSummary = bits.join(", ");
|
|
1209
|
+
}
|
|
1210
|
+
const files = group.count === 1 ? "file" : "files";
|
|
1211
|
+
process.stdout.write(` ${directory}: ${group.count} ${files} (${statusSummary})
|
|
1212
|
+
`);
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
function updateManifestEntry(manifest, path, patch) {
|
|
1216
|
+
const idx = manifest.files.findIndex((f) => f.path === path);
|
|
1217
|
+
if (idx < 0) {
|
|
1218
|
+
manifest.files.push({
|
|
1219
|
+
path,
|
|
1220
|
+
template: patch.template ?? path,
|
|
1221
|
+
template_version: patch.template_version ?? "0.0.1",
|
|
1222
|
+
outcome: "updated",
|
|
1223
|
+
...patch
|
|
1224
|
+
});
|
|
1225
|
+
return;
|
|
1226
|
+
}
|
|
1227
|
+
manifest.files[idx] = { ...manifest.files[idx], ...patch };
|
|
1228
|
+
}
|
|
1229
|
+
function describeCase(c) {
|
|
1230
|
+
switch (c.kind) {
|
|
1231
|
+
case "Current":
|
|
1232
|
+
return "up to date";
|
|
1233
|
+
case "NeedsUpdate":
|
|
1234
|
+
return `update ${c.fromVersion}→${c.toVersion}`;
|
|
1235
|
+
case "Edited":
|
|
1236
|
+
return "edited by you";
|
|
1237
|
+
case "Deleted":
|
|
1238
|
+
return "deleted by you";
|
|
1239
|
+
case "NoMarker":
|
|
1240
|
+
return "no marker";
|
|
1241
|
+
case "UnknownHash":
|
|
1242
|
+
return "no hash in manifest";
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
function usage$1() {
|
|
1246
|
+
return [
|
|
1247
|
+
"",
|
|
1248
|
+
"Usage:",
|
|
1249
|
+
" bassclef sync [options]",
|
|
1250
|
+
"",
|
|
1251
|
+
"Options:",
|
|
1252
|
+
" --dir <path> Target directory. Default: current working directory.",
|
|
1253
|
+
" --force Apply available template updates. Default: refuse.",
|
|
1254
|
+
" --replace-edits Replace files you have edited. Default: refuse.",
|
|
1255
|
+
" --dry-run Print per-file status; write nothing.",
|
|
1256
|
+
" --diff Show template version change per file (--dry-run friendly).",
|
|
1257
|
+
" --allow-root Allow running as root. Default: refuse.",
|
|
1258
|
+
" --allow-any-dir Allow --dir outside your home directory. Default: refuse.",
|
|
1259
|
+
" --verbose Print per-file result.",
|
|
1260
|
+
"",
|
|
1261
|
+
"Sync reads .bassclef/init.manifest.json in the target directory.",
|
|
1262
|
+
"It updates only files listed in the manifest that carry the $bassclef marker.",
|
|
1263
|
+
""
|
|
1264
|
+
].join("\n");
|
|
1265
|
+
}
|
|
1266
|
+
const DEFAULTS = {
|
|
1267
|
+
dryRun: false,
|
|
1268
|
+
verbose: false,
|
|
1269
|
+
yes: false,
|
|
1270
|
+
allowRoot: false,
|
|
1271
|
+
allowAnyDir: false,
|
|
1272
|
+
dir: void 0
|
|
1273
|
+
};
|
|
1274
|
+
class ArgvError2 extends Error {
|
|
1275
|
+
name = "ArgvError";
|
|
1276
|
+
}
|
|
1277
|
+
function parseMigrateArgs(argv) {
|
|
1278
|
+
const out = { ...DEFAULTS };
|
|
1279
|
+
let i = 0;
|
|
1280
|
+
while (i < argv.length) {
|
|
1281
|
+
const token = argv[i];
|
|
1282
|
+
if (token === "--dry-run") {
|
|
1283
|
+
out.dryRun = true;
|
|
1284
|
+
i += 1;
|
|
1285
|
+
continue;
|
|
1286
|
+
}
|
|
1287
|
+
if (token === "--verbose") {
|
|
1288
|
+
out.verbose = true;
|
|
1289
|
+
i += 1;
|
|
1290
|
+
continue;
|
|
1291
|
+
}
|
|
1292
|
+
if (token === "--yes") {
|
|
1293
|
+
out.yes = true;
|
|
1294
|
+
i += 1;
|
|
1295
|
+
continue;
|
|
1296
|
+
}
|
|
1297
|
+
if (token === "--allow-root") {
|
|
1298
|
+
out.allowRoot = true;
|
|
1299
|
+
i += 1;
|
|
1300
|
+
continue;
|
|
1301
|
+
}
|
|
1302
|
+
if (token === "--allow-any-dir") {
|
|
1303
|
+
out.allowAnyDir = true;
|
|
1304
|
+
i += 1;
|
|
1305
|
+
continue;
|
|
1306
|
+
}
|
|
1307
|
+
if (token === "--dir") {
|
|
1308
|
+
const value = argv[i + 1];
|
|
1309
|
+
if (value === void 0 || value.startsWith("--")) {
|
|
1310
|
+
throw new ArgvError2("--dir requires a value");
|
|
1311
|
+
}
|
|
1312
|
+
out.dir = value;
|
|
1313
|
+
i += 2;
|
|
1314
|
+
continue;
|
|
1315
|
+
}
|
|
1316
|
+
if (token.startsWith("--dir=")) {
|
|
1317
|
+
out.dir = token.slice("--dir=".length);
|
|
1318
|
+
i += 1;
|
|
1319
|
+
continue;
|
|
1320
|
+
}
|
|
1321
|
+
if (token.startsWith("--")) {
|
|
1322
|
+
throw new ArgvError2(`unknown flag: ${token}`);
|
|
1323
|
+
}
|
|
1324
|
+
throw new ArgvError2(`unexpected argument: ${token}`);
|
|
1325
|
+
}
|
|
1326
|
+
return out;
|
|
1327
|
+
}
|
|
1328
|
+
async function confirm(question, opts = {}) {
|
|
1329
|
+
const { defaultNo = true, ttyOverride = null } = opts;
|
|
1330
|
+
if (ttyOverride === "yes") return true;
|
|
1331
|
+
if (ttyOverride === "no") return false;
|
|
1332
|
+
if (!node_process.stdin.isTTY) {
|
|
1333
|
+
node_process.stderr.write(
|
|
1334
|
+
`bassclef: no terminal detected; declining prompt.
|
|
1335
|
+
Run with --yes for non-interactive mode.
|
|
1336
|
+
`
|
|
1337
|
+
);
|
|
1338
|
+
return false;
|
|
1339
|
+
}
|
|
1340
|
+
const rl = promises.createInterface({ input: node_process.stdin, output: node_process.stdout });
|
|
1341
|
+
try {
|
|
1342
|
+
const suffix = defaultNo ? " [y/N] " : " [Y/n] ";
|
|
1343
|
+
const answer = (await rl.question(question + suffix)).trim().toLowerCase();
|
|
1344
|
+
if (answer === "") return !defaultNo;
|
|
1345
|
+
return answer === "y" || answer === "yes";
|
|
1346
|
+
} finally {
|
|
1347
|
+
rl.close();
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
async function detectAdopterState(targetDir) {
|
|
1351
|
+
let manifest;
|
|
1352
|
+
try {
|
|
1353
|
+
manifest = readManifest(targetDir);
|
|
1354
|
+
} catch (e) {
|
|
1355
|
+
if (e instanceof ManifestReadError) {
|
|
1356
|
+
if (e.kind === "Missing") return "no-manifest";
|
|
1357
|
+
return { kind: "error", message: e.message };
|
|
1358
|
+
}
|
|
1359
|
+
throw e;
|
|
1360
|
+
}
|
|
1361
|
+
const isLegacy = manifest.files.length === 3;
|
|
1362
|
+
const schemaLegacy = manifest.$bassclef.manifest_schema_version.startsWith("0.0.");
|
|
1363
|
+
if (isLegacy || schemaLegacy) return "legacy-3-entry";
|
|
1364
|
+
if (manifest.files.length === CURRENT_ENTRY_COUNT) return "current";
|
|
1365
|
+
return {
|
|
1366
|
+
kind: "unknown",
|
|
1367
|
+
message: `manifest has ${manifest.files.length} entries; expected 3 (legacy) or ${CURRENT_ENTRY_COUNT} (current)`
|
|
1368
|
+
};
|
|
1369
|
+
}
|
|
1370
|
+
async function runPathA(targetDir, _existingManifest, args, opts = {}) {
|
|
1371
|
+
const configHashes = await computeConfigHashes(targetDir, [...CONFIG_FILES]);
|
|
1372
|
+
const ttyOverride = args.yes ? "yes" : null;
|
|
1373
|
+
const proceed = await confirm(
|
|
1374
|
+
`Ready to migrate to 0.1.0. Adds substrate files; preserves ${Object.keys(configHashes).length} existing config files.
|
|
1375
|
+
Proceed?`,
|
|
1376
|
+
{ defaultNo: true, ttyOverride }
|
|
1377
|
+
);
|
|
1378
|
+
if (!proceed) {
|
|
1379
|
+
process.stdout.write("bassclef migrate: aborted. No files changed.\n");
|
|
1380
|
+
return { added: [], preserved: [], refused: [], errored: [], exitCode: 0 };
|
|
1381
|
+
}
|
|
1382
|
+
if (args.dryRun) {
|
|
1383
|
+
process.stdout.write(
|
|
1384
|
+
`bassclef migrate: dry-run — would add substrate files; would preserve ${Object.keys(configHashes).length} config files.
|
|
1385
|
+
`
|
|
1386
|
+
);
|
|
1387
|
+
return { added: [], preserved: Object.keys(configHashes), refused: [], errored: [], exitCode: 0 };
|
|
1388
|
+
}
|
|
1389
|
+
const copyResult = copySubstrate(targetDir, { force: false, bundleRoot: opts.bundleRoot });
|
|
1390
|
+
const added = copyResult.copied;
|
|
1391
|
+
const preserved = copyResult.refused.filter(
|
|
1392
|
+
(p) => CONFIG_FILES.includes(p)
|
|
1393
|
+
);
|
|
1394
|
+
const refused = copyResult.refused.filter(
|
|
1395
|
+
(p) => !CONFIG_FILES.includes(p)
|
|
1396
|
+
);
|
|
1397
|
+
const errored = copyResult.errored;
|
|
1398
|
+
const newManifest = {
|
|
1399
|
+
$bassclef: {
|
|
1400
|
+
template: "init.manifest.json",
|
|
1401
|
+
manifest_schema_version: "0.1.0",
|
|
1402
|
+
generated_by: "@thebassclef/core",
|
|
1403
|
+
generated_by_version: index.version
|
|
1404
|
+
},
|
|
1405
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1406
|
+
target_dir: targetDir,
|
|
1407
|
+
files: [
|
|
1408
|
+
...added.map((path) => ({
|
|
1409
|
+
path,
|
|
1410
|
+
template: basename(path),
|
|
1411
|
+
template_version: "0.1.0",
|
|
1412
|
+
outcome: "created"
|
|
1413
|
+
})),
|
|
1414
|
+
...preserved.map((path) => ({
|
|
1415
|
+
path,
|
|
1416
|
+
template: basename(path),
|
|
1417
|
+
template_version: "0.1.0",
|
|
1418
|
+
content_hash_sha256: configHashes[path],
|
|
1419
|
+
outcome: "unchanged"
|
|
1420
|
+
}))
|
|
1421
|
+
]
|
|
1422
|
+
};
|
|
1423
|
+
writeManifest$1(targetDir, newManifest);
|
|
1424
|
+
const parts = [
|
|
1425
|
+
`${added.length} added`,
|
|
1426
|
+
`${preserved.length} preserved with existing content`,
|
|
1427
|
+
...refused.length > 0 ? [`${refused.length} refused`] : [],
|
|
1428
|
+
...errored.length > 0 ? [`${errored.length} errored`] : []
|
|
1429
|
+
];
|
|
1430
|
+
process.stdout.write(`bassclef migrate: ${parts.join("; ")}.
|
|
1431
|
+
`);
|
|
1432
|
+
process.stdout.write(
|
|
1433
|
+
`bassclef migrate: your substrate lives under .claude/. Add .claude/ to .gitignore if you have not.
|
|
1434
|
+
`
|
|
1435
|
+
);
|
|
1436
|
+
const exitCode = errored.length > 0 ? 1 : refused.length > 0 ? 2 : 0;
|
|
1437
|
+
return { added, preserved, refused, errored, exitCode };
|
|
1438
|
+
}
|
|
1439
|
+
async function runPathB(targetDir, args) {
|
|
1440
|
+
process.stdout.write(
|
|
1441
|
+
`bassclef migrate: no prior manifest detected. Running full init for ${CURRENT_ENTRY_COUNT} files.
|
|
1442
|
+
`
|
|
1443
|
+
);
|
|
1444
|
+
const initArgv = [];
|
|
1445
|
+
if (args.dir) initArgv.push("--dir", args.dir);
|
|
1446
|
+
if (args.dryRun) initArgv.push("--dry-run");
|
|
1447
|
+
if (args.verbose) initArgv.push("--verbose");
|
|
1448
|
+
if (args.allowRoot) initArgv.push("--allow-root");
|
|
1449
|
+
if (args.allowAnyDir) initArgv.push("--allow-any-dir");
|
|
1450
|
+
initArgv.push("--force");
|
|
1451
|
+
const exitCode = runInit(initArgv);
|
|
1452
|
+
if (exitCode === 0) {
|
|
1453
|
+
process.stdout.write(
|
|
1454
|
+
`bassclef migrate: your substrate lives under .claude/. Add .claude/ to .gitignore if you have not.
|
|
1455
|
+
`
|
|
1456
|
+
);
|
|
1457
|
+
}
|
|
1458
|
+
return {
|
|
1459
|
+
added: exitCode === 0 ? Array(CURRENT_ENTRY_COUNT).fill("(via init)") : [],
|
|
1460
|
+
preserved: [],
|
|
1461
|
+
refused: [],
|
|
1462
|
+
errored: [],
|
|
1463
|
+
exitCode
|
|
1464
|
+
};
|
|
1465
|
+
}
|
|
1466
|
+
function basename(path) {
|
|
1467
|
+
const parts = path.split("/");
|
|
1468
|
+
return parts[parts.length - 1] ?? path;
|
|
1469
|
+
}
|
|
1470
|
+
function usage() {
|
|
1471
|
+
return `bassclef migrate — upgrade adopter substrate to the current shape
|
|
1472
|
+
|
|
1473
|
+
Usage:
|
|
1474
|
+
bassclef migrate [options]
|
|
1475
|
+
|
|
1476
|
+
Options:
|
|
1477
|
+
--dry-run Preview the shape without writing anything
|
|
1478
|
+
--verbose Print one line per file (not just the summary)
|
|
1479
|
+
--yes Skip the interactive confirmation prompt (CI + scripts)
|
|
1480
|
+
--allow-root Allow running as root
|
|
1481
|
+
--allow-any-dir Allow a target outside the current user's home
|
|
1482
|
+
--dir <path> Target directory (defaults to current working dir)
|
|
1483
|
+
--help Print this message
|
|
1484
|
+
|
|
1485
|
+
Two upgrade paths run automatically based on adopter state:
|
|
1486
|
+
0.0.2 legacy install → Path A: adds 146 substrate files; preserves
|
|
1487
|
+
the 3 edited config files via SHA-256 hash
|
|
1488
|
+
0.0.1 name-reservation → Path B: full init for all 149 files
|
|
1489
|
+
|
|
1490
|
+
Exit codes:
|
|
1491
|
+
0 success (files landed, or already at current shape, or user aborted)
|
|
1492
|
+
1 argv error, root check failed, or manifest could not be read
|
|
1493
|
+
2 one or more per-file writes refused (symlink, parent not writable)
|
|
1494
|
+
4 manifest schema version newer than this migrate knows about
|
|
1495
|
+
5 adopter state does not match 0.0.x or 0.1.0 — reinstall @thebassclef/core
|
|
1496
|
+
`;
|
|
1497
|
+
}
|
|
1498
|
+
async function runMigrate(argv) {
|
|
1499
|
+
let args;
|
|
1500
|
+
try {
|
|
1501
|
+
args = parseMigrateArgs(argv);
|
|
1502
|
+
} catch (e) {
|
|
1503
|
+
if (e instanceof ArgvError2) {
|
|
1504
|
+
process.stderr.write(`bassclef migrate: ${e.message}
|
|
1505
|
+
`);
|
|
1506
|
+
return 1;
|
|
1507
|
+
}
|
|
1508
|
+
throw e;
|
|
1509
|
+
}
|
|
1510
|
+
const uid = typeof process.getuid === "function" ? process.getuid() : void 0;
|
|
1511
|
+
if (shouldRefuseRoot(uid, args.allowRoot)) {
|
|
1512
|
+
process.stderr.write(
|
|
1513
|
+
`bassclef migrate: refusing to run as root. Pass --allow-root to override, or run as the project owner.
|
|
1514
|
+
`
|
|
1515
|
+
);
|
|
1516
|
+
return 1;
|
|
1517
|
+
}
|
|
1518
|
+
const targetDir = args.dir ?? process.cwd();
|
|
1519
|
+
const state = await detectAdopterState(targetDir);
|
|
1520
|
+
if (state === "current") {
|
|
1521
|
+
process.stdout.write("bassclef migrate: already at 0.1.0 shape. Nothing to migrate.\n");
|
|
1522
|
+
return 0;
|
|
1523
|
+
}
|
|
1524
|
+
if (state === "no-manifest") {
|
|
1525
|
+
const result = await runPathB(targetDir, args);
|
|
1526
|
+
return result.exitCode;
|
|
1527
|
+
}
|
|
1528
|
+
if (state === "legacy-3-entry") {
|
|
1529
|
+
const manifest = readManifest(targetDir);
|
|
1530
|
+
const result = await runPathA(targetDir, manifest, args);
|
|
1531
|
+
return result.exitCode;
|
|
1532
|
+
}
|
|
1533
|
+
if (state.kind === "unknown") {
|
|
1534
|
+
process.stderr.write(
|
|
1535
|
+
`bassclef migrate: adopter state does not match 0.0.x or 0.1.0. Reinstall @thebassclef/core then rerun. (${state.message})
|
|
1536
|
+
`
|
|
1537
|
+
);
|
|
1538
|
+
return 5;
|
|
1539
|
+
}
|
|
1540
|
+
if (state.kind === "error") {
|
|
1541
|
+
process.stderr.write(`bassclef migrate: ${state.message}
|
|
1542
|
+
`);
|
|
1543
|
+
if (state.message.includes("newer than this package")) return 4;
|
|
1544
|
+
return 1;
|
|
1545
|
+
}
|
|
1546
|
+
return 1;
|
|
1547
|
+
}
|
|
1548
|
+
const USAGE = `bassclef — install and upgrade bassclef in your project
|
|
1549
|
+
|
|
1550
|
+
Usage:
|
|
1551
|
+
bassclef init [options] Write bassclef config into a project directory
|
|
1552
|
+
bassclef sync [options] Update bassclef-managed files in place
|
|
1553
|
+
bassclef migrate [options] Upgrade adopter substrate to the current shape
|
|
1554
|
+
bassclef --version Print the running version
|
|
1555
|
+
bassclef --help Print this message
|
|
1556
|
+
|
|
1557
|
+
Run \`bassclef init --help\` for init options + the safety defaults.
|
|
1558
|
+
Run \`bassclef sync --help\` for sync options + the update semantics.
|
|
1559
|
+
Run \`bassclef migrate --help\` for migrate options + the upgrade paths.
|
|
1560
|
+
|
|
1561
|
+
Version: ${index.version}
|
|
1562
|
+
Docs: https://github.com/sunj-labs/bassclef-cli
|
|
1563
|
+
`;
|
|
1564
|
+
async function main(argv) {
|
|
1565
|
+
const first = argv[0];
|
|
1566
|
+
if (first === void 0 || first === "--help" || first === "-h" || first === "help") {
|
|
1567
|
+
process.stdout.write(USAGE);
|
|
1568
|
+
return 0;
|
|
1569
|
+
}
|
|
1570
|
+
if (first === "--version" || first === "-v" || first === "version") {
|
|
1571
|
+
process.stdout.write(`${index.version}
|
|
1572
|
+
`);
|
|
1573
|
+
return 0;
|
|
1574
|
+
}
|
|
1575
|
+
if (first === "init") {
|
|
1576
|
+
const rest = argv.slice(1);
|
|
1577
|
+
if (rest.includes("--help") || rest.includes("-h")) {
|
|
1578
|
+
process.stdout.write(usage$2());
|
|
1579
|
+
return 0;
|
|
1580
|
+
}
|
|
1581
|
+
return runInit(rest);
|
|
1582
|
+
}
|
|
1583
|
+
if (first === "sync") {
|
|
1584
|
+
const rest = argv.slice(1);
|
|
1585
|
+
if (rest.includes("--help") || rest.includes("-h")) {
|
|
1586
|
+
process.stdout.write(usage$1());
|
|
1587
|
+
return 0;
|
|
1588
|
+
}
|
|
1589
|
+
return runSync(rest);
|
|
1590
|
+
}
|
|
1591
|
+
if (first === "migrate") {
|
|
1592
|
+
const rest = argv.slice(1);
|
|
1593
|
+
if (rest.includes("--help") || rest.includes("-h")) {
|
|
1594
|
+
process.stdout.write(usage());
|
|
1595
|
+
return 0;
|
|
1596
|
+
}
|
|
1597
|
+
return await runMigrate(rest);
|
|
1598
|
+
}
|
|
1599
|
+
process.stderr.write(
|
|
1600
|
+
`bassclef: unknown command '${first}'
|
|
1601
|
+
|
|
1602
|
+
` + USAGE
|
|
1603
|
+
);
|
|
1604
|
+
return 3;
|
|
1605
|
+
}
|
|
1606
|
+
main(process.argv.slice(2)).then((exitCode) => process.exit(exitCode)).catch((err) => {
|
|
1607
|
+
process.stderr.write(`bassclef: ${err.message ?? String(err)}
|
|
1608
|
+
`);
|
|
1609
|
+
process.exit(1);
|
|
1610
|
+
});
|