@indigoai-us/hq-cli 5.87.0 → 5.88.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/CHANGELOG.md +9 -0
- package/dist/commands/core.d.ts +14 -3
- package/dist/commands/core.js +75 -24
- package/dist/lib/index-render/companies.d.ts +3 -0
- package/dist/lib/index-render/companies.js +57 -0
- package/dist/lib/index-render/company-knowledge.d.ts +3 -0
- package/dist/lib/index-render/company-knowledge.js +85 -0
- package/dist/lib/index-render/index.d.ts +5 -0
- package/dist/lib/index-render/index.js +43 -0
- package/dist/lib/index-render/orchestrator.d.ts +3 -0
- package/dist/lib/index-render/orchestrator.js +52 -0
- package/dist/lib/index-render/projects.d.ts +3 -0
- package/dist/lib/index-render/projects.js +33 -0
- package/dist/lib/index-render/public-knowledge.d.ts +3 -0
- package/dist/lib/index-render/public-knowledge.js +37 -0
- package/dist/lib/index-render/reports.d.ts +3 -0
- package/dist/lib/index-render/reports.js +37 -0
- package/dist/lib/index-render/shared.d.ts +35 -0
- package/dist/lib/index-render/shared.js +126 -0
- package/dist/lib/index-render/social-drafts.d.ts +3 -0
- package/dist/lib/index-render/social-drafts.js +53 -0
- package/dist/lib/index-render/threads.d.ts +3 -0
- package/dist/lib/index-render/threads.js +42 -0
- package/dist/lib/index-render/workers.d.ts +3 -0
- package/dist/lib/index-render/workers.js +49 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.88.0]
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- Replaced the ten bundled `rebuild-*-index.sh` assets behind
|
|
10
|
+
`hq core rebuild-index <target>` with native, tested TypeScript renderers.
|
|
11
|
+
The command surface and scaffold-only assets remain unchanged, while rebuilds
|
|
12
|
+
no longer depend on bundled scaffold assets being present. (#311)
|
|
13
|
+
|
|
5
14
|
## [5.87.0]
|
|
6
15
|
|
|
7
16
|
### Added
|
package/dist/commands/core.d.ts
CHANGED
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
* `ScaffoldRoot`.
|
|
30
30
|
*/
|
|
31
31
|
import { Command } from "commander";
|
|
32
|
+
import { type IndexTarget } from "../lib/index-render/index.js";
|
|
32
33
|
/**
|
|
33
34
|
* Which tree a scaffold script operates on, mirroring how the ORIGINAL script
|
|
34
35
|
* found its root before it moved. Getting this wrong is not cosmetic: injecting a
|
|
@@ -64,9 +65,13 @@ export type ScaffoldCommand = ScaffoldAsset & {
|
|
|
64
65
|
/** Direct subcommand name — `hq core <name>`. */
|
|
65
66
|
name: string;
|
|
66
67
|
};
|
|
67
|
-
export type RebuildIndexTarget =
|
|
68
|
+
export type RebuildIndexTarget = {
|
|
68
69
|
/** Rebuild target — `hq core rebuild-index <target>`. */
|
|
69
70
|
target: string;
|
|
71
|
+
/** Native renderer identifier. */
|
|
72
|
+
renderer: IndexTarget;
|
|
73
|
+
/** One-line summary, shown in `hq core --help`. */
|
|
74
|
+
summary: string;
|
|
70
75
|
};
|
|
71
76
|
export type WorkerSubcommand = ScaffoldAsset & {
|
|
72
77
|
/** Subcommand name under the `worker` group — `hq core worker <name>`. */
|
|
@@ -86,7 +91,7 @@ export declare const WORKER_SUBCOMMANDS: WorkerSubcommand[];
|
|
|
86
91
|
/**
|
|
87
92
|
* The ten index rebuild targets. The command registration below intentionally
|
|
88
93
|
* loops over this table for lookup, so adding a target never requires another
|
|
89
|
-
* action handler and every target retains its own
|
|
94
|
+
* action handler and every target retains its own native renderer.
|
|
90
95
|
*/
|
|
91
96
|
export declare const REBUILD_INDEX_TARGETS: RebuildIndexTarget[];
|
|
92
97
|
/**
|
|
@@ -98,7 +103,13 @@ export declare const REBUILD_INDEX_TARGETS: RebuildIndexTarget[];
|
|
|
98
103
|
* would make the forwarders harder to audit against this table.
|
|
99
104
|
*/
|
|
100
105
|
export declare const SCAFFOLD_COMMANDS: ScaffoldCommand[];
|
|
101
|
-
/**
|
|
106
|
+
/**
|
|
107
|
+
* Bundled solely for HQ-root scaffolds, which still need these as loose files.
|
|
108
|
+
* `rebuild-index` dispatches them natively since this change, so none has a
|
|
109
|
+
* direct `hq core` command.
|
|
110
|
+
*/
|
|
111
|
+
export declare const SCAFFOLD_ONLY_ASSETS: ScaffoldAsset[];
|
|
112
|
+
/** Every claimed bundled asset, used by packaging and interpreter tests. */
|
|
102
113
|
export declare const SCAFFOLD_ASSETS: ScaffoldAsset[];
|
|
103
114
|
export declare function registerCoreCommands(program: Command): Command;
|
|
104
115
|
export {};
|
package/dist/commands/core.js
CHANGED
|
@@ -32,6 +32,7 @@ import { Option } from "commander";
|
|
|
32
32
|
import { registerCoreCheckpointCommand } from "./core-checkpoint.js";
|
|
33
33
|
import { resolveLiveRoot } from "../utils/hq-roots.js";
|
|
34
34
|
import { runBundledScript } from "../utils/run-bundled-script.js";
|
|
35
|
+
import { renderIndexTarget } from "../lib/index-render/index.js";
|
|
35
36
|
/**
|
|
36
37
|
* The `hq core worker <name>` subgroup — worker-scoped skill maintenance.
|
|
37
38
|
*
|
|
@@ -59,67 +60,57 @@ export const WORKER_SUBCOMMANDS = [
|
|
|
59
60
|
/**
|
|
60
61
|
* The ten index rebuild targets. The command registration below intentionally
|
|
61
62
|
* loops over this table for lookup, so adding a target never requires another
|
|
62
|
-
* action handler and every target retains its own
|
|
63
|
+
* action handler and every target retains its own native renderer.
|
|
63
64
|
*/
|
|
64
65
|
export const REBUILD_INDEX_TARGETS = [
|
|
65
66
|
{
|
|
66
67
|
target: "all",
|
|
67
|
-
|
|
68
|
-
root: "live",
|
|
68
|
+
renderer: "all",
|
|
69
69
|
summary: "Regenerate every INDEX.md across the HQ tree",
|
|
70
70
|
},
|
|
71
71
|
{
|
|
72
72
|
target: "companies",
|
|
73
|
-
|
|
74
|
-
root: "live",
|
|
73
|
+
renderer: "companies",
|
|
75
74
|
summary: "Regenerate companies/INDEX.md",
|
|
76
75
|
},
|
|
77
76
|
{
|
|
78
77
|
target: "company-knowledge",
|
|
79
|
-
|
|
80
|
-
root: "live",
|
|
78
|
+
renderer: "company-knowledge",
|
|
81
79
|
summary: "Regenerate a company's knowledge INDEX.md",
|
|
82
80
|
},
|
|
83
81
|
{
|
|
84
82
|
target: "orchestrator",
|
|
85
|
-
|
|
86
|
-
root: "live",
|
|
83
|
+
renderer: "orchestrator",
|
|
87
84
|
summary: "Regenerate the orchestrator workspace INDEX.md",
|
|
88
85
|
},
|
|
89
86
|
{
|
|
90
87
|
target: "projects",
|
|
91
|
-
|
|
92
|
-
root: "live",
|
|
88
|
+
renderer: "projects",
|
|
93
89
|
summary: "Regenerate the projects INDEX.md",
|
|
94
90
|
},
|
|
95
91
|
{
|
|
96
92
|
target: "public-knowledge",
|
|
97
|
-
|
|
98
|
-
root: "live",
|
|
93
|
+
renderer: "public-knowledge",
|
|
99
94
|
summary: "Regenerate core/knowledge/public/INDEX.md",
|
|
100
95
|
},
|
|
101
96
|
{
|
|
102
97
|
target: "reports",
|
|
103
|
-
|
|
104
|
-
root: "live",
|
|
98
|
+
renderer: "reports",
|
|
105
99
|
summary: "Regenerate the reports INDEX.md",
|
|
106
100
|
},
|
|
107
101
|
{
|
|
108
102
|
target: "social-drafts",
|
|
109
|
-
|
|
110
|
-
root: "live",
|
|
103
|
+
renderer: "social-drafts",
|
|
111
104
|
summary: "Regenerate workspace/social-drafts/INDEX.md",
|
|
112
105
|
},
|
|
113
106
|
{
|
|
114
107
|
target: "threads",
|
|
115
|
-
|
|
116
|
-
root: "live",
|
|
108
|
+
renderer: "threads",
|
|
117
109
|
summary: "Regenerate the session-threads INDEX.md",
|
|
118
110
|
},
|
|
119
111
|
{
|
|
120
112
|
target: "workers",
|
|
121
|
-
|
|
122
|
-
root: "live",
|
|
113
|
+
renderer: "workers",
|
|
123
114
|
summary: "Regenerate the workers INDEX.md",
|
|
124
115
|
},
|
|
125
116
|
];
|
|
@@ -201,10 +192,67 @@ export const SCAFFOLD_COMMANDS = [
|
|
|
201
192
|
summary: "Create a git worktree under workspace/worktrees/",
|
|
202
193
|
},
|
|
203
194
|
];
|
|
204
|
-
/**
|
|
195
|
+
/**
|
|
196
|
+
* Bundled solely for HQ-root scaffolds, which still need these as loose files.
|
|
197
|
+
* `rebuild-index` dispatches them natively since this change, so none has a
|
|
198
|
+
* direct `hq core` command.
|
|
199
|
+
*/
|
|
200
|
+
export const SCAFFOLD_ONLY_ASSETS = [
|
|
201
|
+
{
|
|
202
|
+
asset: "core/scripts/rebuild-all-indexes.sh",
|
|
203
|
+
root: "cwd",
|
|
204
|
+
summary: "Scaffold-only native rebuild-index companion",
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
asset: "core/scripts/rebuild-companies-index.sh",
|
|
208
|
+
root: "cwd",
|
|
209
|
+
summary: "Scaffold-only native rebuild-index companion",
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
asset: "core/scripts/rebuild-company-knowledge-index.sh",
|
|
213
|
+
root: "cwd",
|
|
214
|
+
summary: "Scaffold-only native rebuild-index companion",
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
asset: "core/scripts/rebuild-orchestrator-index.sh",
|
|
218
|
+
root: "cwd",
|
|
219
|
+
summary: "Scaffold-only native rebuild-index companion",
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
asset: "core/scripts/rebuild-projects-index.sh",
|
|
223
|
+
root: "cwd",
|
|
224
|
+
summary: "Scaffold-only native rebuild-index companion",
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
asset: "core/scripts/rebuild-public-knowledge-index.sh",
|
|
228
|
+
root: "cwd",
|
|
229
|
+
summary: "Scaffold-only native rebuild-index companion",
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
asset: "core/scripts/rebuild-reports-index.sh",
|
|
233
|
+
root: "cwd",
|
|
234
|
+
summary: "Scaffold-only native rebuild-index companion",
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
asset: "core/scripts/rebuild-social-drafts-index.sh",
|
|
238
|
+
root: "cwd",
|
|
239
|
+
summary: "Scaffold-only native rebuild-index companion",
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
asset: "core/scripts/rebuild-threads-index.sh",
|
|
243
|
+
root: "cwd",
|
|
244
|
+
summary: "Scaffold-only native rebuild-index companion",
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
asset: "core/scripts/rebuild-workers-index.sh",
|
|
248
|
+
root: "cwd",
|
|
249
|
+
summary: "Scaffold-only native rebuild-index companion",
|
|
250
|
+
},
|
|
251
|
+
];
|
|
252
|
+
/** Every claimed bundled asset, used by packaging and interpreter tests. */
|
|
205
253
|
export const SCAFFOLD_ASSETS = [
|
|
206
|
-
...REBUILD_INDEX_TARGETS,
|
|
207
254
|
...SCAFFOLD_COMMANDS,
|
|
255
|
+
...SCAFFOLD_ONLY_ASSETS,
|
|
208
256
|
...WORKER_SUBCOMMANDS,
|
|
209
257
|
];
|
|
210
258
|
/**
|
|
@@ -307,7 +355,10 @@ export function registerCoreCommands(program) {
|
|
|
307
355
|
if (!entry) {
|
|
308
356
|
throw expectedUserError(`Unknown rebuild-index target "${target}". Valid targets: ${validTargets.join(", ")}.`);
|
|
309
357
|
}
|
|
310
|
-
|
|
358
|
+
const scope = core.opts();
|
|
359
|
+
const hqRoot = resolveLiveRoot({ hqRoot: scope.hqRoot });
|
|
360
|
+
const operands = cmd.args.length > 0 ? cmd.args : args;
|
|
361
|
+
renderIndexTarget(entry.renderer, { root: hqRoot, log: (message) => process.stderr.write(`${message}\n`) }, operands.slice(1));
|
|
311
362
|
});
|
|
312
363
|
for (const entry of SCAFFOLD_COMMANDS) {
|
|
313
364
|
core
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import { basename, countImmediate, date, exists, immediateEntries, isHidden, log, projectStatus, readJson, sanitize, titleize, truncate, write } from "./shared.js";
|
|
4
|
+
function description(root, company, name) {
|
|
5
|
+
const base = `companies/${company}/${name}`;
|
|
6
|
+
const nonHidden = (child) => !isHidden(child);
|
|
7
|
+
switch (name) {
|
|
8
|
+
case "data": return `${countImmediate(root, base, nonHidden)} item(s)`;
|
|
9
|
+
case "knowledge": return exists(root, `${base}/.git`) ? "Embedded git repo" : `Inline knowledge (${countImmediate(root, base, (n, f) => !isHidden(n) && fs.statSync(f).isDirectory())} subdirs)`;
|
|
10
|
+
case "settings": return `${countImmediate(root, base, (n, f) => !isHidden(n) && fs.statSync(f).isDirectory())} integration(s)`;
|
|
11
|
+
case "policies": return `${countImmediate(root, base, (n) => n.endsWith(".md"))} policy file(s)`;
|
|
12
|
+
case "projects": return `${countImmediate(root, base, (n, f) => !n.startsWith("_") && !isHidden(n) && fs.statSync(f).isDirectory())} project(s)`;
|
|
13
|
+
case "workers": return `${countImmediate(root, base, (n, f) => !isHidden(n) && fs.statSync(f).isDirectory())} worker(s)`;
|
|
14
|
+
case "repos": return `${countImmediate(root, base, (n, f) => { const s = fs.lstatSync(f); return !isHidden(n) && (s.isDirectory() || s.isSymbolicLink()); })} repo link(s)`;
|
|
15
|
+
case "registry": return `Resource registry (${countImmediate(root, `${base}/resources`, (n) => n.endsWith(".yaml"))} resource(s))`;
|
|
16
|
+
case "scripts": return `${countImmediate(root, base, (_n, f) => fs.statSync(f).isFile())} script(s)`;
|
|
17
|
+
default: return "—";
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export function renderCompanies(context) {
|
|
21
|
+
const written = [];
|
|
22
|
+
for (const directory of immediateEntries(context.root, "companies", "dir")) {
|
|
23
|
+
const company = basename(directory);
|
|
24
|
+
if (company.startsWith("_") || isHidden(company))
|
|
25
|
+
continue;
|
|
26
|
+
const lines = [`# ${titleize(company)}`, "", `> Auto-generated. Updated: ${date(context.now)}`, "", "| Name | Description |", "|------|-------------|"];
|
|
27
|
+
for (const name of ["data", "knowledge", "policies", "projects", "registry", "repos", "scripts", "settings", "workers"]) {
|
|
28
|
+
if (exists(context.root, `companies/${company}/${name}`))
|
|
29
|
+
lines.push(`| \`${name}/\` | ${sanitize(description(context.root, company, name))} |`);
|
|
30
|
+
}
|
|
31
|
+
lines.push("", "## Projects", "");
|
|
32
|
+
const projects = `companies/${company}/projects`;
|
|
33
|
+
if (exists(context.root, projects)) {
|
|
34
|
+
lines.push("| Project | Status | Description |", "|---------|--------|-------------|");
|
|
35
|
+
for (const projectDirectory of immediateEntries(context.root, projects, "dir")) {
|
|
36
|
+
const project = basename(projectDirectory);
|
|
37
|
+
if (project.startsWith("_") || isHidden(project))
|
|
38
|
+
continue;
|
|
39
|
+
const prd = path.join(projectDirectory, "prd.json");
|
|
40
|
+
const parsed = readJson(prd);
|
|
41
|
+
let text = typeof parsed?.description === "string" ? parsed.description : "";
|
|
42
|
+
text = truncate(sanitize(text), 110) || "—";
|
|
43
|
+
lines.push(`| \`${project}\` | ${projectStatus(context.root, project, prd, "active")} | ${text} |`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
else
|
|
47
|
+
lines.push("_No projects directory._");
|
|
48
|
+
lines.push("", "## Deployments", "", "_See `companies/manifest.yaml` for deployment targets._", "");
|
|
49
|
+
const output = `companies/${company}/INDEX.md`;
|
|
50
|
+
write(context, output, lines.join("\n"));
|
|
51
|
+
written.push(output);
|
|
52
|
+
log(context, `rebuild-companies-index: wrote ${output}`);
|
|
53
|
+
}
|
|
54
|
+
log(context, `rebuild-companies-index: regenerated ${written.length} company INDEX.md file(s)`);
|
|
55
|
+
return { written };
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=companies.js.map
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { basename, date, immediateEntries, isHidden, log, readJson, readText, sanitize, titleize, truncate, write } from "./shared.js";
|
|
2
|
+
function describe(item) {
|
|
3
|
+
const name = basename(item);
|
|
4
|
+
// `find -L` follows a symlink only for its direct target. fs.statSync mirrors that.
|
|
5
|
+
try {
|
|
6
|
+
if ((awaitStat(item)).isDirectory()) {
|
|
7
|
+
const fs = requireFs();
|
|
8
|
+
return `${fs.readdirSync(item).filter((entry) => !entry.startsWith(".")).length} item(s)`;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return name;
|
|
13
|
+
}
|
|
14
|
+
if (name.endsWith(".md"))
|
|
15
|
+
return readText(item)?.match(/^# +(.+)$/m)?.[1] || name;
|
|
16
|
+
if (name.endsWith(".yaml") || name.endsWith(".yml")) {
|
|
17
|
+
const found = readText(item)?.match(/^description:\s*(.*)$/m)?.[1]?.replace(/^["']|["']$/g, "");
|
|
18
|
+
return found || name;
|
|
19
|
+
}
|
|
20
|
+
if (name.endsWith(".json")) {
|
|
21
|
+
const parsed = readJson(item);
|
|
22
|
+
return typeof parsed?.description === "string" ? parsed.description : typeof parsed?.name === "string" ? parsed.name : name;
|
|
23
|
+
}
|
|
24
|
+
return name;
|
|
25
|
+
}
|
|
26
|
+
// Avoid a heavier tree abstraction here: the source script's `find -L` is
|
|
27
|
+
// specifically about knowledge gitlinks, so stat is intentionally local.
|
|
28
|
+
function requireFs() { return fsModule; }
|
|
29
|
+
import * as fsModule from "fs";
|
|
30
|
+
function awaitStat(file) { return fsModule.statSync(file); }
|
|
31
|
+
export function renderCompanyKnowledge(context) {
|
|
32
|
+
const written = [];
|
|
33
|
+
for (const companyDir of immediateEntries(context.root, "companies", "dir")) {
|
|
34
|
+
const company = basename(companyDir);
|
|
35
|
+
if (company.startsWith("_") || isHidden(company))
|
|
36
|
+
continue;
|
|
37
|
+
const relative = `companies/${company}/knowledge`;
|
|
38
|
+
const knowledge = `${companyDir}/knowledge`;
|
|
39
|
+
try {
|
|
40
|
+
if (!fsModule.statSync(knowledge).isDirectory())
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
const entries = fsModule.readdirSync(knowledge).flatMap((name) => {
|
|
47
|
+
const item = `${knowledge}/${name}`;
|
|
48
|
+
try {
|
|
49
|
+
return fsModule.statSync(item).isDirectory() ? [item] : [];
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return [];
|
|
53
|
+
}
|
|
54
|
+
}).sort().concat(fsModule.readdirSync(knowledge).flatMap((name) => {
|
|
55
|
+
const item = `${knowledge}/${name}`;
|
|
56
|
+
try {
|
|
57
|
+
return fsModule.statSync(item).isFile() ? [item] : [];
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return [];
|
|
61
|
+
}
|
|
62
|
+
}).sort());
|
|
63
|
+
const lines = [`# ${titleize(company)} Knowledge`, "", `> Auto-generated. Updated: ${date(context.now)}`, "", "| Name | Description |", "|------|-------------|"];
|
|
64
|
+
for (const item of entries) {
|
|
65
|
+
const name = basename(item);
|
|
66
|
+
if (isHidden(name) || name === "INDEX.md")
|
|
67
|
+
continue;
|
|
68
|
+
const value = truncate(sanitize(describe(item)), 100) || "—";
|
|
69
|
+
let directory = false;
|
|
70
|
+
try {
|
|
71
|
+
directory = fsModule.statSync(item).isDirectory();
|
|
72
|
+
}
|
|
73
|
+
catch { /* omitted */ }
|
|
74
|
+
lines.push(`| \`${name}${directory ? "/" : ""}\` | ${value} |`);
|
|
75
|
+
}
|
|
76
|
+
lines.push("");
|
|
77
|
+
const output = `${relative}/INDEX.md`;
|
|
78
|
+
write(context, output, lines.join("\n"));
|
|
79
|
+
written.push(output);
|
|
80
|
+
log(context, `rebuild-company-knowledge-index: wrote ${output}`);
|
|
81
|
+
}
|
|
82
|
+
log(context, `rebuild-company-knowledge-index: regenerated ${written.length} knowledge INDEX.md file(s)`);
|
|
83
|
+
return { written };
|
|
84
|
+
}
|
|
85
|
+
//# sourceMappingURL=company-knowledge.js.map
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { RenderContext, RenderResult } from "./shared.js";
|
|
2
|
+
export type IndexTarget = "all" | "companies" | "company-knowledge" | "orchestrator" | "projects" | "public-knowledge" | "reports" | "social-drafts" | "threads" | "workers";
|
|
3
|
+
export declare function renderIndexTarget(target: IndexTarget, context: RenderContext, args?: string[]): RenderResult;
|
|
4
|
+
export { atomicWrite, comparePath, immediateEntries, type RenderContext, type RenderResult } from "./shared.js";
|
|
5
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { renderCompanies } from "./companies.js";
|
|
2
|
+
import { renderCompanyKnowledge } from "./company-knowledge.js";
|
|
3
|
+
import { renderOrchestrator } from "./orchestrator.js";
|
|
4
|
+
import { renderProjects } from "./projects.js";
|
|
5
|
+
import { renderPublicKnowledge } from "./public-knowledge.js";
|
|
6
|
+
import { renderReports } from "./reports.js";
|
|
7
|
+
import { renderSocialDrafts } from "./social-drafts.js";
|
|
8
|
+
import { renderThreads } from "./threads.js";
|
|
9
|
+
import { renderWorkers } from "./workers.js";
|
|
10
|
+
const allOrder = ["threads", "orchestrator", "companies", "projects", "company-knowledge", "public-knowledge", "workers", "reports", "social-drafts"];
|
|
11
|
+
export function renderIndexTarget(target, context, args = []) {
|
|
12
|
+
switch (target) {
|
|
13
|
+
case "companies": return renderCompanies(context);
|
|
14
|
+
case "company-knowledge": return renderCompanyKnowledge(context);
|
|
15
|
+
case "orchestrator": return renderOrchestrator(context);
|
|
16
|
+
case "projects": return renderProjects(context);
|
|
17
|
+
case "public-knowledge": return renderPublicKnowledge(context);
|
|
18
|
+
case "reports": return renderReports(context);
|
|
19
|
+
case "social-drafts": return renderSocialDrafts(context);
|
|
20
|
+
case "threads": return renderThreads(context, args);
|
|
21
|
+
case "workers": return renderWorkers(context);
|
|
22
|
+
case "all": {
|
|
23
|
+
const written = [];
|
|
24
|
+
let errors = 0;
|
|
25
|
+
for (const child of allOrder) {
|
|
26
|
+
try {
|
|
27
|
+
written.push(...renderIndexTarget(child, context).written);
|
|
28
|
+
}
|
|
29
|
+
catch (_error) {
|
|
30
|
+
errors += 1;
|
|
31
|
+
context.log?.(`rebuild-all-indexes: ERROR in rebuild-${child}-index.sh`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
// The shell sorts and deduplicates the `wrote` paths before JSON output.
|
|
35
|
+
const stdoutPaths = written.filter((file) => /^(workspace|companies|knowledge|workers|projects)/.test(file));
|
|
36
|
+
(context.output ?? ((message) => process.stdout.write(message)))(`${JSON.stringify([...new Set(stdoutPaths)].sort(), null, 2)}\n`);
|
|
37
|
+
context.log?.(errors ? `all-indexes: errors (${errors})` : "all-indexes: ok");
|
|
38
|
+
return { written };
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
export { atomicWrite, comparePath, immediateEntries } from "./shared.js";
|
|
43
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import { at, log, readJson, timestamp, write } from "./shared.js";
|
|
3
|
+
function stateFiles(root) {
|
|
4
|
+
const base = at(root, "workspace/orchestrator");
|
|
5
|
+
try {
|
|
6
|
+
// The source intentionally does not sort this `find`; opendir's iterator
|
|
7
|
+
// preserves the directory stream order rather than readdirSync's sorting.
|
|
8
|
+
const stream = fs.opendirSync(base);
|
|
9
|
+
const entries = [];
|
|
10
|
+
try {
|
|
11
|
+
for (let entry = stream.readSync(); entry; entry = stream.readSync()) {
|
|
12
|
+
if (entry.name === "_archive" || entry.name === "_pipeline")
|
|
13
|
+
continue;
|
|
14
|
+
const state = `${base}/${entry.name}/state.json`;
|
|
15
|
+
if (fs.existsSync(state))
|
|
16
|
+
entries.push(state);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
finally {
|
|
20
|
+
stream.closeSync();
|
|
21
|
+
}
|
|
22
|
+
return entries;
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return [];
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function scalar(value, fallback) { return typeof value === "string" ? value : fallback; }
|
|
29
|
+
export function renderOrchestrator(context) {
|
|
30
|
+
fs.mkdirSync(at(context.root, "workspace/orchestrator"), { recursive: true });
|
|
31
|
+
const rows = [];
|
|
32
|
+
for (const file of stateFiles(context.root)) {
|
|
33
|
+
const state = readJson(file);
|
|
34
|
+
if (!state) {
|
|
35
|
+
log(context, `rebuild-orchestrator-index: WARNING: skipped unparseable ${file}: invalid JSON`);
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (["completed", "complete"].includes(scalar(state.status, "")) || scalar(state.phase, "") === "complete")
|
|
39
|
+
continue;
|
|
40
|
+
const source = scalar(state.prd_path, scalar(state.scope, ""));
|
|
41
|
+
const company = source.match(/companies\/([^/]+)\//)?.[1] ?? "-";
|
|
42
|
+
const progress = state.progress && typeof state.progress === "object" && !Array.isArray(state.progress)
|
|
43
|
+
? `${typeof state.progress.completed === "number" ? state.progress.completed : 0}/${typeof state.progress.total === "number" ? state.progress.total : 0}` : "-";
|
|
44
|
+
rows.push(`| ${scalar(state.project, scalar(state.run_id, "-"))} | ${company} | ${scalar(state.status, "-")} | ${progress} | ${scalar(state.updated_at, "-")} |`);
|
|
45
|
+
}
|
|
46
|
+
const output = "workspace/orchestrator/INDEX.md";
|
|
47
|
+
const text = ["# Orchestrator Projects", "", `Generated: ${timestamp(context.now)}`, "", "Active runs only. Archived runs in `workspace/orchestrator/_archive/`.", "", "| Project | Company | Status | Progress | Last Updated |", "|---------|---------|--------|----------|--------------|", ...rows, ""].join("\n");
|
|
48
|
+
write(context, output, text);
|
|
49
|
+
log(context, `rebuild-orchestrator-index: wrote ${output} (${rows.length} active)`);
|
|
50
|
+
return { written: [output] };
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=orchestrator.js.map
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import * as path from "path";
|
|
2
|
+
import { basename, date, exists, immediateEntries, isHidden, log, projectStatus, readJson, sanitize, truncate, write } from "./shared.js";
|
|
3
|
+
export function renderProjects(context) {
|
|
4
|
+
const base = "personal/projects";
|
|
5
|
+
if (!exists(context.root, base)) {
|
|
6
|
+
log(context, "rebuild-projects-index: personal/projects/ missing, skipping");
|
|
7
|
+
return { written: [] };
|
|
8
|
+
}
|
|
9
|
+
const lines = ["# Projects", "", `> Auto-generated. Updated: ${date(context.now)}`, "", "Personal/HQ projects only. Company-scoped projects live in `companies/{co}/projects/`.", "", "## Company Project Directories", "", "| Company | Path |", "|---------|------|"];
|
|
10
|
+
for (const companyDirectory of immediateEntries(context.root, "companies", "dir")) {
|
|
11
|
+
const company = basename(companyDirectory);
|
|
12
|
+
if (!company.startsWith("_") && !isHidden(company) && exists(context.root, `companies/${company}/projects`))
|
|
13
|
+
lines.push(`| ${company} | \`companies/${company}/projects/\` |`);
|
|
14
|
+
}
|
|
15
|
+
lines.push("", "## Personal/HQ Projects", "", "Projects here are HQ infrastructure or cross-company tools.", "", "| Project | Stories | Status | Description |", "|---------|---------|--------|-------------|");
|
|
16
|
+
const projects = immediateEntries(context.root, base, "dir");
|
|
17
|
+
for (const directory of projects) {
|
|
18
|
+
const name = basename(directory);
|
|
19
|
+
if (name.startsWith("_") || isHidden(name))
|
|
20
|
+
continue;
|
|
21
|
+
const prd = path.join(directory, "prd.json");
|
|
22
|
+
const parsed = readJson(prd);
|
|
23
|
+
const stories = Array.isArray(parsed?.userStories) && parsed.userStories.length > 0 ? String(parsed.userStories.length) : "—";
|
|
24
|
+
const description = truncate(sanitize(typeof parsed?.description === "string" ? parsed.description : ""), 110) || "—";
|
|
25
|
+
lines.push(`| \`${name}/\` | ${stories} | ${projectStatus(context.root, name, prd, "—")} | ${description} |`);
|
|
26
|
+
}
|
|
27
|
+
lines.push("");
|
|
28
|
+
const output = `${base}/INDEX.md`;
|
|
29
|
+
write(context, output, lines.join("\n"));
|
|
30
|
+
log(context, `rebuild-projects-index: wrote ${output} (${projects.length} HQ project(s))`);
|
|
31
|
+
return { written: [output] };
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=projects.js.map
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { basename, countImmediate, date, exists, heading, immediateEntries, isHidden, log, sanitize, truncate, write } from "./shared.js";
|
|
2
|
+
export function renderPublicKnowledge(context) {
|
|
3
|
+
const base = "core/knowledge/public";
|
|
4
|
+
if (!exists(context.root, base)) {
|
|
5
|
+
log(context, "rebuild-public-knowledge-index: core/knowledge/public/ missing, skipping");
|
|
6
|
+
return { written: [] };
|
|
7
|
+
}
|
|
8
|
+
const directories = immediateEntries(context.root, base, "dir");
|
|
9
|
+
const lines = ["# Public Knowledge", "", `> Auto-generated. Updated: ${date(context.now)}`, "", "| Name | Description |", "|------|-------------|"];
|
|
10
|
+
for (const directory of directories) {
|
|
11
|
+
const name = basename(directory);
|
|
12
|
+
if (isHidden(name))
|
|
13
|
+
continue;
|
|
14
|
+
const relative = `${base}/${name}`;
|
|
15
|
+
const count = countImmediate(context.root, relative, (child) => child.endsWith(".md") && child !== "INDEX.md");
|
|
16
|
+
let value = heading(`${directory}/README.md`);
|
|
17
|
+
if (!value) {
|
|
18
|
+
const first = immediateEntries(context.root, relative, "file").filter((file) => file.endsWith(".md") && !file.endsWith("/INDEX.md") && !file.endsWith("/README.md"))[0];
|
|
19
|
+
if (first)
|
|
20
|
+
value = heading(first);
|
|
21
|
+
}
|
|
22
|
+
value = value ? `${value} (${count} file(s))` : `${count} file(s)`;
|
|
23
|
+
lines.push(`| \`${name}/\` | ${truncate(sanitize(value), 100)} |`);
|
|
24
|
+
}
|
|
25
|
+
for (const file of immediateEntries(context.root, base, "file").filter((item) => item.endsWith(".md"))) {
|
|
26
|
+
const name = basename(file);
|
|
27
|
+
if (name === "INDEX.md" || isHidden(name))
|
|
28
|
+
continue;
|
|
29
|
+
lines.push(`| \`${name}\` | ${truncate(sanitize(heading(file) || name), 100)} |`);
|
|
30
|
+
}
|
|
31
|
+
lines.push("");
|
|
32
|
+
const output = `${base}/INDEX.md`;
|
|
33
|
+
write(context, output, lines.join("\n"));
|
|
34
|
+
log(context, `rebuild-public-knowledge-index: wrote ${output} (${directories.length} subdir(s))`);
|
|
35
|
+
return { written: [output] };
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=public-knowledge.js.map
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { basename, countImmediate, date, exists, heading, immediateEntries, isHidden, log, readText, sanitize, truncate, write } from "./shared.js";
|
|
2
|
+
function reportDescription(file) {
|
|
3
|
+
const name = basename(file);
|
|
4
|
+
if (name.endsWith(".md"))
|
|
5
|
+
return heading(file) || name.slice(0, -3);
|
|
6
|
+
if (/\.html?$/.test(name))
|
|
7
|
+
return readText(file)?.match(/<title>(.*?)<\/title>/i)?.[1] || name.replace(/\.[^.]+$/, "");
|
|
8
|
+
return name;
|
|
9
|
+
}
|
|
10
|
+
function reportDate(name) { return name.match(/^(\d{4}-(?:\d{2}-\d{2}|W\d{2}))/)?.[1] ?? "—"; }
|
|
11
|
+
export function renderReports(context) {
|
|
12
|
+
const base = "workspace/reports";
|
|
13
|
+
if (!exists(context.root, base)) {
|
|
14
|
+
log(context, "rebuild-reports-index: workspace/reports/ missing, skipping");
|
|
15
|
+
return { written: [] };
|
|
16
|
+
}
|
|
17
|
+
const lines = ["# Reports", "", `> Auto-generated. Updated: ${date(context.now)}`, "", "Personal/HQ reports only. Company reports moved to `companies/{co}/data/reports/`.", "", "## Directories", "", "| Name | Description |", "|------|-------------|"];
|
|
18
|
+
for (const directory of immediateEntries(context.root, base, "dir")) {
|
|
19
|
+
const name = basename(directory);
|
|
20
|
+
if (!isHidden(name))
|
|
21
|
+
lines.push(`| \`${name}/\` | ${countImmediate(context.root, `${base}/${name}`, (child) => !isHidden(child))} item(s) |`);
|
|
22
|
+
}
|
|
23
|
+
lines.push("", "## Individual Reports", "", "| Name | Date | Description |", "|------|------|-------------|");
|
|
24
|
+
const files = immediateEntries(context.root, base, "file");
|
|
25
|
+
for (const file of files) {
|
|
26
|
+
const name = basename(file);
|
|
27
|
+
if (name === "INDEX.md" || isHidden(name))
|
|
28
|
+
continue;
|
|
29
|
+
lines.push(`| \`${name}\` | ${reportDate(name)} | ${truncate(sanitize(reportDescription(file)), 100) || "—"} |`);
|
|
30
|
+
}
|
|
31
|
+
lines.push("");
|
|
32
|
+
const output = `${base}/INDEX.md`;
|
|
33
|
+
write(context, output, lines.join("\n"));
|
|
34
|
+
log(context, `rebuild-reports-index: wrote ${output} (${files.filter((file) => basename(file) !== "INDEX.md").length} individual report(s))`);
|
|
35
|
+
return { written: [output] };
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=reports.js.map
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export type RenderContext = {
|
|
2
|
+
root: string;
|
|
3
|
+
now?: Date;
|
|
4
|
+
log?: (message: string) => void;
|
|
5
|
+
output?: (message: string) => void;
|
|
6
|
+
};
|
|
7
|
+
export type RenderResult = {
|
|
8
|
+
written: string[];
|
|
9
|
+
};
|
|
10
|
+
export declare function rel(root: string, file: string): string;
|
|
11
|
+
export declare function at(root: string, relative: string): string;
|
|
12
|
+
export declare function exists(root: string, relative: string): boolean;
|
|
13
|
+
/** Bytewise, locale-independent ordering matching `find … | sort`. */
|
|
14
|
+
export declare function comparePath(a: string, b: string): number;
|
|
15
|
+
export declare function immediateEntries(root: string, relative: string, kind: "dir" | "file", followSymlinks?: boolean): string[];
|
|
16
|
+
export declare function countImmediate(root: string, relative: string, predicate: (name: string, file: string) => boolean): number;
|
|
17
|
+
export declare function readText(file: string): string | undefined;
|
|
18
|
+
export declare function readJson(file: string): Record<string, unknown> | undefined;
|
|
19
|
+
export declare function heading(file: string): string;
|
|
20
|
+
export declare function sanitize(value: unknown): string;
|
|
21
|
+
/** Bash's character-count truncation; filenames and fixture text are UTF-8. */
|
|
22
|
+
export declare function truncate(value: string, length: number): string;
|
|
23
|
+
export declare function titleize(slug: string): string;
|
|
24
|
+
export declare function date(now?: Date): string;
|
|
25
|
+
export declare function timestamp(now?: Date): string;
|
|
26
|
+
/** Write via a same-directory temporary file, then atomically replace the destination. */
|
|
27
|
+
export declare function atomicWrite(file: string, content: string): void;
|
|
28
|
+
export declare function write(context: RenderContext, relative: string, content: string): string;
|
|
29
|
+
export declare function log(context: RenderContext, message: string): void;
|
|
30
|
+
export declare function projectStatus(root: string, project: string, prdPath: string, fallback: string): string;
|
|
31
|
+
export declare function basename(file: string): string;
|
|
32
|
+
export declare function isHidden(name: string): boolean;
|
|
33
|
+
export declare function mtime(file: string): number;
|
|
34
|
+
export declare function tempDirectory(prefix: string): string;
|
|
35
|
+
//# sourceMappingURL=shared.d.ts.map
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as os from "os";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
export function rel(root, file) {
|
|
5
|
+
return path.relative(root, file).split(path.sep).join("/");
|
|
6
|
+
}
|
|
7
|
+
export function at(root, relative) {
|
|
8
|
+
return path.join(root, ...relative.split("/"));
|
|
9
|
+
}
|
|
10
|
+
export function exists(root, relative) {
|
|
11
|
+
return fs.existsSync(at(root, relative));
|
|
12
|
+
}
|
|
13
|
+
/** Bytewise, locale-independent ordering matching `find … | sort`. */
|
|
14
|
+
export function comparePath(a, b) {
|
|
15
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
16
|
+
}
|
|
17
|
+
export function immediateEntries(root, relative, kind, followSymlinks = false) {
|
|
18
|
+
const base = at(root, relative);
|
|
19
|
+
try {
|
|
20
|
+
return fs.readdirSync(base).flatMap((name) => {
|
|
21
|
+
const candidate = path.join(base, name);
|
|
22
|
+
try {
|
|
23
|
+
const stat = followSymlinks ? fs.statSync(candidate) : fs.lstatSync(candidate);
|
|
24
|
+
return (kind === "dir" ? stat.isDirectory() : stat.isFile()) ? [candidate] : [];
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return [];
|
|
28
|
+
}
|
|
29
|
+
}).sort(comparePath);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return [];
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export function countImmediate(root, relative, predicate) {
|
|
36
|
+
try {
|
|
37
|
+
return fs.readdirSync(at(root, relative)).filter((name) => predicate(name, at(root, `${relative}/${name}`))).length;
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return 0;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export function readText(file) {
|
|
44
|
+
try {
|
|
45
|
+
return fs.readFileSync(file, "utf8");
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
export function readJson(file) {
|
|
52
|
+
const text = readText(file);
|
|
53
|
+
if (text === undefined)
|
|
54
|
+
return undefined;
|
|
55
|
+
try {
|
|
56
|
+
const parsed = JSON.parse(text);
|
|
57
|
+
return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)
|
|
58
|
+
? parsed : undefined;
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
export function heading(file) {
|
|
65
|
+
return readText(file)?.match(/^# +(.+)$/m)?.[1] ?? "";
|
|
66
|
+
}
|
|
67
|
+
export function sanitize(value) {
|
|
68
|
+
return String(value ?? "").replace(/\n/g, " ").replace(/\|/g, "\\|").replace(/ */g, " ").trim();
|
|
69
|
+
}
|
|
70
|
+
/** Bash's character-count truncation; filenames and fixture text are UTF-8. */
|
|
71
|
+
export function truncate(value, length) {
|
|
72
|
+
return value.length <= length ? value : `${value.slice(0, length - 1)}…`;
|
|
73
|
+
}
|
|
74
|
+
export function titleize(slug) {
|
|
75
|
+
return slug.split("-").map((part) => part ? `${part[0].toUpperCase()}${part.slice(1)}` : "").join(" ");
|
|
76
|
+
}
|
|
77
|
+
export function date(now = new Date()) { return now.toISOString().slice(0, 10); }
|
|
78
|
+
export function timestamp(now = new Date()) { return now.toISOString().replace(/\.\d{3}Z$/, "Z"); }
|
|
79
|
+
/** Write via a same-directory temporary file, then atomically replace the destination. */
|
|
80
|
+
export function atomicWrite(file, content) {
|
|
81
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
82
|
+
const temporary = path.join(path.dirname(file), `.${path.basename(file)}.${process.pid}.${Math.random().toString(16).slice(2)}.tmp`);
|
|
83
|
+
try {
|
|
84
|
+
fs.writeFileSync(temporary, content, "utf8");
|
|
85
|
+
fs.renameSync(temporary, file);
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
try {
|
|
89
|
+
fs.unlinkSync(temporary);
|
|
90
|
+
}
|
|
91
|
+
catch { /* best-effort cleanup */ }
|
|
92
|
+
throw error;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
export function write(context, relative, content) {
|
|
96
|
+
atomicWrite(at(context.root, relative), content);
|
|
97
|
+
return relative;
|
|
98
|
+
}
|
|
99
|
+
export function log(context, message) { context.log?.(message); }
|
|
100
|
+
export function projectStatus(root, project, prdPath, fallback) {
|
|
101
|
+
const state = readJson(at(root, `workspace/orchestrator/${project}/state.json`));
|
|
102
|
+
if (state) {
|
|
103
|
+
const raw = typeof state.status === "string" ? state.status : "active";
|
|
104
|
+
if (["completed", "complete"].includes(raw))
|
|
105
|
+
return "complete";
|
|
106
|
+
if (["in_progress", "active", "started"].includes(raw))
|
|
107
|
+
return "active";
|
|
108
|
+
return raw;
|
|
109
|
+
}
|
|
110
|
+
const prd = readJson(prdPath);
|
|
111
|
+
if (prd && Array.isArray(prd.userStories) && prd.userStories.length > 0 && prd.userStories.every((story) => Boolean(story.passes)))
|
|
112
|
+
return "complete";
|
|
113
|
+
return fallback;
|
|
114
|
+
}
|
|
115
|
+
export function basename(file) { return path.basename(file); }
|
|
116
|
+
export function isHidden(name) { return name.startsWith("."); }
|
|
117
|
+
export function mtime(file) { try {
|
|
118
|
+
return fs.statSync(file).mtimeMs / 1000;
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
return 0;
|
|
122
|
+
} }
|
|
123
|
+
// Kept exported for primitive tests and consumers that need a temp base without
|
|
124
|
+
// relying on an application-specific fixture location.
|
|
125
|
+
export function tempDirectory(prefix) { return fs.mkdtempSync(path.join(os.tmpdir(), prefix)); }
|
|
126
|
+
//# sourceMappingURL=shared.js.map
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import { basename, countImmediate, date, exists, heading, immediateEntries, isHidden, log, mtime, sanitize, truncate, write } from "./shared.js";
|
|
3
|
+
function directoryDescription(context, name) {
|
|
4
|
+
const base = `workspace/social-drafts/${name}`;
|
|
5
|
+
const files = countImmediate(context.root, base, (child, file) => !isHidden(child) && fs.statSync(file).isFile());
|
|
6
|
+
const dirs = countImmediate(context.root, base, (child, file) => !isHidden(child) && fs.statSync(file).isDirectory());
|
|
7
|
+
if (name === "x")
|
|
8
|
+
return `X/Twitter post drafts (${files} files)`;
|
|
9
|
+
if (name === "linkedin")
|
|
10
|
+
return `LinkedIn post drafts (${files} files)`;
|
|
11
|
+
if (name === "blog")
|
|
12
|
+
return `Blog post drafts (${files} files)`;
|
|
13
|
+
if (name === "tiktok")
|
|
14
|
+
return `TikTok scripts/drafts (${files} files)`;
|
|
15
|
+
if (name === "images")
|
|
16
|
+
return `Infographics and post images (${dirs} dirs)`;
|
|
17
|
+
return dirs > 0 ? `${dirs} subdir(s), ${files} file(s)` : `${files} file(s)`;
|
|
18
|
+
}
|
|
19
|
+
function fileDescription(file) { const name = basename(file); if (name.endsWith(".md"))
|
|
20
|
+
return heading(file) || name.slice(0, -3); if (name.endsWith(".json"))
|
|
21
|
+
return `${name.slice(0, -5)} data`; if (/\.(py|sh)$/.test(name))
|
|
22
|
+
return `Script: ${name}`; return name; }
|
|
23
|
+
export function renderSocialDrafts(context) {
|
|
24
|
+
const base = "workspace/social-drafts";
|
|
25
|
+
if (!exists(context.root, base)) {
|
|
26
|
+
log(context, "rebuild-social-drafts-index: workspace/social-drafts/ missing, skipping");
|
|
27
|
+
return { written: [] };
|
|
28
|
+
}
|
|
29
|
+
const recent = ["x", "linkedin", "blog", "tiktok"].flatMap((channel) => immediateEntries(context.root, `${base}/${channel}`, "file").filter((file) => file.endsWith(".md"))).sort((a, b) => mtime(b) - mtime(a)).slice(0, 20);
|
|
30
|
+
const lines = ["# Social Drafts", "", `> Auto-generated. Updated: ${date(context.now)}`, "", "## Recent Drafts", ""];
|
|
31
|
+
for (const file of recent) {
|
|
32
|
+
const relative = file.slice(`${context.root}/workspace/social-drafts/`.length);
|
|
33
|
+
lines.push(`- \`${relative}\` - ${truncate(sanitize(heading(file) || basename(file).slice(0, -3)), 100)}`);
|
|
34
|
+
}
|
|
35
|
+
lines.push("", "## Directories", "", "| Name | Description |", "|------|-------------|");
|
|
36
|
+
for (const directory of immediateEntries(context.root, base, "dir")) {
|
|
37
|
+
const name = basename(directory);
|
|
38
|
+
if (!isHidden(name))
|
|
39
|
+
lines.push(`| \`${name}/\` | ${truncate(sanitize(directoryDescription(context, name)), 100)} |`);
|
|
40
|
+
}
|
|
41
|
+
for (const file of immediateEntries(context.root, base, "file")) {
|
|
42
|
+
const name = basename(file);
|
|
43
|
+
if (name !== "INDEX.md" && !isHidden(name))
|
|
44
|
+
lines.push(`| \`${name}\` | ${truncate(sanitize(fileDescription(file)), 100)} |`);
|
|
45
|
+
}
|
|
46
|
+
lines.push("");
|
|
47
|
+
const output = `${base}/INDEX.md`;
|
|
48
|
+
write(context, output, lines.join("\n"));
|
|
49
|
+
const draftCount = ["x", "linkedin", "blog", "tiktok"].reduce((sum, channel) => sum + immediateEntries(context.root, `${base}/${channel}`, "file").filter((file) => file.endsWith(".md")).length, 0);
|
|
50
|
+
log(context, `rebuild-social-drafts-index: wrote ${output} (${draftCount} draft file(s))`);
|
|
51
|
+
return { written: [output] };
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=social-drafts.js.map
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import { at, log, readJson, sanitize, timestamp, write } from "./shared.js";
|
|
3
|
+
export function renderThreads(context, args = []) {
|
|
4
|
+
const mode = args[0] ?? "--index";
|
|
5
|
+
const directory = at(context.root, "workspace/threads");
|
|
6
|
+
fs.mkdirSync(directory, { recursive: true });
|
|
7
|
+
// Match `ls -t`: newest filesystem modification time first. `updated_at` is
|
|
8
|
+
// rendered metadata only, and must not influence the order.
|
|
9
|
+
const files = fs.readdirSync(directory)
|
|
10
|
+
.filter((name) => /^T-.*\.json$/.test(name) && !name.endsWith(".changeset.json"))
|
|
11
|
+
.map((name) => `${directory}/${name}`)
|
|
12
|
+
.sort((a, b) => {
|
|
13
|
+
const aTime = fs.statSync(a, { bigint: true }).mtimeNs;
|
|
14
|
+
const bTime = fs.statSync(b, { bigint: true }).mtimeNs;
|
|
15
|
+
return bTime > aTime ? 1 : bTime < aTime ? -1 : 0;
|
|
16
|
+
});
|
|
17
|
+
const rows = files.flatMap((file) => {
|
|
18
|
+
const data = readJson(file);
|
|
19
|
+
if (!data)
|
|
20
|
+
return [];
|
|
21
|
+
const metadata = data.metadata && typeof data.metadata === "object" ? data.metadata : {};
|
|
22
|
+
return [`| ${typeof data.thread_id === "string" ? data.thread_id : "-"} | ${typeof data.type === "string" ? data.type : "-"} | ${typeof data.updated_at === "string" ? data.updated_at : "-"} | ${sanitize(typeof metadata.title === "string" ? metadata.title.replace(/\|/g, "/") : "-")} |`];
|
|
23
|
+
});
|
|
24
|
+
const written = [];
|
|
25
|
+
const table = (title, limited) => [
|
|
26
|
+
`# ${title}`, "", `Generated: ${timestamp(context.now)}`, `Active threads: ${files.length} (archive/ excluded)`, "", "| Thread | Type | Updated | Title |", "|--------|------|---------|-------|", ...(limited ? rows.slice(0, limited) : rows), "",
|
|
27
|
+
].join("\n");
|
|
28
|
+
if (mode === "--index" || mode === "--both") {
|
|
29
|
+
const output = "workspace/threads/INDEX.md";
|
|
30
|
+
write(context, output, table("Threads INDEX", 0));
|
|
31
|
+
written.push(output);
|
|
32
|
+
log(context, `rebuild-threads-index: wrote ${output} (${files.length} threads)`);
|
|
33
|
+
}
|
|
34
|
+
if (mode === "--recent" || mode === "--both") {
|
|
35
|
+
const output = "workspace/threads/recent.md";
|
|
36
|
+
write(context, output, table("Recent Threads", 15));
|
|
37
|
+
written.push(output);
|
|
38
|
+
log(context, `rebuild-threads-index: wrote ${output} (last 15)`);
|
|
39
|
+
}
|
|
40
|
+
return { written };
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=threads.js.map
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import * as yaml from "js-yaml";
|
|
2
|
+
import { basename, date, exists, immediateEntries, isHidden, log, readText, sanitize, truncate, write } from "./shared.js";
|
|
3
|
+
function workerDescription(file) {
|
|
4
|
+
const text = readText(file);
|
|
5
|
+
if (!text)
|
|
6
|
+
return "";
|
|
7
|
+
try {
|
|
8
|
+
const parsed = yaml.load(text);
|
|
9
|
+
const value = parsed?.worker?.description ?? parsed?.worker?.bio?.role;
|
|
10
|
+
return typeof value === "string" ? value : "";
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return text.match(/^\s+description:\s*(.*)$/m)?.[1]?.replace(/^["']|["']$/g, "") ?? "";
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function subindex(context, directory, label) {
|
|
17
|
+
if (!exists(context.root, directory))
|
|
18
|
+
return undefined;
|
|
19
|
+
const lines = [`# ${label}`, "", `> Auto-generated. Updated: ${date(context.now)}`, "", "| Name | Description |", "|------|-------------|"];
|
|
20
|
+
for (const entry of immediateEntries(context.root, directory, "dir")) {
|
|
21
|
+
const name = basename(entry);
|
|
22
|
+
if (!isHidden(name))
|
|
23
|
+
lines.push(`| \`${name}/\` | ${truncate(sanitize(workerDescription(`${entry}/worker.yaml`)), 100) || "—"} |`);
|
|
24
|
+
}
|
|
25
|
+
lines.push("");
|
|
26
|
+
const output = `${directory}/INDEX.md`;
|
|
27
|
+
write(context, output, lines.join("\n"));
|
|
28
|
+
log(context, `rebuild-workers-index: wrote ${output}`);
|
|
29
|
+
return output;
|
|
30
|
+
}
|
|
31
|
+
function rootIndex(context) {
|
|
32
|
+
const directory = "core/workers";
|
|
33
|
+
if (!exists(context.root, directory))
|
|
34
|
+
return undefined;
|
|
35
|
+
const lines = ["# Workers", "", `> Auto-generated. Updated: ${date(context.now)}`, "", "Shared workers live under `public/`. Private/local workers live under `personal/workers/`. `registry.yaml` is the auto-generated index (regenerated by core/scripts/generate-workers-registry.sh).", "", "| Name | Description |", "|------|-------------|"];
|
|
36
|
+
for (const entry of immediateEntries(context.root, directory, "dir")) {
|
|
37
|
+
const name = basename(entry);
|
|
38
|
+
if (isHidden(name))
|
|
39
|
+
continue;
|
|
40
|
+
lines.push(name === "public" ? "| `public/` | Shared public workers (see public/INDEX.md) |" : `| \`${name}/\` | ${truncate(sanitize(workerDescription(`${entry}/worker.yaml`)), 100) || "—"} |`);
|
|
41
|
+
}
|
|
42
|
+
lines.push("");
|
|
43
|
+
const output = `${directory}/INDEX.md`;
|
|
44
|
+
write(context, output, lines.join("\n"));
|
|
45
|
+
log(context, `rebuild-workers-index: wrote ${output}`);
|
|
46
|
+
return output;
|
|
47
|
+
}
|
|
48
|
+
export function renderWorkers(context) { const written = [subindex(context, "core/workers/public", "Public Workers"), subindex(context, "personal/workers", "Personal Workers"), rootIndex(context)].filter((value) => Boolean(value)); log(context, "rebuild-workers-index: done"); return { written }; }
|
|
49
|
+
//# sourceMappingURL=workers.js.map
|