@brainervirus/workit-core 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +10 -14
- package/scripts/doctor-check.ts +20 -0
- package/scripts/install-cursor-plugin.sh +51 -28
- package/scripts/install-opencode-plugin.sh +25 -22
- package/scripts/rewrite-workspace-deps.ts +15 -9
- package/scripts/sync-runtime.sh +71 -19
- package/scripts/vendor-assets.ts +37 -0
- package/skills/wk-implement/SKILL.md +2 -2
- package/skills/wk-pr/SKILL.md +1 -1
- package/src/core/boundary.ts +27 -0
- package/src/core/branch-policy.ts +63 -0
- package/src/core/branch.ts +166 -57
- package/src/core/changelog.ts +17 -14
- package/src/core/config-guard.ts +9 -2
- package/src/core/config.ts +227 -32
- package/src/core/detector.ts +22 -11
- package/src/core/docs-layout.ts +251 -0
- package/src/core/docs-migration.ts +639 -0
- package/src/core/docs-repo.ts +58 -21
- package/src/core/docs-validate.ts +181 -43
- package/src/core/doctor.ts +801 -0
- package/src/core/flow-state.ts +1579 -137
- package/src/core/git.ts +22 -5
- package/src/core/gitignore.ts +11 -2
- package/src/core/handoff-context.ts +18 -5
- package/src/{tools/handoff.ts → core/handoff-tools.ts} +16 -63
- package/src/core/hygiene.ts +53 -17
- package/src/core/init.ts +128 -31
- package/src/core/logger.ts +321 -0
- package/src/core/package-root.ts +28 -0
- package/src/core/parse-sections.ts +2 -2
- package/src/core/plan-tasks.ts +13 -3
- package/src/core/ports/init-toolkit-status.ts +1 -1
- package/src/core/ports/vcs-verify-token.ts +1 -1
- package/src/core/ports/youtrack-api.ts +4 -2
- package/src/core/ports/youtrack-config.ts +1 -3
- package/src/core/ports/youtrack-verify-token.ts +1 -1
- package/src/core/pr-create.ts +156 -29
- package/src/core/present.ts +11 -2
- package/src/core/registration.ts +215 -0
- package/src/core/reminder.ts +1 -2
- package/src/core/repo-context.ts +447 -0
- package/src/core/repo-tool.ts +4 -1
- package/src/core/repo-tools.ts +23 -0
- package/src/core/rules.ts +10 -7
- package/src/core/safe-write.ts +22 -0
- package/src/core/scripts.ts +3 -39
- package/src/core/sdd.ts +56 -31
- package/src/core/setup-state.ts +54 -0
- package/src/core/setup.ts +1216 -0
- package/src/core/skill-manifests.ts +95 -0
- package/src/core/support-matrix.ts +12 -0
- package/src/core/sync-runtime.ts +348 -0
- package/src/core/templates.ts +16 -6
- package/src/core/vcs-config.ts +191 -62
- package/src/core/verify-parse.ts +4 -2
- package/src/core/verify-project.ts +181 -0
- package/src/core/workspaces.ts +136 -17
- package/src/core/youtrack-tools.ts +228 -0
- package/src/core/youtrack.ts +320 -87
- package/src/core.ts +18 -3
- package/templates/execution-contract.md +9 -7
- package/templates/superpowers-doc-contract.md +5 -4
- package/scripts/_shared/common.sh +0 -158
- package/scripts/changelog-context.sh +0 -42
- package/scripts/docs-refresh-context.sh +0 -40
- package/scripts/init/apply.sh +0 -5
- package/scripts/init/status.sh +0 -5
- package/scripts/init/toolkit-status.sh +0 -5
- package/scripts/pr-create.sh +0 -5
- package/scripts/pr-ready-context.sh +0 -88
- package/scripts/present/ascii-wireframe.sh +0 -5
- package/scripts/present/flow-diagram.sh +0 -5
- package/scripts/release-notes-context.sh +0 -40
- package/scripts/vcs/config.sh +0 -5
- package/scripts/vcs/merged-style.sh +0 -5
- package/scripts/vcs/token-create-urls.sh +0 -5
- package/scripts/vcs/verify-token.sh +0 -5
- package/scripts/verify-project.sh +0 -140
- package/scripts/youtrack/api.sh +0 -5
- package/scripts/youtrack/config.sh +0 -5
- package/scripts/youtrack/greeting.sh +0 -5
- package/scripts/youtrack/parse-duration.sh +0 -5
- package/scripts/youtrack/token-create-url.sh +0 -5
- package/scripts/youtrack/verify-token.sh +0 -5
- package/scripts/youtrack/work-date-ms.sh +0 -5
- package/src/tools/docs-repo.ts +0 -42
- package/src/tools/flow.ts +0 -88
- package/src/tools/index.ts +0 -22
- package/src/tools/present.ts +0 -45
- package/src/tools/repo.ts +0 -357
- package/src/tools/rules.ts +0 -30
- package/src/tools/sdd.ts +0 -189
- package/src/tools/templates.ts +0 -27
- package/src/tools/youtrack.ts +0 -360
package/src/core/git.ts
CHANGED
|
@@ -2,7 +2,13 @@ import { execFileSync } from "node:child_process";
|
|
|
2
2
|
|
|
3
3
|
const run = (cwd: string, args: string[]): { stdout: string; stderr: string; exitCode: number } => {
|
|
4
4
|
try {
|
|
5
|
-
const stdout = execFileSync("git", args, {
|
|
5
|
+
const stdout = execFileSync("git", args, {
|
|
6
|
+
cwd,
|
|
7
|
+
encoding: "utf8",
|
|
8
|
+
// AR-14: expected negative fixtures (non-repo dirs, unborn HEADs, bogus
|
|
9
|
+
// shas) must not inherit raw git usage/fatal stderr into the suite output.
|
|
10
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
11
|
+
});
|
|
6
12
|
return { stdout: stdout.trimEnd(), stderr: "", exitCode: 0 };
|
|
7
13
|
} catch (error) {
|
|
8
14
|
const e = error as { stdout?: string; stderr?: string; status?: number };
|
|
@@ -16,8 +22,18 @@ const run = (cwd: string, args: string[]): { stdout: string; stderr: string; exi
|
|
|
16
22
|
|
|
17
23
|
export const gitContext = (workspaceRoot: string, paths: string[] = []) => {
|
|
18
24
|
const cwd = workspaceRoot;
|
|
19
|
-
|
|
20
|
-
|
|
25
|
+
let failure: { stderr: string; exitCode: number } | undefined;
|
|
26
|
+
|
|
27
|
+
const runHere = (args: string[]) => {
|
|
28
|
+
const result = run(cwd, args);
|
|
29
|
+
if (!failure && result.exitCode !== 0) {
|
|
30
|
+
failure = { stderr: result.stderr, exitCode: result.exitCode };
|
|
31
|
+
}
|
|
32
|
+
return result.stdout;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const branch = runHere(["rev-parse", "--abbrev-ref", "HEAD"]) || "unknown";
|
|
36
|
+
const status_short = runHere(["status", "--porcelain"]);
|
|
21
37
|
|
|
22
38
|
const staged: string[] = [];
|
|
23
39
|
const unstaged: string[] = [];
|
|
@@ -35,8 +51,8 @@ export const gitContext = (workspaceRoot: string, paths: string[] = []) => {
|
|
|
35
51
|
}
|
|
36
52
|
|
|
37
53
|
const pathArgs = paths.length ? ["--", ...paths] : [];
|
|
38
|
-
const diff_stat =
|
|
39
|
-
const cached_stat =
|
|
54
|
+
const diff_stat = runHere(["diff", "--stat", ...pathArgs]);
|
|
55
|
+
const cached_stat = runHere(["diff", "--cached", "--stat", ...pathArgs]);
|
|
40
56
|
|
|
41
57
|
const stagedSet = new Set(staged);
|
|
42
58
|
const unstagedSet = new Set(unstaged);
|
|
@@ -52,5 +68,6 @@ export const gitContext = (workspaceRoot: string, paths: string[] = []) => {
|
|
|
52
68
|
diff_stat: [diff_stat, cached_stat].filter(Boolean).join("\n"),
|
|
53
69
|
partial_staged: partial_staged.length > 0,
|
|
54
70
|
partial_staged_files: partial_staged,
|
|
71
|
+
...(failure ? { stderr: failure.stderr, exitCode: failure.exitCode } : {}),
|
|
55
72
|
};
|
|
56
73
|
};
|
package/src/core/gitignore.ts
CHANGED
|
@@ -25,7 +25,12 @@ export const ensureProjectGitignore = (
|
|
|
25
25
|
if (!confirmed) return { ok: false, error: "confirmed: true required" };
|
|
26
26
|
const file = path.join(workspaceRoot, ".gitignore");
|
|
27
27
|
const existing = existsSync(file) ? readFileSync(file, "utf8") : "";
|
|
28
|
-
const existingLines = new Set(
|
|
28
|
+
const existingLines = new Set(
|
|
29
|
+
existing
|
|
30
|
+
.split("\n")
|
|
31
|
+
.map((l) => l.trim())
|
|
32
|
+
.filter(Boolean),
|
|
33
|
+
);
|
|
29
34
|
const added: string[] = [];
|
|
30
35
|
const append: string[] = [];
|
|
31
36
|
for (const entry of GITIGNORE_ENTRIES) {
|
|
@@ -35,7 +40,11 @@ export const ensureProjectGitignore = (
|
|
|
35
40
|
}
|
|
36
41
|
if (append.length) {
|
|
37
42
|
const separator = existing && !existing.endsWith("\n") ? "\n" : "";
|
|
38
|
-
writeFileSync(
|
|
43
|
+
writeFileSync(
|
|
44
|
+
file,
|
|
45
|
+
existing + separator + (existing ? "\n" : "") + append.join("\n") + "\n",
|
|
46
|
+
"utf8",
|
|
47
|
+
);
|
|
39
48
|
} else if (!existsSync(file)) {
|
|
40
49
|
writeFileSync(file, "", "utf8");
|
|
41
50
|
}
|
|
@@ -25,7 +25,8 @@ const resolveFromMessagePaths = (root: string, message: string): Resolved => {
|
|
|
25
25
|
const paths = extractMessagePaths(message);
|
|
26
26
|
if (paths.length === 0) return { error: "no paths" };
|
|
27
27
|
const slugs = [...new Set(paths.map((p) => p.split("/")[1]))];
|
|
28
|
-
if (slugs.length !== 1)
|
|
28
|
+
if (slugs.length !== 1)
|
|
29
|
+
return { error: "multiple features in message — use exactly one docs/<slug>/ pair" };
|
|
29
30
|
const slug = slugs[0];
|
|
30
31
|
const plan = `docs/${slug}/plan.md`;
|
|
31
32
|
const spec = `docs/${slug}/spec.md`;
|
|
@@ -49,8 +50,15 @@ const resolveActivePair = (root: string): Resolved => {
|
|
|
49
50
|
const plan = path.join("docs", slug, "plan.md");
|
|
50
51
|
const spec = path.join("docs", slug, "spec.md");
|
|
51
52
|
if (!existsSync(path.join(root, plan)) || !existsSync(path.join(root, spec))) continue;
|
|
52
|
-
const score = Math.max(
|
|
53
|
-
|
|
53
|
+
const score = Math.max(
|
|
54
|
+
statSync(path.join(root, spec)).mtimeMs,
|
|
55
|
+
statSync(path.join(root, plan)).mtimeMs,
|
|
56
|
+
);
|
|
57
|
+
if (
|
|
58
|
+
best === null ||
|
|
59
|
+
score > best.score ||
|
|
60
|
+
(score === best.score && slug < best.spec.split("/")[1])
|
|
61
|
+
) {
|
|
54
62
|
best = { score, spec, plan, source: "active_pair" };
|
|
55
63
|
}
|
|
56
64
|
}
|
|
@@ -69,7 +77,10 @@ export const resolveWorkflowPaths = (root: string, message: string): Resolved =>
|
|
|
69
77
|
if (!existsSync(docsDir) || listMd(docsDir).length === 0) {
|
|
70
78
|
return { error: "no docs/<slug>/ features found under docs/" };
|
|
71
79
|
}
|
|
72
|
-
return {
|
|
80
|
+
return {
|
|
81
|
+
error:
|
|
82
|
+
"could not resolve spec and plan — mention docs/<slug>/plan.md or create docs/<slug>/{spec.md,plan.md}",
|
|
83
|
+
};
|
|
73
84
|
};
|
|
74
85
|
|
|
75
86
|
export const buildHandoffContract = ({
|
|
@@ -85,7 +96,9 @@ export const buildHandoffContract = ({
|
|
|
85
96
|
}): { prompt: string } | { error: string } => {
|
|
86
97
|
const validated = docsValidate({ spec_path: spec, plan_path: plan, workspace_root: root });
|
|
87
98
|
if (validated.ok === false) {
|
|
88
|
-
return {
|
|
99
|
+
return {
|
|
100
|
+
error: `docs validation failed\n${JSON.stringify({ ok: false, errors: validated.errors })}`,
|
|
101
|
+
};
|
|
89
102
|
}
|
|
90
103
|
const branchResolved = resolveBranch({ spec_path: spec, plan_path: plan, workspace_root: root });
|
|
91
104
|
if ("error" in branchResolved) return { error: branchResolved.error as string };
|
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
-
import { fileURLToPath } from "node:url";
|
|
3
|
-
import { tool, type PluginInput } from "@opencode-ai/plugin";
|
|
4
2
|
import { fail, ok, type Result } from "../core";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { WorkflowStateStore } from "../state";
|
|
3
|
+
import { resolveWorkflowPaths, buildHandoffContract } from "./handoff-context";
|
|
4
|
+
import { assetRoot } from "./package-root";
|
|
8
5
|
|
|
9
6
|
type ApiResponse<T> = { data?: T; error?: unknown };
|
|
10
7
|
type ApiResult<T> = Promise<ApiResponse<T>>;
|
|
@@ -31,19 +28,6 @@ export type HandoffClient = {
|
|
|
31
28
|
};
|
|
32
29
|
};
|
|
33
30
|
|
|
34
|
-
export const adaptPluginHandoffClient = (client: PluginInput["client"]): HandoffClient => ({
|
|
35
|
-
session: {
|
|
36
|
-
create: (input) => client.session.create(input),
|
|
37
|
-
promptAsync: (input) => client.session.promptAsync(input),
|
|
38
|
-
},
|
|
39
|
-
tui: {
|
|
40
|
-
selectSession: ({ body, query }) => client.tui.publish({
|
|
41
|
-
body: { type: "tui.session.select", properties: { sessionID: body.sessionID } } as never,
|
|
42
|
-
query,
|
|
43
|
-
}),
|
|
44
|
-
},
|
|
45
|
-
});
|
|
46
|
-
|
|
47
31
|
export type HandoffRequest = {
|
|
48
32
|
directory: string;
|
|
49
33
|
title: string;
|
|
@@ -58,12 +42,13 @@ type HandoffData = {
|
|
|
58
42
|
stage?: "create" | "seed" | "select";
|
|
59
43
|
};
|
|
60
44
|
|
|
61
|
-
const message = (error: unknown) =>
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
45
|
+
export const message = (error: unknown) =>
|
|
46
|
+
error instanceof Error
|
|
47
|
+
? error.message
|
|
48
|
+
: typeof error === "object" && error !== null && "message" in error
|
|
49
|
+
? String(error.message)
|
|
50
|
+
: String(error);
|
|
51
|
+
export const apiError = (response: ApiResponse<unknown> | void) => response?.error;
|
|
67
52
|
|
|
68
53
|
export async function handoffSession(
|
|
69
54
|
client: HandoffClient,
|
|
@@ -108,8 +93,6 @@ export async function handoffSession(
|
|
|
108
93
|
}
|
|
109
94
|
}
|
|
110
95
|
|
|
111
|
-
const output = (value: unknown) => JSON.stringify(value, null, 2);
|
|
112
|
-
|
|
113
96
|
export type HandoffContextResult =
|
|
114
97
|
| { prompt: string; spec: string; plan: string; sdd: string }
|
|
115
98
|
| { error: string };
|
|
@@ -117,44 +100,14 @@ export type HandoffContextResult =
|
|
|
117
100
|
export const buildHandoffPrompt = (root: string, message: string): HandoffContextResult => {
|
|
118
101
|
const resolved = resolveWorkflowPaths(root, message);
|
|
119
102
|
if ("error" in resolved) return { error: resolved.error };
|
|
120
|
-
const templatePath = path.
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
103
|
+
const templatePath = path.join(assetRoot(), "templates", "execution-contract.md");
|
|
104
|
+
const contract = buildHandoffContract({
|
|
105
|
+
root,
|
|
106
|
+
spec: resolved.spec,
|
|
107
|
+
plan: resolved.plan,
|
|
108
|
+
templatePath,
|
|
109
|
+
});
|
|
125
110
|
if ("error" in contract) return { error: contract.error };
|
|
126
111
|
const sdd = `docs/${path.basename(path.dirname(resolved.plan))}/sdd`;
|
|
127
112
|
return { prompt: contract.prompt, spec: resolved.spec, plan: resolved.plan, sdd };
|
|
128
113
|
};
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
export function createHandoffTools(
|
|
133
|
-
client: HandoffClient,
|
|
134
|
-
state: WorkflowStateStore,
|
|
135
|
-
) {
|
|
136
|
-
return {
|
|
137
|
-
workflow_handoff_session: tool({
|
|
138
|
-
description: "Create, seed, and select a continuation session; --stay in the message skips selection",
|
|
139
|
-
args: { message: tool.schema.string() },
|
|
140
|
-
execute: async ({ message: userMessage }, context) => {
|
|
141
|
-
const built = buildHandoffPrompt(context.directory, userMessage);
|
|
142
|
-
if ("error" in built) return output(fail(built.error));
|
|
143
|
-
const active = built;
|
|
144
|
-
try {
|
|
145
|
-
const gate = assertFlowGates(context.directory, active.plan);
|
|
146
|
-
if (!gate.ok) return output(fail(gate.error));
|
|
147
|
-
state.set(context.sessionID, { spec: active.spec, plan: active.plan, sdd: active.sdd });
|
|
148
|
-
return output(await handoffSession(client, {
|
|
149
|
-
directory: context.directory,
|
|
150
|
-
title: `Continue ${path.basename(path.dirname(active.plan))}`,
|
|
151
|
-
prompt: active.prompt,
|
|
152
|
-
stay: /(?:^|\s)--stay(?:\s|$)/.test(userMessage),
|
|
153
|
-
}));
|
|
154
|
-
} catch (error) {
|
|
155
|
-
return output(fail(message(error)));
|
|
156
|
-
}
|
|
157
|
-
},
|
|
158
|
-
}),
|
|
159
|
-
};
|
|
160
|
-
}
|
package/src/core/hygiene.ts
CHANGED
|
@@ -1,17 +1,27 @@
|
|
|
1
1
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { fileURLToPath } from "node:url";
|
|
4
3
|
import { changelogUnreleasedStats } from "./changelog";
|
|
4
|
+
import { assetRoot } from "./package-root";
|
|
5
5
|
|
|
6
|
-
export type HygieneFile =
|
|
6
|
+
export type HygieneFile =
|
|
7
|
+
| "CHANGELOG.md"
|
|
8
|
+
| "README.md"
|
|
9
|
+
| ".editorconfig"
|
|
10
|
+
| ".gitattributes"
|
|
11
|
+
| "LICENSE"
|
|
12
|
+
| "CONTRIBUTING.md";
|
|
7
13
|
type State = "missing" | "invalid" | "ok" | "skip";
|
|
8
14
|
|
|
9
|
-
const repoRoot =
|
|
15
|
+
const repoRoot = assetRoot();
|
|
10
16
|
const templatesDir = () => path.join(repoRoot, "templates", "hygiene");
|
|
11
17
|
|
|
12
18
|
const packageJson = (root: string): Record<string, unknown> | null => {
|
|
13
19
|
if (!existsSync(path.join(root, "package.json"))) return null;
|
|
14
|
-
try {
|
|
20
|
+
try {
|
|
21
|
+
return JSON.parse(readFileSync(path.join(root, "package.json"), "utf8"));
|
|
22
|
+
} catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
15
25
|
};
|
|
16
26
|
|
|
17
27
|
const isOpenSource = (root: string): boolean => {
|
|
@@ -34,15 +44,27 @@ const licenseHolder = (root: string): string => {
|
|
|
34
44
|
return "";
|
|
35
45
|
};
|
|
36
46
|
|
|
37
|
-
export const hygieneFiles = (
|
|
47
|
+
export const hygieneFiles = (
|
|
48
|
+
root: string,
|
|
49
|
+
): { state: Record<HygieneFile, State>; openSource: boolean } => {
|
|
38
50
|
const openSource = isOpenSource(root);
|
|
39
51
|
const state = {} as Record<HygieneFile, State>;
|
|
40
|
-
for (const file of [
|
|
52
|
+
for (const file of [
|
|
53
|
+
"CHANGELOG.md",
|
|
54
|
+
"README.md",
|
|
55
|
+
".editorconfig",
|
|
56
|
+
".gitattributes",
|
|
57
|
+
"LICENSE",
|
|
58
|
+
"CONTRIBUTING.md",
|
|
59
|
+
] as HygieneFile[]) {
|
|
41
60
|
if (file === "LICENSE" || file === "CONTRIBUTING.md") {
|
|
42
61
|
state[file] = openSource ? (existsSync(path.join(root, file)) ? "ok" : "missing") : "skip";
|
|
43
62
|
continue;
|
|
44
63
|
}
|
|
45
|
-
if (!existsSync(path.join(root, file))) {
|
|
64
|
+
if (!existsSync(path.join(root, file))) {
|
|
65
|
+
state[file] = "missing";
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
46
68
|
if (file === "CHANGELOG.md") {
|
|
47
69
|
const stats = changelogUnreleasedStats(root);
|
|
48
70
|
state[file] = stats.exists && stats.has_unreleased ? "ok" : "invalid";
|
|
@@ -53,25 +75,39 @@ export const hygieneFiles = (root: string): { state: Record<HygieneFile, State>;
|
|
|
53
75
|
return { state, openSource };
|
|
54
76
|
};
|
|
55
77
|
|
|
56
|
-
|
|
78
|
+
// Pure planner: which hygiene files would be created and with what content,
|
|
79
|
+
// without touching the filesystem. Drives the wizard preview (WZ-08) and the
|
|
80
|
+
// apply path; ensureHygieneFiles is just "plan then write".
|
|
81
|
+
export const planHygieneFiles = (
|
|
57
82
|
root: string,
|
|
58
|
-
opts: {
|
|
59
|
-
): {
|
|
60
|
-
|
|
83
|
+
opts: { includeOpenSource?: boolean } = {},
|
|
84
|
+
): Array<{ path: string; content: string }> => {
|
|
85
|
+
const openSource = opts.includeOpenSource ?? isOpenSource(root);
|
|
61
86
|
const files = ["CHANGELOG.md", "README.md", ".editorconfig", ".gitattributes"] as HygieneFile[];
|
|
62
|
-
if (
|
|
63
|
-
const
|
|
64
|
-
const tplDir = templatesDir();
|
|
87
|
+
if (openSource) files.push("LICENSE", "CONTRIBUTING.md");
|
|
88
|
+
const planned: Array<{ path: string; content: string }> = [];
|
|
65
89
|
for (const file of files) {
|
|
66
90
|
if (existsSync(path.join(root, file))) continue;
|
|
67
|
-
const tpl = path.join(
|
|
91
|
+
const tpl = path.join(templatesDir(), file);
|
|
68
92
|
if (!existsSync(tpl)) continue; // skip missing template, never fail
|
|
69
93
|
const content = readFileSync(tpl, "utf8")
|
|
70
94
|
.replace(/<PROJECT>/g, path.basename(root))
|
|
71
95
|
.replace(/<YEAR>/g, String(new Date().getFullYear()))
|
|
72
96
|
.replace(/<HOLDER>\s*/g, licenseHolder(root));
|
|
73
|
-
|
|
74
|
-
|
|
97
|
+
planned.push({ path: path.join(root, file), content });
|
|
98
|
+
}
|
|
99
|
+
return planned;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
export const ensureHygieneFiles = (
|
|
103
|
+
root: string,
|
|
104
|
+
opts: { confirmed: boolean; includeOpenSource?: boolean },
|
|
105
|
+
): { ok: true; created: string[] } | { ok: false; error: string } => {
|
|
106
|
+
if (!opts.confirmed) return { ok: false, error: "confirmed: true required" };
|
|
107
|
+
const created: string[] = [];
|
|
108
|
+
for (const planned of planHygieneFiles(root, opts)) {
|
|
109
|
+
writeFileSync(planned.path, planned.content, "utf8");
|
|
110
|
+
created.push(path.basename(planned.path));
|
|
75
111
|
}
|
|
76
112
|
return { ok: true, created };
|
|
77
113
|
};
|