@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.
Files changed (44) hide show
  1. package/README.md +2 -2
  2. package/package.json +2 -3
  3. package/scripts/sync-runtime.sh +9 -5
  4. package/src/core/detect-hosts.ts +24 -22
  5. package/src/core/doctor.ts +11 -4
  6. package/src/core/host-install.ts +458 -0
  7. package/src/core/init.ts +1 -256
  8. package/src/core/policy-resolver.ts +0 -45
  9. package/src/core/registration.ts +3 -4
  10. package/src/core/repo-context.ts +1 -1
  11. package/src/core/rules.ts +1 -61
  12. package/src/core/setup.ts +203 -11
  13. package/src/core/templates.ts +1 -26
  14. package/src/core/vcs-config.ts +2 -8
  15. package/src/core/workspaces.ts +18 -17
  16. package/src/core/config-guard.ts +0 -27
  17. package/src/core/doc-render.ts +0 -14
  18. package/src/core/docs-layout.ts +0 -279
  19. package/src/core/docs-migration.ts +0 -639
  20. package/src/core/docs-repo.ts +0 -233
  21. package/src/core/docs-validate.ts +0 -393
  22. package/src/core/parse-sections.ts +0 -24
  23. package/src/core/ports/init-apply.ts +0 -15
  24. package/src/core/ports/init-status.ts +0 -4
  25. package/src/core/ports/init-toolkit-status.ts +0 -4
  26. package/src/core/ports/pr-create.ts +0 -30
  27. package/src/core/ports/present-ascii.ts +0 -10
  28. package/src/core/ports/present-flow.ts +0 -10
  29. package/src/core/ports/vcs-config.ts +0 -14
  30. package/src/core/ports/vcs-merged-style.ts +0 -5
  31. package/src/core/ports/vcs-verify-token.ts +0 -4
  32. package/src/core/ports/youtrack-api.ts +0 -19
  33. package/src/core/ports/youtrack-config.ts +0 -21
  34. package/src/core/ports/youtrack-greeting.ts +0 -10
  35. package/src/core/ports/youtrack-parse-duration.ts +0 -14
  36. package/src/core/ports/youtrack-token-create-url.ts +0 -4
  37. package/src/core/ports/youtrack-verify-token.ts +0 -12
  38. package/src/core/ports/youtrack-work-date-ms.ts +0 -10
  39. package/src/core/present.ts +0 -109
  40. package/src/core/repo-tool.ts +0 -12
  41. package/src/core/repo-tools.ts +0 -22
  42. package/src/core/sync-runtime.ts +0 -375
  43. package/src/core/verify-parse.ts +0 -29
  44. package/src/core/verify-project.ts +0 -181
@@ -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
- };