@brainervirus/workit-core 2.0.2 → 2.1.1
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 +2 -2
- package/package.json +2 -3
- package/scripts/sync-runtime.sh +9 -5
- package/src/core/detect-hosts.ts +24 -22
- package/src/core/doctor.ts +11 -4
- package/src/core/host-install.ts +458 -0
- package/src/core/init.ts +1 -256
- package/src/core/policy-resolver.ts +0 -45
- package/src/core/registration.ts +3 -4
- package/src/core/repo-context.ts +1 -1
- package/src/core/rules.ts +1 -61
- package/src/core/setup.ts +203 -11
- package/src/core/templates.ts +1 -26
- package/src/core/vcs-config.ts +2 -8
- package/src/core/workspaces.ts +18 -17
- package/src/core/config-guard.ts +0 -27
- package/src/core/doc-render.ts +0 -14
- package/src/core/docs-layout.ts +0 -279
- package/src/core/docs-migration.ts +0 -639
- package/src/core/docs-repo.ts +0 -233
- package/src/core/docs-validate.ts +0 -393
- package/src/core/parse-sections.ts +0 -24
- package/src/core/ports/init-apply.ts +0 -15
- package/src/core/ports/init-status.ts +0 -4
- package/src/core/ports/init-toolkit-status.ts +0 -4
- package/src/core/ports/pr-create.ts +0 -30
- package/src/core/ports/present-ascii.ts +0 -10
- package/src/core/ports/present-flow.ts +0 -10
- package/src/core/ports/vcs-config.ts +0 -14
- package/src/core/ports/vcs-merged-style.ts +0 -5
- package/src/core/ports/vcs-verify-token.ts +0 -4
- package/src/core/ports/youtrack-api.ts +0 -19
- package/src/core/ports/youtrack-config.ts +0 -21
- package/src/core/ports/youtrack-greeting.ts +0 -10
- package/src/core/ports/youtrack-parse-duration.ts +0 -14
- package/src/core/ports/youtrack-token-create-url.ts +0 -4
- package/src/core/ports/youtrack-verify-token.ts +0 -12
- package/src/core/ports/youtrack-work-date-ms.ts +0 -10
- package/src/core/present.ts +0 -109
- package/src/core/repo-tool.ts +0 -12
- package/src/core/repo-tools.ts +0 -22
- package/src/core/sync-runtime.ts +0 -375
- package/src/core/verify-parse.ts +0 -29
- package/src/core/verify-project.ts +0 -181
package/src/core/docs-layout.ts
DELETED
|
@@ -1,279 +0,0 @@
|
|
|
1
|
-
import { existsSync, lstatSync, mkdirSync, realpathSync, statSync } from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
|
|
4
|
-
// One canonical document path contract (DC-01, DC-02, DC-04, DC-14): workspace
|
|
5
|
-
// root, slug, and spec/plan pair resolution all funnel through here so every
|
|
6
|
-
// document/flow/SDD consumer on both hosts enforces the same containment rules.
|
|
7
|
-
|
|
8
|
-
export type CanonicalLayout = {
|
|
9
|
-
/** Canonical (realpath) workspace root. */
|
|
10
|
-
workspace: string;
|
|
11
|
-
/** Validated slug. */
|
|
12
|
-
slug: string;
|
|
13
|
-
/** Canonical path of docs/. */
|
|
14
|
-
docs: string;
|
|
15
|
-
/** Canonical path of docs/<slug>/. */
|
|
16
|
-
dir: string;
|
|
17
|
-
/** Canonical path of docs/<slug>/spec.md. */
|
|
18
|
-
spec: string;
|
|
19
|
-
/** Canonical path of docs/<slug>/plan.md. */
|
|
20
|
-
plan: string;
|
|
21
|
-
/** Canonical path of docs/<slug>/sdd/. */
|
|
22
|
-
sdd: string;
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
export type LayoutResult = { ok: true; layout: CanonicalLayout } | { ok: false; error: string };
|
|
26
|
-
|
|
27
|
-
export type LegacyProbe = {
|
|
28
|
-
/** `.superpowers/sdd` exists. */
|
|
29
|
-
legacy_sdd: boolean;
|
|
30
|
-
/** `docs/superpowers/` exists. */
|
|
31
|
-
superpowers_dir: boolean;
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
export type PrepareResult =
|
|
35
|
-
| { ok: true; layout: CanonicalLayout; created: string[]; legacy: LegacyProbe }
|
|
36
|
-
| { ok: false; error: string };
|
|
37
|
-
|
|
38
|
-
const SLUG_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
39
|
-
|
|
40
|
-
/** Reserved legacy root (DC-05): never resolve or prepare a slug under it. */
|
|
41
|
-
const LEGACY_SLUG = "superpowers";
|
|
42
|
-
|
|
43
|
-
const posix = (p: string) => p.split(path.sep).join("/");
|
|
44
|
-
|
|
45
|
-
// Realpath the nearest existing ancestor of `candidate` and reject the result
|
|
46
|
-
// when it escapes `base` (base must already be canonical). The returned path is
|
|
47
|
-
// canonical where it exists and joined for the non-existent tail.
|
|
48
|
-
const canonicalize = (base: string, candidate: string): string => {
|
|
49
|
-
const abs = path.resolve(base, candidate);
|
|
50
|
-
let ancestor = abs;
|
|
51
|
-
while (!existsSync(ancestor)) ancestor = path.dirname(ancestor);
|
|
52
|
-
let real: string;
|
|
53
|
-
try {
|
|
54
|
-
real = realpathSync(ancestor);
|
|
55
|
-
} catch (error) {
|
|
56
|
-
// macOS realpath fails with EACCES on a mode-000 file while Linux succeeds;
|
|
57
|
-
// an existing non-symlink file cannot escape the workspace, so resolve the
|
|
58
|
-
// nearest existing directory instead (symlinks keep the strict path).
|
|
59
|
-
if (
|
|
60
|
-
(error as NodeJS.ErrnoException).code === "EACCES" &&
|
|
61
|
-
!lstatSync(ancestor).isSymbolicLink()
|
|
62
|
-
) {
|
|
63
|
-
real = path.join(realpathSync(path.dirname(ancestor)), path.basename(ancestor));
|
|
64
|
-
} else {
|
|
65
|
-
throw error;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
if (real !== base && !real.startsWith(base + path.sep)) {
|
|
69
|
-
throw new Error(`path must stay inside repository root: ${candidate}`);
|
|
70
|
-
}
|
|
71
|
-
return path.join(real, path.relative(ancestor, abs));
|
|
72
|
-
};
|
|
73
|
-
|
|
74
|
-
const buildLayout = (workspace: string, slug: string): CanonicalLayout => {
|
|
75
|
-
const docs = canonicalize(workspace, "docs");
|
|
76
|
-
const dir = canonicalize(workspace, path.join(docs, slug));
|
|
77
|
-
return {
|
|
78
|
-
workspace,
|
|
79
|
-
slug,
|
|
80
|
-
docs,
|
|
81
|
-
dir,
|
|
82
|
-
spec: path.join(dir, "spec.md"),
|
|
83
|
-
plan: path.join(dir, "plan.md"),
|
|
84
|
-
sdd: path.join(dir, "sdd"),
|
|
85
|
-
};
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
/** Read-only legacy detection (DC-04): never mutates legacy state. */
|
|
89
|
-
export const probeLegacyDocs = (workspace: string): LegacyProbe => ({
|
|
90
|
-
legacy_sdd: existsSync(path.join(workspace, ".superpowers", "sdd")),
|
|
91
|
-
superpowers_dir: existsSync(path.join(workspace, "docs", LEGACY_SLUG)),
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* The one canonical workspace/slug/pair resolver. Given a slug or any spec/plan
|
|
96
|
-
* pair path, returns canonical paths for the pair. Rejects absolute paths,
|
|
97
|
-
* traversal, symlink escapes, cross-slug pairs, wrong basenames, and arbitrary
|
|
98
|
-
* or legacy locations (DC-01, DC-02). Creates nothing.
|
|
99
|
-
*/
|
|
100
|
-
export const resolveCanonicalLayout = (input: {
|
|
101
|
-
workspace_root: string;
|
|
102
|
-
slug?: string;
|
|
103
|
-
spec_path?: string;
|
|
104
|
-
plan_path?: string;
|
|
105
|
-
}): LayoutResult => {
|
|
106
|
-
const { workspace_root, slug, spec_path, plan_path } = input;
|
|
107
|
-
if (!workspace_root) return { ok: false, error: "workspace_root required" };
|
|
108
|
-
let workspace: string;
|
|
109
|
-
try {
|
|
110
|
-
workspace = realpathSync(path.resolve(workspace_root));
|
|
111
|
-
} catch {
|
|
112
|
-
return { ok: false, error: `workspace root not found: ${workspace_root}` };
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
let derived: string | null = null;
|
|
116
|
-
for (const [candidate, kind] of [
|
|
117
|
-
[spec_path, "spec"],
|
|
118
|
-
[plan_path, "plan"],
|
|
119
|
-
] as const) {
|
|
120
|
-
if (!candidate) continue;
|
|
121
|
-
if (path.isAbsolute(candidate)) {
|
|
122
|
-
return { ok: false, error: `absolute path not allowed: ${candidate}` };
|
|
123
|
-
}
|
|
124
|
-
// Exact-spelling contract (DC-01): the caller path must be written as the
|
|
125
|
-
// canonical `docs/<slug>/spec.md` / `docs/<slug>/plan.md` — no `./`,
|
|
126
|
-
// no `..` segments, no repeated or trailing separators. The strict regex
|
|
127
|
-
// below rejects those spellings before any bytes are read or resolved.
|
|
128
|
-
const spelling = posix(candidate);
|
|
129
|
-
const match = spelling.match(/^docs\/([^/]+)\/(spec|plan)\.md$/);
|
|
130
|
-
if (!match) {
|
|
131
|
-
return {
|
|
132
|
-
ok: false,
|
|
133
|
-
error: `path must be docs/<slug>/(spec|plan).md inside workspace_root: ${candidate}`,
|
|
134
|
-
};
|
|
135
|
-
}
|
|
136
|
-
const pathSlug = match[1];
|
|
137
|
-
if (pathSlug === LEGACY_SLUG) {
|
|
138
|
-
return { ok: false, error: `legacy path not allowed: ${candidate}` };
|
|
139
|
-
}
|
|
140
|
-
if (!SLUG_RE.test(pathSlug)) {
|
|
141
|
-
return { ok: false, error: `invalid slug derived from path: ${JSON.stringify(pathSlug)}` };
|
|
142
|
-
}
|
|
143
|
-
if (match[2] !== kind) {
|
|
144
|
-
return {
|
|
145
|
-
ok: false,
|
|
146
|
-
error: `wrong basename for ${kind}: expected ${kind}.md, got ${path.basename(candidate)}`,
|
|
147
|
-
};
|
|
148
|
-
}
|
|
149
|
-
if (derived && derived !== pathSlug) {
|
|
150
|
-
return {
|
|
151
|
-
ok: false,
|
|
152
|
-
error: "cross-slug pair: spec_path and plan_path must share the same docs/<slug>/",
|
|
153
|
-
};
|
|
154
|
-
}
|
|
155
|
-
derived = pathSlug;
|
|
156
|
-
// Symlink/canonical containment (DC-02): after the exact-spelling match,
|
|
157
|
-
// resolve the canonical path so a symlinked docs/<slug> or doc file that
|
|
158
|
-
// escapes the workspace or resolves to a different slug is still rejected.
|
|
159
|
-
let abs: string;
|
|
160
|
-
try {
|
|
161
|
-
abs = canonicalize(workspace, candidate);
|
|
162
|
-
} catch (error) {
|
|
163
|
-
return { ok: false, error: error instanceof Error ? error.message : String(error) };
|
|
164
|
-
}
|
|
165
|
-
if (posix(path.relative(workspace, abs)) !== spelling) {
|
|
166
|
-
return {
|
|
167
|
-
ok: false,
|
|
168
|
-
error: `path must resolve to ${JSON.stringify(spelling)}: ${candidate}`,
|
|
169
|
-
};
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
let resolvedSlug = slug;
|
|
174
|
-
if (resolvedSlug !== undefined) {
|
|
175
|
-
if (!SLUG_RE.test(resolvedSlug)) {
|
|
176
|
-
return { ok: false, error: `invalid slug: ${JSON.stringify(resolvedSlug)}` };
|
|
177
|
-
}
|
|
178
|
-
if (resolvedSlug === LEGACY_SLUG) {
|
|
179
|
-
return { ok: false, error: `reserved slug: ${LEGACY_SLUG}` };
|
|
180
|
-
}
|
|
181
|
-
if (derived && derived !== resolvedSlug) {
|
|
182
|
-
return {
|
|
183
|
-
ok: false,
|
|
184
|
-
error: `slug ${JSON.stringify(resolvedSlug)} does not match docs path ${JSON.stringify(derived)}`,
|
|
185
|
-
};
|
|
186
|
-
}
|
|
187
|
-
} else if (derived) {
|
|
188
|
-
resolvedSlug = derived;
|
|
189
|
-
} else {
|
|
190
|
-
return { ok: false, error: "slug or spec_path/plan_path required" };
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
try {
|
|
194
|
-
return { ok: true, layout: buildLayout(workspace, resolvedSlug) };
|
|
195
|
-
} catch (error) {
|
|
196
|
-
return { ok: false, error: error instanceof Error ? error.message : String(error) };
|
|
197
|
-
}
|
|
198
|
-
};
|
|
199
|
-
|
|
200
|
-
/**
|
|
201
|
-
* Prepare the canonical layout (DC-04): create only missing `docs/` and
|
|
202
|
-
* `docs/<slug>/`, return canonical (realpath) paths, and probe legacy state
|
|
203
|
-
* read-only. Never creates sdd/, spec.md, or plan.md.
|
|
204
|
-
*/
|
|
205
|
-
export const prepareDocsLayout = (input: {
|
|
206
|
-
workspace_root: string;
|
|
207
|
-
slug?: string;
|
|
208
|
-
spec_path?: string;
|
|
209
|
-
plan_path?: string;
|
|
210
|
-
}): PrepareResult => {
|
|
211
|
-
const resolved = resolveCanonicalLayout(input);
|
|
212
|
-
if (!resolved.ok) return { ok: false, error: resolved.error };
|
|
213
|
-
const { layout } = resolved;
|
|
214
|
-
const created: string[] = [];
|
|
215
|
-
const ensure = (dir: string) => {
|
|
216
|
-
if (!existsSync(dir)) {
|
|
217
|
-
mkdirSync(dir, { recursive: true });
|
|
218
|
-
const rel = posix(path.relative(layout.workspace, dir));
|
|
219
|
-
created.push(rel || ".");
|
|
220
|
-
} else if (!statSync(dir).isDirectory()) {
|
|
221
|
-
throw new Error(
|
|
222
|
-
`path exists but is not a directory: ${posix(path.relative(layout.workspace, dir))}`,
|
|
223
|
-
);
|
|
224
|
-
}
|
|
225
|
-
};
|
|
226
|
-
try {
|
|
227
|
-
ensure(layout.docs);
|
|
228
|
-
ensure(layout.dir);
|
|
229
|
-
// Re-canonicalize after creation so a symlinked docs/<slug> that escapes
|
|
230
|
-
// the workspace is rejected, not silently accepted (DC-02).
|
|
231
|
-
const docs = canonicalize(layout.workspace, "docs");
|
|
232
|
-
const dir = canonicalize(layout.workspace, path.join(docs, layout.slug));
|
|
233
|
-
return {
|
|
234
|
-
ok: true,
|
|
235
|
-
layout: { ...layout, docs, dir },
|
|
236
|
-
created,
|
|
237
|
-
legacy: probeLegacyDocs(layout.workspace),
|
|
238
|
-
};
|
|
239
|
-
} catch (error) {
|
|
240
|
-
return { ok: false, error: error instanceof Error ? error.message : String(error) };
|
|
241
|
-
}
|
|
242
|
-
};
|
|
243
|
-
|
|
244
|
-
/**
|
|
245
|
-
* Containment check for paths that must live inside the workspace docs tree
|
|
246
|
-
* (sdd dirs, progress files, plan links). Rejects absolute paths, escapes, and
|
|
247
|
-
* the reserved legacy root `docs/superpowers/`.
|
|
248
|
-
*/
|
|
249
|
-
export const resolveDocsPath = (input: {
|
|
250
|
-
workspace_root: string;
|
|
251
|
-
path: string;
|
|
252
|
-
}): { ok: true; path: string; relative: string; base: string } | { ok: false; error: string } => {
|
|
253
|
-
if (path.isAbsolute(input.path)) {
|
|
254
|
-
return { ok: false, error: `absolute path not allowed: ${input.path}` };
|
|
255
|
-
}
|
|
256
|
-
let workspace: string;
|
|
257
|
-
try {
|
|
258
|
-
workspace = realpathSync(path.resolve(input.workspace_root));
|
|
259
|
-
} catch {
|
|
260
|
-
return { ok: false, error: `workspace root not found: ${input.workspace_root}` };
|
|
261
|
-
}
|
|
262
|
-
try {
|
|
263
|
-
const abs = canonicalize(workspace, input.path);
|
|
264
|
-
const relative = posix(path.relative(workspace, abs));
|
|
265
|
-
if (
|
|
266
|
-
!relative.startsWith("docs/") ||
|
|
267
|
-
relative === `docs/${LEGACY_SLUG}` ||
|
|
268
|
-
relative.startsWith(`docs/${LEGACY_SLUG}/`)
|
|
269
|
-
) {
|
|
270
|
-
return {
|
|
271
|
-
ok: false,
|
|
272
|
-
error: `path must live under docs/ and not under docs/${LEGACY_SLUG}/: ${input.path}`,
|
|
273
|
-
};
|
|
274
|
-
}
|
|
275
|
-
return { ok: true, path: abs, relative, base: workspace };
|
|
276
|
-
} catch (error) {
|
|
277
|
-
return { ok: false, error: error instanceof Error ? error.message : String(error) };
|
|
278
|
-
}
|
|
279
|
-
};
|