stego-cli 0.4.1 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -0
- package/dist/shared/src/contracts/cli/envelopes.js +19 -0
- package/dist/shared/src/contracts/cli/errors.js +14 -0
- package/dist/shared/src/contracts/cli/exit-codes.js +15 -0
- package/dist/shared/src/contracts/cli/index.js +6 -0
- package/dist/shared/src/contracts/cli/metadata.js +1 -0
- package/dist/shared/src/contracts/cli/operations.js +1 -0
- package/dist/shared/src/domain/comments/anchors.js +1 -0
- package/dist/shared/src/domain/comments/index.js +4 -0
- package/dist/shared/src/domain/comments/serializer.js +1 -0
- package/dist/shared/src/domain/comments/thread-key.js +21 -0
- package/dist/shared/src/domain/frontmatter/index.js +3 -0
- package/dist/shared/src/domain/frontmatter/parser.js +34 -0
- package/dist/shared/src/domain/frontmatter/serializer.js +32 -0
- package/dist/shared/src/domain/frontmatter/validators.js +47 -0
- package/dist/shared/src/domain/project/index.js +4 -0
- package/dist/shared/src/domain/stages/index.js +20 -0
- package/dist/shared/src/index.js +6 -0
- package/dist/shared/src/utils/guards.js +6 -0
- package/dist/shared/src/utils/index.js +3 -0
- package/dist/shared/src/utils/invariant.js +5 -0
- package/dist/shared/src/utils/result.js +6 -0
- package/dist/stego-cli/src/app/cli-version.js +32 -0
- package/dist/stego-cli/src/app/command-context.js +1 -0
- package/dist/stego-cli/src/app/command-registry.js +121 -0
- package/dist/stego-cli/src/app/create-cli-app.js +42 -0
- package/dist/stego-cli/src/app/error-boundary.js +42 -0
- package/dist/stego-cli/src/app/index.js +6 -0
- package/dist/stego-cli/src/app/output-renderer.js +6 -0
- package/dist/stego-cli/src/main.js +14 -0
- package/dist/stego-cli/src/modules/comments/application/comment-operations.js +457 -0
- package/dist/stego-cli/src/modules/comments/commands/comments-add.js +40 -0
- package/dist/stego-cli/src/modules/comments/commands/comments-clear-resolved.js +32 -0
- package/dist/stego-cli/src/modules/comments/commands/comments-delete.js +33 -0
- package/dist/stego-cli/src/modules/comments/commands/comments-read.js +32 -0
- package/dist/stego-cli/src/modules/comments/commands/comments-reply.js +36 -0
- package/dist/stego-cli/src/modules/comments/commands/comments-set-status.js +35 -0
- package/dist/stego-cli/src/modules/comments/commands/comments-sync-anchors.js +33 -0
- package/dist/stego-cli/src/modules/comments/domain/comment-policy.js +14 -0
- package/dist/stego-cli/src/modules/comments/index.js +20 -0
- package/dist/stego-cli/src/modules/comments/infra/comments-repo.js +68 -0
- package/dist/stego-cli/src/modules/comments/types.js +1 -0
- package/dist/stego-cli/src/modules/compile/application/compile-manuscript.js +16 -0
- package/dist/stego-cli/src/modules/compile/commands/build.js +51 -0
- package/dist/stego-cli/src/modules/compile/domain/compile-structure.js +105 -0
- package/dist/stego-cli/src/modules/compile/index.js +8 -0
- package/dist/stego-cli/src/modules/compile/infra/dist-writer.js +8 -0
- package/dist/stego-cli/src/modules/compile/types.js +1 -0
- package/dist/stego-cli/src/modules/export/application/run-export.js +29 -0
- package/dist/stego-cli/src/modules/export/commands/export.js +61 -0
- package/dist/stego-cli/src/modules/export/domain/exporter.js +6 -0
- package/dist/stego-cli/src/modules/export/index.js +8 -0
- package/dist/stego-cli/src/modules/export/types.js +1 -0
- package/dist/stego-cli/src/modules/index.js +22 -0
- package/dist/stego-cli/src/modules/manuscript/application/create-manuscript.js +107 -0
- package/dist/stego-cli/src/modules/manuscript/application/order-inference.js +56 -0
- package/dist/stego-cli/src/modules/manuscript/commands/new-manuscript.js +54 -0
- package/dist/stego-cli/src/modules/manuscript/domain/manuscript.js +1 -0
- package/dist/stego-cli/src/modules/manuscript/index.js +9 -0
- package/dist/stego-cli/src/modules/manuscript/infra/manuscript-repo.js +39 -0
- package/dist/stego-cli/src/modules/manuscript/types.js +1 -0
- package/dist/stego-cli/src/modules/metadata/application/apply-metadata.js +45 -0
- package/dist/stego-cli/src/modules/metadata/application/read-metadata.js +18 -0
- package/dist/stego-cli/src/modules/metadata/commands/metadata-apply.js +38 -0
- package/dist/stego-cli/src/modules/metadata/commands/metadata-read.js +33 -0
- package/dist/stego-cli/src/modules/metadata/domain/metadata.js +1 -0
- package/dist/stego-cli/src/modules/metadata/index.js +11 -0
- package/dist/stego-cli/src/modules/metadata/infra/metadata-repo.js +68 -0
- package/dist/stego-cli/src/modules/metadata/types.js +1 -0
- package/dist/stego-cli/src/modules/project/application/create-project.js +203 -0
- package/dist/stego-cli/src/modules/project/application/infer-project.js +72 -0
- package/dist/stego-cli/src/modules/project/commands/new-project.js +86 -0
- package/dist/stego-cli/src/modules/project/domain/project.js +1 -0
- package/dist/stego-cli/src/modules/project/index.js +9 -0
- package/dist/stego-cli/src/modules/project/infra/project-repo.js +27 -0
- package/dist/stego-cli/src/modules/project/types.js +1 -0
- package/dist/stego-cli/src/modules/quality/application/inspect-project.js +603 -0
- package/dist/stego-cli/src/modules/quality/application/lint-runner.js +313 -0
- package/dist/stego-cli/src/modules/quality/application/stage-check.js +87 -0
- package/dist/stego-cli/src/modules/quality/commands/check-stage.js +54 -0
- package/dist/stego-cli/src/modules/quality/commands/lint.js +51 -0
- package/dist/stego-cli/src/modules/quality/commands/validate.js +50 -0
- package/dist/stego-cli/src/modules/quality/domain/issues.js +1 -0
- package/dist/stego-cli/src/modules/quality/domain/policies.js +1 -0
- package/dist/stego-cli/src/modules/quality/index.js +14 -0
- package/dist/stego-cli/src/modules/quality/infra/cspell-adapter.js +1 -0
- package/dist/stego-cli/src/modules/quality/infra/markdownlint-adapter.js +1 -0
- package/dist/stego-cli/src/modules/quality/types.js +1 -0
- package/dist/stego-cli/src/modules/scaffold/application/scaffold-workspace.js +307 -0
- package/dist/stego-cli/src/modules/scaffold/commands/init.js +34 -0
- package/dist/stego-cli/src/modules/scaffold/domain/templates.js +182 -0
- package/dist/stego-cli/src/modules/scaffold/index.js +8 -0
- package/dist/stego-cli/src/modules/scaffold/infra/template-repo.js +33 -0
- package/dist/stego-cli/src/modules/scaffold/types.js +1 -0
- package/dist/stego-cli/src/modules/spine/application/create-category.js +14 -0
- package/dist/stego-cli/src/modules/spine/application/create-entry.js +9 -0
- package/dist/stego-cli/src/modules/spine/application/read-catalog.js +13 -0
- package/dist/stego-cli/src/modules/spine/commands/spine-deprecated-aliases.js +33 -0
- package/dist/stego-cli/src/modules/spine/commands/spine-new-category.js +64 -0
- package/dist/stego-cli/src/modules/spine/commands/spine-new-entry.js +65 -0
- package/dist/stego-cli/src/modules/spine/commands/spine-read.js +49 -0
- package/dist/{spine/spine-domain.js → stego-cli/src/modules/spine/domain/spine.js} +13 -7
- package/dist/stego-cli/src/modules/spine/index.js +16 -0
- package/dist/stego-cli/src/modules/spine/infra/spine-repo.js +46 -0
- package/dist/stego-cli/src/modules/spine/types.js +1 -0
- package/dist/stego-cli/src/modules/workspace/application/discover-projects.js +18 -0
- package/dist/stego-cli/src/modules/workspace/application/resolve-workspace.js +73 -0
- package/dist/stego-cli/src/modules/workspace/commands/list-projects.js +40 -0
- package/dist/stego-cli/src/modules/workspace/index.js +9 -0
- package/dist/stego-cli/src/modules/workspace/infra/workspace-repo.js +37 -0
- package/dist/stego-cli/src/modules/workspace/types.js +1 -0
- package/dist/stego-cli/src/platform/clock.js +3 -0
- package/dist/stego-cli/src/platform/fs.js +13 -0
- package/dist/stego-cli/src/platform/index.js +3 -0
- package/dist/stego-cli/src/platform/temp-files.js +6 -0
- package/package.json +20 -11
- package/dist/comments/comments-command.js +0 -499
- package/dist/comments/errors.js +0 -20
- package/dist/metadata/metadata-command.js +0 -127
- package/dist/metadata/metadata-domain.js +0 -209
- package/dist/spine/spine-command.js +0 -129
- package/dist/stego-cli.js +0 -2264
- /package/dist/{exporters/exporter-types.js → shared/src/contracts/cli/comments.js} +0 -0
- /package/dist/{comments/comment-domain.js → shared/src/domain/comments/parser.js} +0 -0
- /package/dist/{exporters → stego-cli/src/modules/export/infra}/markdown-exporter.js +0 -0
- /package/dist/{exporters → stego-cli/src/modules/export/infra}/pandoc-exporter.js +0 -0
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import process from "node:process";
|
|
4
|
+
import { spawnSync } from "node:child_process";
|
|
5
|
+
import { createInterface } from "node:readline/promises";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { CliError } from "../../../../../shared/src/contracts/cli/index.js";
|
|
8
|
+
import { ROOT_CONFIG_FILENAME } from "../../workspace/index.js";
|
|
9
|
+
import { COMMENT_AUTHOR_PROMPT, PROJECT_EXTENSION_RECOMMENDATIONS, PROSE_FONT_PROMPT, PROSE_MARKDOWN_EDITOR_SETTINGS, SCAFFOLD_AGENTS_CONTENT, SCAFFOLD_GITIGNORE_CONTENT, SCAFFOLD_README_CONTENT } from "../domain/templates.js";
|
|
10
|
+
import { copyDirectory, copyFile, ensureDirectory, listDirectoryEntries, pathExists, readTextFile, statPath, writeTextFile } from "../infra/template-repo.js";
|
|
11
|
+
export async function scaffoldWorkspace(input) {
|
|
12
|
+
const targetRoot = path.resolve(input.cwd);
|
|
13
|
+
const entries = listDirectoryEntries(targetRoot)
|
|
14
|
+
.filter((entry) => entry.name !== "." && entry.name !== "..");
|
|
15
|
+
if (entries.length > 0 && !input.force) {
|
|
16
|
+
throw new Error(`Target directory is not empty: ${targetRoot}. Re-run with --force to continue.`);
|
|
17
|
+
}
|
|
18
|
+
const packageRoot = resolvePackageRoot();
|
|
19
|
+
const copiedPaths = [];
|
|
20
|
+
writeScaffoldGitignore(targetRoot, copiedPaths);
|
|
21
|
+
writeScaffoldReadme(targetRoot, copiedPaths);
|
|
22
|
+
writeScaffoldAgents(targetRoot, copiedPaths);
|
|
23
|
+
copyTemplateAsset(packageRoot, ".markdownlint.json", targetRoot, copiedPaths);
|
|
24
|
+
copyTemplateAsset(packageRoot, ".markdownlint.manuscript.json", targetRoot, copiedPaths);
|
|
25
|
+
copyTemplateAsset(packageRoot, ".cspell.json", targetRoot, copiedPaths);
|
|
26
|
+
copyTemplateAsset(packageRoot, ROOT_CONFIG_FILENAME, targetRoot, copiedPaths);
|
|
27
|
+
copyTemplateAsset(packageRoot, "projects", targetRoot, copiedPaths);
|
|
28
|
+
copyTemplateAsset(packageRoot, path.join(".vscode", "tasks.json"), targetRoot, copiedPaths);
|
|
29
|
+
copyTemplateAsset(packageRoot, path.join(".vscode", "extensions.json"), targetRoot, copiedPaths, { optional: true });
|
|
30
|
+
rewriteTemplateProjectPackageScripts(targetRoot);
|
|
31
|
+
const enableProseFont = await promptYesNo(PROSE_FONT_PROMPT, true);
|
|
32
|
+
const suggestedCommentAuthor = resolveSuggestedCommentAuthor(targetRoot);
|
|
33
|
+
const commentAuthor = (await promptText(COMMENT_AUTHOR_PROMPT, suggestedCommentAuthor)).trim();
|
|
34
|
+
if (enableProseFont || commentAuthor) {
|
|
35
|
+
writeProjectProseEditorSettings(targetRoot, copiedPaths, {
|
|
36
|
+
enableProseFont,
|
|
37
|
+
commentAuthor
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
writeInitRootPackageJson(targetRoot, packageRoot);
|
|
41
|
+
return {
|
|
42
|
+
targetRoot,
|
|
43
|
+
copiedPaths
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function resolvePackageRoot() {
|
|
47
|
+
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
|
48
|
+
const candidates = [
|
|
49
|
+
path.resolve(scriptDir, "../../../../"),
|
|
50
|
+
path.resolve(scriptDir, "../../../../../../")
|
|
51
|
+
];
|
|
52
|
+
for (const candidate of candidates) {
|
|
53
|
+
const configPath = path.join(candidate, ROOT_CONFIG_FILENAME);
|
|
54
|
+
const projectsPath = path.join(candidate, "projects");
|
|
55
|
+
if (pathExists(configPath) && pathExists(projectsPath)) {
|
|
56
|
+
return candidate;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
throw new CliError("INTERNAL_ERROR", "Unable to resolve stego-cli package root for scaffolding.");
|
|
60
|
+
}
|
|
61
|
+
async function promptYesNo(question, defaultYes) {
|
|
62
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
63
|
+
return defaultYes;
|
|
64
|
+
}
|
|
65
|
+
const rl = createInterface({
|
|
66
|
+
input: process.stdin,
|
|
67
|
+
output: process.stdout
|
|
68
|
+
});
|
|
69
|
+
const suffix = defaultYes ? " [Y/n] " : " [y/N] ";
|
|
70
|
+
try {
|
|
71
|
+
while (true) {
|
|
72
|
+
const answer = (await rl.question(`${question}${suffix}`)).trim().toLowerCase();
|
|
73
|
+
if (!answer) {
|
|
74
|
+
return defaultYes;
|
|
75
|
+
}
|
|
76
|
+
if (answer === "y" || answer === "yes") {
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
if (answer === "n" || answer === "no") {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
process.stdout.write("Please answer y or n.\n");
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
finally {
|
|
86
|
+
rl.close();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
async function promptText(question, defaultValue = "") {
|
|
90
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
91
|
+
return defaultValue;
|
|
92
|
+
}
|
|
93
|
+
const rl = createInterface({
|
|
94
|
+
input: process.stdin,
|
|
95
|
+
output: process.stdout
|
|
96
|
+
});
|
|
97
|
+
const suffix = defaultValue ? ` [${defaultValue}] ` : " ";
|
|
98
|
+
try {
|
|
99
|
+
const answer = (await rl.question(`${question}${suffix}`)).trim();
|
|
100
|
+
return answer || defaultValue;
|
|
101
|
+
}
|
|
102
|
+
finally {
|
|
103
|
+
rl.close();
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
function resolveSuggestedCommentAuthor(cwd) {
|
|
107
|
+
const gitAuthor = spawnSync("git", ["config", "--get", "user.name"], {
|
|
108
|
+
cwd,
|
|
109
|
+
encoding: "utf8"
|
|
110
|
+
});
|
|
111
|
+
const fromGit = (gitAuthor.stdout || "").trim();
|
|
112
|
+
if (gitAuthor.status === 0 && fromGit) {
|
|
113
|
+
return fromGit;
|
|
114
|
+
}
|
|
115
|
+
try {
|
|
116
|
+
const username = os.userInfo().username.trim();
|
|
117
|
+
if (username) {
|
|
118
|
+
return username;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
// ignore and fall back to empty string
|
|
123
|
+
}
|
|
124
|
+
return "";
|
|
125
|
+
}
|
|
126
|
+
function copyTemplateAsset(packageRoot, sourceRelativePath, targetRoot, copiedPaths, options) {
|
|
127
|
+
const sourcePath = path.join(packageRoot, sourceRelativePath);
|
|
128
|
+
if (!pathExists(sourcePath)) {
|
|
129
|
+
if (options?.optional) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
throw new Error(`Template asset is missing from stego-cli package: ${sourceRelativePath}`);
|
|
133
|
+
}
|
|
134
|
+
const destinationPath = path.join(targetRoot, sourceRelativePath);
|
|
135
|
+
const stats = statPath(sourcePath);
|
|
136
|
+
if (stats.isDirectory()) {
|
|
137
|
+
copyDirectory(sourcePath, destinationPath, (currentSourcePath) => shouldCopyTemplatePath(packageRoot, currentSourcePath));
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
copyFile(sourcePath, destinationPath);
|
|
141
|
+
}
|
|
142
|
+
copiedPaths.push(sourceRelativePath);
|
|
143
|
+
}
|
|
144
|
+
function writeScaffoldGitignore(targetRoot, copiedPaths) {
|
|
145
|
+
writeTextFile(path.join(targetRoot, ".gitignore"), SCAFFOLD_GITIGNORE_CONTENT);
|
|
146
|
+
copiedPaths.push(".gitignore");
|
|
147
|
+
}
|
|
148
|
+
function writeScaffoldReadme(targetRoot, copiedPaths) {
|
|
149
|
+
writeTextFile(path.join(targetRoot, "README.md"), SCAFFOLD_README_CONTENT);
|
|
150
|
+
copiedPaths.push("README.md");
|
|
151
|
+
}
|
|
152
|
+
function writeScaffoldAgents(targetRoot, copiedPaths) {
|
|
153
|
+
writeTextFile(path.join(targetRoot, "AGENTS.md"), SCAFFOLD_AGENTS_CONTENT);
|
|
154
|
+
copiedPaths.push("AGENTS.md");
|
|
155
|
+
}
|
|
156
|
+
function shouldCopyTemplatePath(packageRoot, currentSourcePath) {
|
|
157
|
+
const relativePath = path.relative(packageRoot, currentSourcePath);
|
|
158
|
+
if (!relativePath || relativePath.startsWith("..")) {
|
|
159
|
+
return true;
|
|
160
|
+
}
|
|
161
|
+
const parts = relativePath.split(path.sep);
|
|
162
|
+
const name = parts[parts.length - 1] || "";
|
|
163
|
+
if (name === ".DS_Store") {
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
if (parts[0] === "projects") {
|
|
167
|
+
if (parts[parts.length - 2] === ".vscode" && name === "settings.json") {
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
const distIndex = parts.indexOf("dist");
|
|
171
|
+
if (distIndex >= 0) {
|
|
172
|
+
const isDistRoot = distIndex === parts.length - 1;
|
|
173
|
+
const isGitkeep = name === ".gitkeep";
|
|
174
|
+
return isDistRoot || isGitkeep;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
179
|
+
function rewriteTemplateProjectPackageScripts(targetRoot) {
|
|
180
|
+
const projectsRoot = path.join(targetRoot, "projects");
|
|
181
|
+
if (!pathExists(projectsRoot)) {
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
for (const entry of listDirectoryEntries(projectsRoot)) {
|
|
185
|
+
if (!entry.isDirectory()) {
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
const projectRoot = path.join(projectsRoot, entry.name);
|
|
189
|
+
const packageJsonPath = path.join(projectRoot, "package.json");
|
|
190
|
+
if (!pathExists(packageJsonPath)) {
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
const parsed = tryReadJsonObject(packageJsonPath) ?? {};
|
|
194
|
+
const scripts = isPlainObject(parsed.scripts) ? { ...parsed.scripts } : {};
|
|
195
|
+
scripts.validate = "npx --no-install stego validate";
|
|
196
|
+
scripts.build = "npx --no-install stego build";
|
|
197
|
+
scripts["check-stage"] = "npx --no-install stego check-stage";
|
|
198
|
+
scripts.export = "npx --no-install stego export";
|
|
199
|
+
scripts.new = "npx --no-install stego new";
|
|
200
|
+
writeTextFile(packageJsonPath, `${JSON.stringify({ ...parsed, scripts }, null, 2)}\n`);
|
|
201
|
+
ensureProjectExtensionsRecommendations(projectRoot);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
function ensureProjectExtensionsRecommendations(projectRoot) {
|
|
205
|
+
const vscodeDir = path.join(projectRoot, ".vscode");
|
|
206
|
+
const extensionsPath = path.join(vscodeDir, "extensions.json");
|
|
207
|
+
ensureDirectory(vscodeDir);
|
|
208
|
+
let existingRecommendations = [];
|
|
209
|
+
const parsed = tryReadJsonObject(extensionsPath);
|
|
210
|
+
if (parsed && Array.isArray(parsed.recommendations)) {
|
|
211
|
+
existingRecommendations = parsed.recommendations.filter((value) => typeof value === "string");
|
|
212
|
+
}
|
|
213
|
+
const mergedRecommendations = [
|
|
214
|
+
...new Set([...PROJECT_EXTENSION_RECOMMENDATIONS, ...existingRecommendations])
|
|
215
|
+
];
|
|
216
|
+
writeTextFile(extensionsPath, `${JSON.stringify({ recommendations: mergedRecommendations }, null, 2)}\n`);
|
|
217
|
+
}
|
|
218
|
+
function writeProjectProseEditorSettings(targetRoot, copiedPaths, options) {
|
|
219
|
+
const projectsRoot = path.join(targetRoot, "projects");
|
|
220
|
+
if (!pathExists(projectsRoot)) {
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
for (const entry of listDirectoryEntries(projectsRoot)) {
|
|
224
|
+
if (!entry.isDirectory()) {
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
const projectRoot = path.join(projectsRoot, entry.name);
|
|
228
|
+
const settingsPath = writeProseEditorSettingsForProject(projectRoot, options);
|
|
229
|
+
copiedPaths.push(path.relative(targetRoot, settingsPath));
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
function writeProseEditorSettingsForProject(projectRoot, options) {
|
|
233
|
+
const vscodeDir = path.join(projectRoot, ".vscode");
|
|
234
|
+
const settingsPath = path.join(vscodeDir, "settings.json");
|
|
235
|
+
ensureDirectory(vscodeDir);
|
|
236
|
+
const enableProseFont = options?.enableProseFont ?? true;
|
|
237
|
+
const commentAuthor = (options?.commentAuthor ?? "").trim();
|
|
238
|
+
const existingSettings = tryReadJsonObject(settingsPath) ?? {};
|
|
239
|
+
const proseMarkdownSettings = isPlainObject(PROSE_MARKDOWN_EDITOR_SETTINGS["[markdown]"])
|
|
240
|
+
? PROSE_MARKDOWN_EDITOR_SETTINGS["[markdown]"]
|
|
241
|
+
: {};
|
|
242
|
+
const nextSettings = {
|
|
243
|
+
...existingSettings
|
|
244
|
+
};
|
|
245
|
+
if (enableProseFont) {
|
|
246
|
+
const existingMarkdownSettings = isPlainObject(existingSettings["[markdown]"])
|
|
247
|
+
? existingSettings["[markdown]"]
|
|
248
|
+
: {};
|
|
249
|
+
nextSettings["[markdown]"] = {
|
|
250
|
+
...existingMarkdownSettings,
|
|
251
|
+
...proseMarkdownSettings
|
|
252
|
+
};
|
|
253
|
+
nextSettings["markdown.preview.fontFamily"] = PROSE_MARKDOWN_EDITOR_SETTINGS["markdown.preview.fontFamily"];
|
|
254
|
+
}
|
|
255
|
+
if (commentAuthor) {
|
|
256
|
+
nextSettings["stego.comments.author"] = commentAuthor;
|
|
257
|
+
}
|
|
258
|
+
writeTextFile(settingsPath, `${JSON.stringify(nextSettings, null, 2)}\n`);
|
|
259
|
+
return settingsPath;
|
|
260
|
+
}
|
|
261
|
+
function writeInitRootPackageJson(targetRoot, packageRoot) {
|
|
262
|
+
const cliPackagePath = path.join(packageRoot, "package.json");
|
|
263
|
+
const cliPackage = tryReadJsonObject(cliPackagePath) ?? {};
|
|
264
|
+
const cliVersion = typeof cliPackage.version === "string" ? cliPackage.version : "0.1.0";
|
|
265
|
+
const manifest = {
|
|
266
|
+
name: path.basename(targetRoot) || "stego-workspace",
|
|
267
|
+
private: true,
|
|
268
|
+
type: "module",
|
|
269
|
+
description: "Stego writing workspace",
|
|
270
|
+
engines: {
|
|
271
|
+
node: ">=20"
|
|
272
|
+
},
|
|
273
|
+
scripts: {
|
|
274
|
+
"list-projects": "stego list-projects",
|
|
275
|
+
"new-project": "stego new-project",
|
|
276
|
+
new: "stego new",
|
|
277
|
+
spine: "stego spine",
|
|
278
|
+
metadata: "stego metadata",
|
|
279
|
+
lint: "stego lint",
|
|
280
|
+
validate: "stego validate",
|
|
281
|
+
build: "stego build",
|
|
282
|
+
"check-stage": "stego check-stage",
|
|
283
|
+
export: "stego export"
|
|
284
|
+
},
|
|
285
|
+
devDependencies: {
|
|
286
|
+
"stego-cli": `^${cliVersion}`,
|
|
287
|
+
cspell: "^9.6.4",
|
|
288
|
+
"markdownlint-cli": "^0.47.0"
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
writeTextFile(path.join(targetRoot, "package.json"), `${JSON.stringify(manifest, null, 2)}\n`);
|
|
292
|
+
}
|
|
293
|
+
function tryReadJsonObject(filePath) {
|
|
294
|
+
if (!pathExists(filePath)) {
|
|
295
|
+
return null;
|
|
296
|
+
}
|
|
297
|
+
try {
|
|
298
|
+
const parsed = JSON.parse(readTextFile(filePath));
|
|
299
|
+
return isPlainObject(parsed) ? parsed : null;
|
|
300
|
+
}
|
|
301
|
+
catch {
|
|
302
|
+
return null;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
function isPlainObject(value) {
|
|
306
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
307
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { writeText } from "../../../app/output-renderer.js";
|
|
2
|
+
import { scaffoldWorkspace } from "../application/scaffold-workspace.js";
|
|
3
|
+
export function registerInitCommand(registry) {
|
|
4
|
+
registry.register({
|
|
5
|
+
name: "init",
|
|
6
|
+
description: "Initialize a new Stego workspace",
|
|
7
|
+
options: [
|
|
8
|
+
{
|
|
9
|
+
flags: "--force",
|
|
10
|
+
description: "Allow init in non-empty directory"
|
|
11
|
+
}
|
|
12
|
+
],
|
|
13
|
+
action: async (context) => {
|
|
14
|
+
const result = await scaffoldWorkspace({
|
|
15
|
+
cwd: context.cwd,
|
|
16
|
+
force: readBooleanOption(context.options, "force")
|
|
17
|
+
});
|
|
18
|
+
writeText(`Initialized Stego workspace in ${result.targetRoot}`);
|
|
19
|
+
for (const relativePath of result.copiedPaths) {
|
|
20
|
+
writeText(`- ${relativePath}`);
|
|
21
|
+
}
|
|
22
|
+
writeText("- package.json");
|
|
23
|
+
writeText("");
|
|
24
|
+
writeText("Next steps:");
|
|
25
|
+
writeText(" npm install");
|
|
26
|
+
writeText(" stego list-projects");
|
|
27
|
+
writeText(" stego validate --project fiction-example");
|
|
28
|
+
writeText(" stego build --project fiction-example");
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
function readBooleanOption(options, key) {
|
|
33
|
+
return options[key] === true;
|
|
34
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
export const PROSE_FONT_PROMPT = "Switch workspace to proportional (prose-style) font? (recommended)";
|
|
2
|
+
export const COMMENT_AUTHOR_PROMPT = "Default comment author for stego.comments.author?";
|
|
3
|
+
export const SCAFFOLD_GITIGNORE_CONTENT = `node_modules/
|
|
4
|
+
/dist/
|
|
5
|
+
.DS_Store
|
|
6
|
+
*.log
|
|
7
|
+
projects/*/dist/*
|
|
8
|
+
!projects/*/dist/.gitkeep
|
|
9
|
+
projects/*/.vscode/settings.json
|
|
10
|
+
.vscode/settings.json
|
|
11
|
+
`;
|
|
12
|
+
export const SCAFFOLD_README_CONTENT = `# Stego Workspace
|
|
13
|
+
|
|
14
|
+
This directory is a Stego writing workspace (a monorepo for one or more writing projects).
|
|
15
|
+
|
|
16
|
+
## What was scaffolded
|
|
17
|
+
|
|
18
|
+
- \`stego.config.json\` workspace configuration
|
|
19
|
+
- \`projects/\` demo projects (\`stego-docs\` and \`fiction-example\`)
|
|
20
|
+
- root \`package.json\` scripts for Stego commands
|
|
21
|
+
- root \`.vscode/tasks.json\` tasks for common workflows
|
|
22
|
+
|
|
23
|
+
Full documentation lives in \`projects/stego-docs\`.
|
|
24
|
+
|
|
25
|
+
## First run
|
|
26
|
+
|
|
27
|
+
\`\`\`bash
|
|
28
|
+
npm install
|
|
29
|
+
stego list-projects
|
|
30
|
+
\`\`\`
|
|
31
|
+
|
|
32
|
+
## Run commands for a specific project (from workspace root)
|
|
33
|
+
|
|
34
|
+
\`\`\`bash
|
|
35
|
+
stego validate --project fiction-example
|
|
36
|
+
stego build --project fiction-example
|
|
37
|
+
stego check-stage --project fiction-example --stage revise
|
|
38
|
+
stego export --project fiction-example --format md
|
|
39
|
+
stego new --project fiction-example
|
|
40
|
+
\`\`\`
|
|
41
|
+
|
|
42
|
+
## Work inside one project
|
|
43
|
+
|
|
44
|
+
Each project also has local scripts, so you can run commands from inside a project directory:
|
|
45
|
+
|
|
46
|
+
\`\`\`bash
|
|
47
|
+
cd projects/fiction-example
|
|
48
|
+
npm run validate
|
|
49
|
+
npm run build
|
|
50
|
+
\`\`\`
|
|
51
|
+
|
|
52
|
+
## VS Code recommendation
|
|
53
|
+
|
|
54
|
+
When you are actively working on one project, open that project directory directly in VS Code (for example \`projects/fiction-example\`).
|
|
55
|
+
|
|
56
|
+
This keeps your editor context focused and applies the project's recommended extensions (including Stego + Saurus) for that project.
|
|
57
|
+
|
|
58
|
+
## Create a new project
|
|
59
|
+
|
|
60
|
+
\`\`\`bash
|
|
61
|
+
stego new-project --project my-book --title "My Book"
|
|
62
|
+
\`\`\`
|
|
63
|
+
|
|
64
|
+
## Add a new manuscript file
|
|
65
|
+
|
|
66
|
+
\`\`\`bash
|
|
67
|
+
stego new --project fiction-example
|
|
68
|
+
\`\`\`
|
|
69
|
+
`;
|
|
70
|
+
export const SCAFFOLD_AGENTS_CONTENT = `# AGENTS.md
|
|
71
|
+
|
|
72
|
+
## Purpose
|
|
73
|
+
|
|
74
|
+
This workspace is designed to be AI-friendly for writing workflows.
|
|
75
|
+
|
|
76
|
+
## Canonical CLI Interface
|
|
77
|
+
|
|
78
|
+
- Run \`stego --help\` for the full command reference.
|
|
79
|
+
- Run \`stego --version\` to confirm which CLI is active.
|
|
80
|
+
- Run project docs commands in \`projects/stego-docs\` when available.
|
|
81
|
+
|
|
82
|
+
## CLI Resolution Rules
|
|
83
|
+
|
|
84
|
+
- Prefer local CLI over global CLI:
|
|
85
|
+
- \`npm exec -- stego ...\`
|
|
86
|
+
- \`npx --no-install stego ...\`
|
|
87
|
+
- At the start of mutation tasks, run \`stego --version\` and report the version used.
|
|
88
|
+
|
|
89
|
+
## Workspace Discovery Checklist
|
|
90
|
+
|
|
91
|
+
1. Confirm workspace root contains \`stego.config.json\`.
|
|
92
|
+
2. Run \`stego list-projects\`.
|
|
93
|
+
3. Use explicit \`--project <id>\` for project-scoped commands.
|
|
94
|
+
|
|
95
|
+
## CLI-First Policy (Required)
|
|
96
|
+
|
|
97
|
+
When asked to edit Stego project content, use documented Stego CLI commands first.
|
|
98
|
+
|
|
99
|
+
Typical targets:
|
|
100
|
+
|
|
101
|
+
- manuscript files
|
|
102
|
+
- spine categories and entries
|
|
103
|
+
- frontmatter metadata
|
|
104
|
+
- comments
|
|
105
|
+
- stage/build/export workflows
|
|
106
|
+
|
|
107
|
+
Preferred commands include:
|
|
108
|
+
|
|
109
|
+
- \`stego new\`
|
|
110
|
+
- \`stego spine read\`
|
|
111
|
+
- \`stego spine new-category\`
|
|
112
|
+
- \`stego spine new\`
|
|
113
|
+
- \`stego metadata read\`
|
|
114
|
+
- \`stego metadata apply\`
|
|
115
|
+
- \`stego comments ...\`
|
|
116
|
+
|
|
117
|
+
## Machine-Mode Output
|
|
118
|
+
|
|
119
|
+
- For automation and integrations, prefer \`--format json\` and parse structured output.
|
|
120
|
+
- Use text output only for human-facing summaries.
|
|
121
|
+
|
|
122
|
+
## Mutation Protocol
|
|
123
|
+
|
|
124
|
+
1. Read current state first (\`metadata read\`, \`spine read\`, \`comments read\`).
|
|
125
|
+
2. Mutate via CLI commands.
|
|
126
|
+
3. Verify after writes (\`stego validate --project <id>\` and relevant read commands).
|
|
127
|
+
|
|
128
|
+
## Manual Edit Fallback
|
|
129
|
+
|
|
130
|
+
Manual file edits are a last resort.
|
|
131
|
+
|
|
132
|
+
If manual edits are required, the agent must:
|
|
133
|
+
|
|
134
|
+
1. warn that CLI was bypassed,
|
|
135
|
+
2. explain why CLI could not be used, and
|
|
136
|
+
3. list which files were manually edited.
|
|
137
|
+
|
|
138
|
+
## Failure Contract
|
|
139
|
+
|
|
140
|
+
When CLI fails:
|
|
141
|
+
|
|
142
|
+
1. show the attempted command,
|
|
143
|
+
2. summarize the error briefly,
|
|
144
|
+
3. report the recovery attempt, and
|
|
145
|
+
4. if fallback is required, apply the Manual Edit Fallback policy.
|
|
146
|
+
|
|
147
|
+
## Validation Expectations
|
|
148
|
+
|
|
149
|
+
After mutations, run relevant checks when feasible (for example \`stego validate --project <id>\`) and report results.
|
|
150
|
+
|
|
151
|
+
## Scope Guardrails
|
|
152
|
+
|
|
153
|
+
- Do not manually edit \`dist/\` outputs or compiled export artifacts.
|
|
154
|
+
- Do not modify files outside the requested project scope unless the user explicitly asks.
|
|
155
|
+
|
|
156
|
+
## Task To Command Quick Map
|
|
157
|
+
|
|
158
|
+
- New manuscript: \`stego new --project <id> [--filename <name>]\`
|
|
159
|
+
- Read spine: \`stego spine read --project <id> --format json\`
|
|
160
|
+
- New spine category: \`stego spine new-category --project <id> --key <category>\`
|
|
161
|
+
- New spine entry: \`stego spine new --project <id> --category <category> [--filename <path>]\`
|
|
162
|
+
- Read metadata: \`stego metadata read <markdown-path> --format json\`
|
|
163
|
+
- Apply metadata: \`stego metadata apply <markdown-path> --input <path|-> --format json\`
|
|
164
|
+
- Read comments: \`stego comments read <manuscript> --format json\`
|
|
165
|
+
- Mutate comments: \`stego comments add|reply|set-status|delete|clear-resolved|sync-anchors ... --format json\`
|
|
166
|
+
`;
|
|
167
|
+
export const PROSE_MARKDOWN_EDITOR_SETTINGS = {
|
|
168
|
+
"[markdown]": {
|
|
169
|
+
"editor.fontFamily": "Inter, Helvetica Neue, Helvetica, Arial, sans-serif",
|
|
170
|
+
"editor.fontSize": 17,
|
|
171
|
+
"editor.lineHeight": 28,
|
|
172
|
+
"editor.wordWrap": "wordWrapColumn",
|
|
173
|
+
"editor.wordWrapColumn": 72,
|
|
174
|
+
"editor.lineNumbers": "off"
|
|
175
|
+
},
|
|
176
|
+
"markdown.preview.fontFamily": "Inter, Helvetica Neue, Helvetica, Arial, sans-serif"
|
|
177
|
+
};
|
|
178
|
+
export const PROJECT_EXTENSION_RECOMMENDATIONS = [
|
|
179
|
+
"matt-gold.stego-extension",
|
|
180
|
+
"matt-gold.saurus-extension",
|
|
181
|
+
"streetsidesoftware.code-spell-checker"
|
|
182
|
+
];
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
export function listDirectoryEntries(dirPath) {
|
|
4
|
+
return fs.readdirSync(dirPath, { withFileTypes: true });
|
|
5
|
+
}
|
|
6
|
+
export function pathExists(filePath) {
|
|
7
|
+
return fs.existsSync(filePath);
|
|
8
|
+
}
|
|
9
|
+
export function statPath(filePath) {
|
|
10
|
+
return fs.statSync(filePath);
|
|
11
|
+
}
|
|
12
|
+
export function ensureDirectory(dirPath) {
|
|
13
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
14
|
+
}
|
|
15
|
+
export function copyFile(sourcePath, destinationPath) {
|
|
16
|
+
ensureDirectory(path.dirname(destinationPath));
|
|
17
|
+
fs.copyFileSync(sourcePath, destinationPath);
|
|
18
|
+
}
|
|
19
|
+
export function copyDirectory(sourcePath, destinationPath, filter) {
|
|
20
|
+
ensureDirectory(destinationPath);
|
|
21
|
+
fs.cpSync(sourcePath, destinationPath, {
|
|
22
|
+
recursive: true,
|
|
23
|
+
force: true,
|
|
24
|
+
filter
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
export function writeTextFile(filePath, content) {
|
|
28
|
+
ensureDirectory(path.dirname(filePath));
|
|
29
|
+
fs.writeFileSync(filePath, content, "utf8");
|
|
30
|
+
}
|
|
31
|
+
export function readTextFile(filePath) {
|
|
32
|
+
return fs.readFileSync(filePath, "utf8");
|
|
33
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { createSpineCategory } from "../domain/spine.js";
|
|
2
|
+
import { readRequiredMetadata, writeRequiredMetadata } from "../infra/spine-repo.js";
|
|
3
|
+
export function createSpineCategoryForProject(input) {
|
|
4
|
+
const requiredMetadata = readRequiredMetadata(input.project.meta);
|
|
5
|
+
const result = createSpineCategory(input.project.root, input.project.spineDir, input.key, input.label, requiredMetadata, input.requireMetadata);
|
|
6
|
+
if (input.requireMetadata && result.requiredMetadataUpdated) {
|
|
7
|
+
writeRequiredMetadata(input.project.root, input.project.meta, requiredMetadata);
|
|
8
|
+
}
|
|
9
|
+
return {
|
|
10
|
+
ok: true,
|
|
11
|
+
operation: "new-category",
|
|
12
|
+
result
|
|
13
|
+
};
|
|
14
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { createSpineEntry } from "../domain/spine.js";
|
|
2
|
+
export function createSpineEntryForProject(input) {
|
|
3
|
+
const result = createSpineEntry(input.project.root, input.project.spineDir, input.category, input.filename);
|
|
4
|
+
return {
|
|
5
|
+
ok: true,
|
|
6
|
+
operation: "new",
|
|
7
|
+
result
|
|
8
|
+
};
|
|
9
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { readSpineCatalog } from "../domain/spine.js";
|
|
2
|
+
export function readSpineCatalogForProject(project) {
|
|
3
|
+
const catalog = readSpineCatalog(project.root, project.spineDir);
|
|
4
|
+
return {
|
|
5
|
+
ok: true,
|
|
6
|
+
operation: "read",
|
|
7
|
+
state: {
|
|
8
|
+
projectId: project.id,
|
|
9
|
+
categories: catalog.categories,
|
|
10
|
+
issues: catalog.issues
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export function registerSpineDeprecatedAliases(registry) {
|
|
2
|
+
registry.register({
|
|
3
|
+
name: "spine add-category",
|
|
4
|
+
description: "Deprecated alias for spine new-category",
|
|
5
|
+
allowUnknownOptions: true,
|
|
6
|
+
options: [
|
|
7
|
+
{ flags: "--project <project-id>", description: "Project id" },
|
|
8
|
+
{ flags: "--key <category>", description: "Category key" },
|
|
9
|
+
{ flags: "--label <label>", description: "Category display label" },
|
|
10
|
+
{ flags: "--require-metadata", description: "Append key to required metadata" },
|
|
11
|
+
{ flags: "--format <format>", description: "text|json" },
|
|
12
|
+
{ flags: "--root <path>", description: "Workspace root path" }
|
|
13
|
+
],
|
|
14
|
+
action: () => {
|
|
15
|
+
throw new Error("`stego spine add-category` was renamed. Use `stego spine new-category`.");
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
registry.register({
|
|
19
|
+
name: "spine new-entry",
|
|
20
|
+
description: "Deprecated alias for spine new",
|
|
21
|
+
allowUnknownOptions: true,
|
|
22
|
+
options: [
|
|
23
|
+
{ flags: "--project <project-id>", description: "Project id" },
|
|
24
|
+
{ flags: "--category <category>", description: "Category key" },
|
|
25
|
+
{ flags: "--filename <path>", description: "Relative entry path" },
|
|
26
|
+
{ flags: "--format <format>", description: "text|json" },
|
|
27
|
+
{ flags: "--root <path>", description: "Workspace root path" }
|
|
28
|
+
],
|
|
29
|
+
action: () => {
|
|
30
|
+
throw new Error("`stego spine new-entry` was renamed. Use `stego spine new`.");
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
}
|