@jskit-ai/jskit-cli 0.2.80 → 0.2.82

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 (32) hide show
  1. package/package.json +6 -4
  2. package/src/server/appBlueprint.js +1 -1
  3. package/src/server/commandHandlers/helperMap.js +104 -0
  4. package/src/server/commandHandlers/session.js +179 -4
  5. package/src/server/commandHandlers/show.js +169 -34
  6. package/src/server/core/argParser.js +20 -0
  7. package/src/server/core/commandCatalog.js +70 -7
  8. package/src/server/core/createCommandHandlers.js +4 -1
  9. package/src/server/helperMap.js +463 -0
  10. package/src/server/helperMapPaths.js +7 -0
  11. package/src/server/sessionRuntime/appReadiness.js +55 -0
  12. package/src/server/sessionRuntime/constants.js +298 -135
  13. package/src/server/sessionRuntime/paths.js +2 -4
  14. package/src/server/sessionRuntime/preconditions.js +393 -26
  15. package/src/server/sessionRuntime/promptRenderer.js +15 -2
  16. package/src/server/sessionRuntime/prompts/app_blueprint.md +26 -2
  17. package/src/server/sessionRuntime/prompts/automated_checks.md +42 -0
  18. package/src/server/sessionRuntime/prompts/deep_ui_check.md +53 -0
  19. package/src/server/sessionRuntime/prompts/doctor_failure.md +21 -1
  20. package/src/server/sessionRuntime/prompts/execute_plan.md +61 -0
  21. package/src/server/sessionRuntime/prompts/final_comment.md +3 -1
  22. package/src/server/sessionRuntime/prompts/issue_details.md +52 -0
  23. package/src/server/sessionRuntime/prompts/new_issue.md +34 -3
  24. package/src/server/sessionRuntime/prompts/plan_issue.md +81 -0
  25. package/src/server/sessionRuntime/prompts/pr_failure.md +14 -1
  26. package/src/server/sessionRuntime/prompts/review_changes.md +77 -15
  27. package/src/server/sessionRuntime/prompts/update_blueprint.md +36 -0
  28. package/src/server/sessionRuntime/prompts/user_check.md +22 -4
  29. package/src/server/sessionRuntime/responses.js +877 -30
  30. package/src/server/sessionRuntime/worktrees.js +31 -0
  31. package/src/server/sessionRuntime.js +2070 -244
  32. package/src/server/sessionRuntime/prompts/implement_issue.md +0 -25
@@ -9,7 +9,9 @@ const OPTION_FLAG_LABELS = Object.freeze({
9
9
  checkDiLabels: "--check-di-labels",
10
10
  verbose: "--verbose",
11
11
  json: "--json",
12
- all: "--all"
12
+ all: "--all",
13
+ abandoned: "--abandoned",
14
+ completed: "--completed"
13
15
  });
14
16
 
15
17
  const PARSED_BOOLEAN_OPTION_KEYS = Object.freeze(Object.keys(OPTION_FLAG_LABELS));
@@ -211,15 +213,27 @@ const COMMAND_DESCRIPTORS = Object.freeze({
211
213
  }),
212
214
  Object.freeze({
213
215
  name: "[step|abandon|adopt-codex-thread]",
214
- description: "Run the next step, abandon a session, or attach a Codex thread id."
216
+ description: "Run the next step, inspect a diff, abandon a session, or attach a Codex thread id."
215
217
  })
216
218
  ]),
217
219
  defaults: Object.freeze([
218
220
  "Active session state lives in .jskit/sessions/active/<session_id> under the current target app.",
219
- "Session worktrees live in .jskit/sessions/worktrees/<session_id>.",
221
+ "Session worktrees live in .jskit/sessions/active/<session_id>/worktree.",
220
222
  "The session id is timestamp-based and is the primary key.",
223
+ "Bare list output includes active sessions only; use --abandoned, --completed, or --all for archived sessions.",
221
224
  "Use --json for the stable machine-readable contract consumed by JSKIT AI Studio.",
222
- "Use --issue - to read approved issue text from stdin."
225
+ "Use --issue - to read approved issue body from stdin.",
226
+ "Use --issue-title when the approved issue title is separate from the body.",
227
+ "Use --issue-details - to read confirmed issue details from stdin.",
228
+ "Use --plan - to read the approved implementation plan from stdin.",
229
+ "Use --rework-notes - with --user-check failed to start the next plan cycle.",
230
+ "Use --agent-decisions - to append session-local decision log entries from implementation, UI review, verification, or repair phases.",
231
+ "Use --review-findings-remaining true --review-findings \"<findings>\" when an accepted review pass needs another pass.",
232
+ "Use --review-findings-remaining false only when the review/deslop loop is done.",
233
+ "Use --skip-ui-check --skip-reason \"<reason>\" only when uiImpact is possible and the Deep UI Check is intentionally skipped.",
234
+ "Use --merge-pr true at PR finalization to merge the pull request.",
235
+ "Use --close-without-merge --close-reason \"<reason>\" at PR finalization to complete the session without merging.",
236
+ "Run the blueprint step once to render its Codex prompt, then again after Codex updates .jskit/APP_BLUEPRINT.md."
223
237
  ]),
224
238
  examples: Object.freeze([
225
239
  Object.freeze({
@@ -228,15 +242,26 @@ const COMMAND_DESCRIPTORS = Object.freeze({
228
242
  "jskit session create",
229
243
  "jskit session 2026-05-11_21-42-08 step",
230
244
  "jskit session 2026-05-11_21-42-08 step --prompt \"Fix the customer filters\"",
231
- "jskit session 2026-05-11_21-42-08 step --issue -"
245
+ "jskit session 2026-05-11_21-42-08 step --issue-title \"Fix customer filters\" --issue -",
246
+ "jskit session 2026-05-11_21-42-08 step --issue-details -",
247
+ "jskit session 2026-05-11_21-42-08 step --plan -",
248
+ "jskit session 2026-05-11_21-42-08 step --agent-decisions -",
249
+ "jskit session 2026-05-11_21-42-08 step --review-findings-remaining true --review-findings \"A helper duplication fix needs another pass\"",
250
+ "jskit session 2026-05-11_21-42-08 step --review-findings-remaining false",
251
+ "jskit session 2026-05-11_21-42-08 step --skip-ui-check --skip-reason \"No user-facing UI changes\"",
252
+ "jskit session 2026-05-11_21-42-08 step",
253
+ "jskit session 2026-05-11_21-42-08 step --merge-pr true",
254
+ "jskit session 2026-05-11_21-42-08 step --close-without-merge --close-reason \"Prototype kept for reference\"",
255
+ "jskit session 2026-05-11_21-42-08 step --user-check failed --rework-notes -",
256
+ "jskit session 2026-05-11_21-42-08 diff --json"
232
257
  ])
233
258
  })
234
259
  ]),
235
260
  fullUse:
236
- "jskit session [create|<sessionId>] [step|abandon|adopt-codex-thread] [--prompt <text>] [--issue <text>|--issue-file <path>] [--user-check <passed|failed>] [--codex-thread-id <id>] [--json]",
261
+ "jskit session [create|<sessionId>] [step|diff|abandon|adopt-codex-thread] [--prompt <text>] [--issue-title <text>|--issue-title-file <path>] [--issue <text>|--issue-file <path>] [--issue-details <text>|--issue-details-file <path>] [--plan <text>|--plan-file <path>] [--agent-decisions <text>|--agent-decisions-file <path>] [--review-findings-remaining true --review-findings <text>|--review-findings-remaining false] [--skip-ui-check --skip-reason <text>] [--merge-pr true|--close-without-merge --close-reason <text>] [--user-check <passed|failed>] [--rework-notes <text>|--rework-notes-file <path>] [--codex-thread-id <id>] [--abandoned|--completed|--all] [--json]",
237
262
  showHelpOnBareInvocation: false,
238
263
  handlerName: "commandSession",
239
- allowedFlagKeys: Object.freeze(["json"]),
264
+ allowedFlagKeys: Object.freeze(["json", "abandoned", "completed", "all"]),
240
265
  inlineOptionMode: "delegate",
241
266
  allowedValueOptionNames: Object.freeze([]),
242
267
  canDelegateInlineOptions: (positional = []) => Array.isArray(positional) && positional.length > 0
@@ -281,6 +306,44 @@ const COMMAND_DESCRIPTORS = Object.freeze({
281
306
  return subcommand === "prompt" || subcommand === "set";
282
307
  }
283
308
  }),
309
+ "helper-map": Object.freeze({
310
+ command: "helper-map",
311
+ aliases: Object.freeze([]),
312
+ showInOverview: true,
313
+ summary: "Read or update the generated app helper map.",
314
+ minimalUse: "jskit helper-map update",
315
+ parameters: Object.freeze([
316
+ Object.freeze({
317
+ name: "[update]",
318
+ description: "Without a subcommand, prints the saved helper map. update refreshes .jskit/helper-map files."
319
+ })
320
+ ]),
321
+ defaults: Object.freeze([
322
+ "The helper map is generated app state, not a hand-maintained workflow file.",
323
+ "The JSON file lives at .jskit/helper-map.json and the readable map lives at .jskit/helper-map.md.",
324
+ "Use the map before adding helpers, composables, service functions, maps, or package glue.",
325
+ "Use --json for a stable machine-readable response."
326
+ ]),
327
+ examples: Object.freeze([
328
+ Object.freeze({
329
+ label: "Refresh helper map",
330
+ lines: Object.freeze([
331
+ "jskit helper-map update",
332
+ "jskit helper-map --json"
333
+ ])
334
+ })
335
+ ]),
336
+ fullUse: "jskit helper-map [update] [--json]",
337
+ showHelpOnBareInvocation: false,
338
+ handlerName: "commandHelperMap",
339
+ allowedFlagKeys: Object.freeze(["json"]),
340
+ inlineOptionMode: "delegate",
341
+ allowedValueOptionNames: Object.freeze([]),
342
+ canDelegateInlineOptions: (positional = []) => {
343
+ const subcommand = String(Array.isArray(positional) ? positional[0] || "" : "").trim();
344
+ return subcommand === "update";
345
+ }
346
+ }),
284
347
  add: Object.freeze({
285
348
  command: "add",
286
349
  aliases: Object.freeze([]),
@@ -8,6 +8,7 @@ import { createHealthCommands } from "../commandHandlers/health.js";
8
8
  import { createCompletionCommands } from "../commandHandlers/completion.js";
9
9
  import { createSessionCommands } from "../commandHandlers/session.js";
10
10
  import { createBlueprintCommands } from "../commandHandlers/blueprint.js";
11
+ import { createHelperMapCommands } from "../commandHandlers/helperMap.js";
11
12
 
12
13
  function createCommandHandlers(deps = {}) {
13
14
  const shared = createCommandHandlerShared(deps);
@@ -33,6 +34,7 @@ function createCommandHandlers(deps = {}) {
33
34
  const { commandCompletion } = createCompletionCommands(commandContext);
34
35
  const { commandSession } = createSessionCommands(commandContext);
35
36
  const { commandBlueprint } = createBlueprintCommands(commandContext);
37
+ const { commandHelperMap } = createHelperMapCommands(commandContext);
36
38
 
37
39
  return {
38
40
  commandList,
@@ -52,7 +54,8 @@ function createCommandHandlers(deps = {}) {
52
54
  commandDoctor,
53
55
  commandLintDescriptors,
54
56
  commandSession,
55
- commandBlueprint
57
+ commandBlueprint,
58
+ commandHelperMap
56
59
  };
57
60
  }
58
61
 
@@ -0,0 +1,463 @@
1
+ import {
2
+ mkdir,
3
+ readFile,
4
+ readdir,
5
+ stat,
6
+ writeFile
7
+ } from "node:fs/promises";
8
+ import path from "node:path";
9
+ import { compileScript, parse as parseVueSfc } from "@vue/compiler-sfc";
10
+ import tsMorph from "ts-morph";
11
+ import {
12
+ HELPER_MAP_JSON_RELATIVE_PATH,
13
+ HELPER_MAP_MARKDOWN_RELATIVE_PATH
14
+ } from "./helperMapPaths.js";
15
+
16
+ const {
17
+ ModuleKind,
18
+ ModuleResolutionKind,
19
+ Project,
20
+ ScriptTarget
21
+ } = tsMorph;
22
+
23
+ const HELPER_MAP_SCHEMA_VERSION = 1;
24
+ const CODE_EXTENSIONS = new Set([".cjs", ".js", ".jsx", ".mjs", ".ts", ".tsx", ".vue"]);
25
+ const APP_SCAN_ROOTS = Object.freeze(["src", "packages", "config", "server", "scripts"]);
26
+ const EXCLUDED_DIR_NAMES = new Set([
27
+ ".git",
28
+ ".jskit",
29
+ ".npm-cache",
30
+ "coverage",
31
+ "dist",
32
+ "node_modules"
33
+ ]);
34
+ const HELPER_NAME_PATTERN =
35
+ /^(assert|build|coerce|create|ensure|extract|format|get|has|is|list|load|make|map|normalize|parse|read|render|resolve|run|serialize|to|update|use|validate|write)[A-Z_]/u;
36
+
37
+ async function pathExists(filePath) {
38
+ try {
39
+ await stat(filePath);
40
+ return true;
41
+ } catch (error) {
42
+ if (error && error.code === "ENOENT") {
43
+ return false;
44
+ }
45
+ throw error;
46
+ }
47
+ }
48
+
49
+ async function readJsonFile(filePath) {
50
+ return JSON.parse(await readFile(filePath, "utf8"));
51
+ }
52
+
53
+ function normalizePackageDependencies(packageJson = {}) {
54
+ const dependencies = {
55
+ ...(packageJson.dependencies || {}),
56
+ ...(packageJson.devDependencies || {}),
57
+ ...(packageJson.peerDependencies || {}),
58
+ ...(packageJson.optionalDependencies || {})
59
+ };
60
+ return Object.keys(dependencies).sort((left, right) => left.localeCompare(right));
61
+ }
62
+
63
+ function classifySymbol(name = "") {
64
+ if (!name || name === "default") {
65
+ return "default";
66
+ }
67
+ if (/^use[A-Z]/u.test(name)) {
68
+ return "composable";
69
+ }
70
+ if (/^[A-Z]/u.test(name)) {
71
+ return "component_or_class";
72
+ }
73
+ if (HELPER_NAME_PATTERN.test(name)) {
74
+ return "helper";
75
+ }
76
+ return "export";
77
+ }
78
+
79
+ function createExportAnalysisProject() {
80
+ return new Project({
81
+ skipAddingFilesFromTsConfig: true,
82
+ compilerOptions: {
83
+ allowJs: true,
84
+ checkJs: false,
85
+ module: ModuleKind.ESNext,
86
+ moduleResolution: ModuleResolutionKind.NodeNext,
87
+ target: ScriptTarget.ESNext
88
+ }
89
+ });
90
+ }
91
+
92
+ function kindFromDeclaration(declaration, exportName = "") {
93
+ if (exportName === "default") {
94
+ return "default";
95
+ }
96
+ switch (declaration.getKindName()) {
97
+ case "ClassDeclaration":
98
+ return "class";
99
+ case "EnumDeclaration":
100
+ return "enum";
101
+ case "FunctionDeclaration":
102
+ case "FunctionExpression":
103
+ case "MethodDeclaration":
104
+ return "function";
105
+ case "InterfaceDeclaration":
106
+ return "interface";
107
+ case "TypeAliasDeclaration":
108
+ return "type";
109
+ case "VariableDeclaration": {
110
+ const initializer = typeof declaration.getInitializer === "function"
111
+ ? declaration.getInitializer()
112
+ : null;
113
+ const initializerKind = initializer?.getKindName?.() || "";
114
+ return initializerKind === "ArrowFunction" || initializerKind === "FunctionExpression"
115
+ ? "function"
116
+ : "value";
117
+ }
118
+ default:
119
+ return "export";
120
+ }
121
+ }
122
+
123
+ function addSymbol(symbols, symbol) {
124
+ if (!symbol.name) {
125
+ return;
126
+ }
127
+ const key = `${symbol.name}:${symbol.kind}`;
128
+ if (symbols.has(key)) {
129
+ return;
130
+ }
131
+ symbols.set(key, {
132
+ name: symbol.name,
133
+ kind: symbol.kind,
134
+ role: classifySymbol(symbol.name)
135
+ });
136
+ }
137
+
138
+ function extractExportedSymbols(sourceFile) {
139
+ const symbols = new Map();
140
+ for (const [name, declarations] of sourceFile.getExportedDeclarations()) {
141
+ for (const declaration of declarations) {
142
+ addSymbol(symbols, {
143
+ name,
144
+ kind: kindFromDeclaration(declaration, name)
145
+ });
146
+ }
147
+ }
148
+
149
+ return [...symbols.values()].sort((left, right) => {
150
+ const byName = left.name.localeCompare(right.name);
151
+ return byName || left.kind.localeCompare(right.kind);
152
+ });
153
+ }
154
+
155
+ function extractVueScriptSource(source = "", filePath = "") {
156
+ const parsed = parseVueSfc(source, {
157
+ filename: filePath
158
+ });
159
+ if (parsed.errors.length > 0) {
160
+ throw new Error(parsed.errors.map((error) => error.message || String(error)).join("; "));
161
+ }
162
+ const descriptor = parsed.descriptor;
163
+ if (descriptor.scriptSetup) {
164
+ return compileScript(descriptor, {
165
+ id: filePath
166
+ }).content;
167
+ }
168
+ if (descriptor.script) {
169
+ return descriptor.script.content;
170
+ }
171
+ return "";
172
+ }
173
+
174
+ async function addCodeFileToProject(project, file) {
175
+ if (path.extname(file.absolutePath) !== ".vue") {
176
+ return project.addSourceFileAtPath(file.absolutePath);
177
+ }
178
+ const source = extractVueScriptSource(await readFile(file.absolutePath, "utf8"), file.absolutePath);
179
+ if (!source.trim()) {
180
+ return null;
181
+ }
182
+ return project.createSourceFile(`${file.absolutePath}.ts`, source, {
183
+ overwrite: true
184
+ });
185
+ }
186
+
187
+ async function walkCodeFiles(rootPath, relativeRoot = "") {
188
+ const entries = await readdir(rootPath, {
189
+ withFileTypes: true
190
+ });
191
+ const files = [];
192
+ for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
193
+ if (EXCLUDED_DIR_NAMES.has(entry.name)) {
194
+ continue;
195
+ }
196
+ const absolutePath = path.join(rootPath, entry.name);
197
+ const relativePath = path.join(relativeRoot, entry.name).split(path.sep).join("/");
198
+ if (entry.isDirectory()) {
199
+ files.push(...await walkCodeFiles(absolutePath, relativePath));
200
+ continue;
201
+ }
202
+ if (!entry.isFile() || !CODE_EXTENSIONS.has(path.extname(entry.name))) {
203
+ continue;
204
+ }
205
+ files.push({
206
+ absolutePath,
207
+ relativePath
208
+ });
209
+ }
210
+ return files;
211
+ }
212
+
213
+ async function collectAppExports(targetRoot) {
214
+ const scanFiles = [];
215
+ for (const scanRoot of APP_SCAN_ROOTS) {
216
+ const rootPath = path.join(targetRoot, scanRoot);
217
+ if (await pathExists(rootPath)) {
218
+ scanFiles.push(...await walkCodeFiles(rootPath, scanRoot));
219
+ }
220
+ }
221
+
222
+ const project = createExportAnalysisProject();
223
+ const files = [];
224
+ for (const file of scanFiles.sort((left, right) => left.relativePath.localeCompare(right.relativePath))) {
225
+ const sourceFile = await addCodeFileToProject(project, file);
226
+ if (!sourceFile) {
227
+ continue;
228
+ }
229
+ const symbols = extractExportedSymbols(sourceFile);
230
+ if (symbols.length === 0) {
231
+ continue;
232
+ }
233
+ files.push({
234
+ path: file.relativePath,
235
+ exports: symbols
236
+ });
237
+ }
238
+ return files;
239
+ }
240
+
241
+ function flattenPackageExports(exportsField) {
242
+ const targets = new Map();
243
+
244
+ function addTarget(subpath, target) {
245
+ if (!target || target.includes("*")) {
246
+ return;
247
+ }
248
+ targets.set(`${subpath}:${target}`, {
249
+ subpath,
250
+ target
251
+ });
252
+ }
253
+
254
+ function collect(value, subpath = ".") {
255
+ if (typeof value === "string") {
256
+ addTarget(subpath, value);
257
+ return;
258
+ }
259
+ if (!value || Array.isArray(value) || typeof value !== "object") {
260
+ return;
261
+ }
262
+ const entries = Object.entries(value);
263
+ const hasSubpathKeys = entries.some(([key]) => key.startsWith("."));
264
+ for (const [key, nested] of entries) {
265
+ if (key.startsWith(".")) {
266
+ collect(nested, key);
267
+ } else {
268
+ collect(nested, hasSubpathKeys ? subpath : subpath || ".");
269
+ }
270
+ }
271
+ }
272
+
273
+ collect(exportsField, ".");
274
+ return [...targets.values()].sort((left, right) => {
275
+ const bySubpath = left.subpath.localeCompare(right.subpath);
276
+ return bySubpath || left.target.localeCompare(right.target);
277
+ });
278
+ }
279
+
280
+ async function collectJskitPackageExports(targetRoot, packageJson = {}) {
281
+ const packageNames = normalizePackageDependencies(packageJson)
282
+ .filter((name) => name.startsWith("@jskit-ai/"));
283
+ const packages = [];
284
+ const project = createExportAnalysisProject();
285
+
286
+ for (const packageName of packageNames) {
287
+ const packageRoot = path.join(targetRoot, "node_modules", ...packageName.split("/"));
288
+ const packageJsonPath = path.join(packageRoot, "package.json");
289
+ if (!await pathExists(packageJsonPath)) {
290
+ packages.push({
291
+ name: packageName,
292
+ installed: false,
293
+ exports: []
294
+ });
295
+ continue;
296
+ }
297
+
298
+ const installedPackageJson = await readJsonFile(packageJsonPath);
299
+ const exportTargets = flattenPackageExports(installedPackageJson.exports || {});
300
+ const exports = [];
301
+ for (const exportTarget of exportTargets) {
302
+ const normalizedTarget = exportTarget.target.replace(/^\.\//u, "");
303
+ const targetPath = path.join(packageRoot, normalizedTarget);
304
+ const ext = path.extname(targetPath);
305
+ if (!CODE_EXTENSIONS.has(ext) || !await pathExists(targetPath)) {
306
+ continue;
307
+ }
308
+ const sourceFile = await addCodeFileToProject(project, {
309
+ absolutePath: targetPath,
310
+ relativePath: normalizedTarget
311
+ });
312
+ exports.push({
313
+ subpath: exportTarget.subpath,
314
+ target: normalizedTarget.split(path.sep).join("/"),
315
+ exports: sourceFile ? extractExportedSymbols(sourceFile) : []
316
+ });
317
+ }
318
+
319
+ packages.push({
320
+ name: packageName,
321
+ version: installedPackageJson.version || "",
322
+ installed: true,
323
+ exports
324
+ });
325
+ }
326
+
327
+ return packages;
328
+ }
329
+
330
+ function renderExportList(symbols = []) {
331
+ if (symbols.length === 0) {
332
+ return " - no exported symbols detected";
333
+ }
334
+ return symbols
335
+ .map((symbol) => ` - ${symbol.name} (${symbol.kind}, ${symbol.role})`)
336
+ .join("\n");
337
+ }
338
+
339
+ function renderHelperMapMarkdown(map) {
340
+ const lines = [
341
+ "# JSKIT Helper Map",
342
+ "",
343
+ "Generated by `jskit helper-map update`. Read this before adding new helpers, composables, service functions, maps, or package glue.",
344
+ "",
345
+ `Root package: ${map.rootPackage.name || "unknown"}`,
346
+ "",
347
+ "## App-local exports",
348
+ ""
349
+ ];
350
+
351
+ if (map.app.files.length === 0) {
352
+ lines.push("No app-local exported helpers or symbols detected.", "");
353
+ } else {
354
+ for (const file of map.app.files) {
355
+ lines.push(`- ${file.path}`, renderExportList(file.exports), "");
356
+ }
357
+ }
358
+
359
+ lines.push("## Direct JSKIT package exports", "");
360
+ if (map.jskitPackages.length === 0) {
361
+ lines.push("No direct `@jskit-ai/*` dependencies were found.", "");
362
+ } else {
363
+ for (const packageEntry of map.jskitPackages) {
364
+ if (!packageEntry.installed) {
365
+ lines.push(`- ${packageEntry.name}: not installed in node_modules`, "");
366
+ continue;
367
+ }
368
+ lines.push(`- ${packageEntry.name}@${packageEntry.version || "unknown"}`);
369
+ if (packageEntry.exports.length === 0) {
370
+ lines.push(" - no exported code files detected");
371
+ } else {
372
+ for (const exportEntry of packageEntry.exports) {
373
+ lines.push(` - ${exportEntry.subpath} -> ${exportEntry.target}`);
374
+ for (const symbol of exportEntry.exports) {
375
+ lines.push(` - ${symbol.name} (${symbol.kind}, ${symbol.role})`);
376
+ }
377
+ }
378
+ }
379
+ lines.push("");
380
+ }
381
+ }
382
+
383
+ return `${lines.join("\n").replace(/\n{3,}/gu, "\n\n").trimEnd()}\n`;
384
+ }
385
+
386
+ async function buildHelperMap({ targetRoot }) {
387
+ const packageJsonPath = path.join(targetRoot, "package.json");
388
+ const packageJson = await readJsonFile(packageJsonPath);
389
+ const map = {
390
+ schemaVersion: HELPER_MAP_SCHEMA_VERSION,
391
+ generatedBy: "jskit helper-map update",
392
+ rootPackage: {
393
+ name: packageJson.name || "",
394
+ version: packageJson.version || ""
395
+ },
396
+ app: {
397
+ files: await collectAppExports(targetRoot)
398
+ },
399
+ jskitPackages: await collectJskitPackageExports(targetRoot, packageJson)
400
+ };
401
+ return {
402
+ ok: true,
403
+ map,
404
+ helperMapJsonPath: path.join(targetRoot, HELPER_MAP_JSON_RELATIVE_PATH),
405
+ helperMapMarkdownPath: path.join(targetRoot, HELPER_MAP_MARKDOWN_RELATIVE_PATH)
406
+ };
407
+ }
408
+
409
+ async function readHelperMap({ targetRoot }) {
410
+ const helperMapJsonPath = path.join(targetRoot, HELPER_MAP_JSON_RELATIVE_PATH);
411
+ const helperMapMarkdownPath = path.join(targetRoot, HELPER_MAP_MARKDOWN_RELATIVE_PATH);
412
+ if (!await pathExists(helperMapJsonPath)) {
413
+ return {
414
+ ok: true,
415
+ exists: false,
416
+ helperMapJsonPath,
417
+ helperMapMarkdownPath,
418
+ map: null,
419
+ markdown: ""
420
+ };
421
+ }
422
+ return {
423
+ ok: true,
424
+ exists: true,
425
+ helperMapJsonPath,
426
+ helperMapMarkdownPath,
427
+ map: await readJsonFile(helperMapJsonPath),
428
+ markdown: await pathExists(helperMapMarkdownPath) ? await readFile(helperMapMarkdownPath, "utf8") : ""
429
+ };
430
+ }
431
+
432
+ async function updateHelperMap({ targetRoot }) {
433
+ const payload = await buildHelperMap({ targetRoot });
434
+ const markdown = renderHelperMapMarkdown(payload.map);
435
+ const json = `${JSON.stringify(payload.map, null, 2)}\n`;
436
+ const currentJson = await pathExists(payload.helperMapJsonPath)
437
+ ? await readFile(payload.helperMapJsonPath, "utf8")
438
+ : "";
439
+ const currentMarkdown = await pathExists(payload.helperMapMarkdownPath)
440
+ ? await readFile(payload.helperMapMarkdownPath, "utf8")
441
+ : "";
442
+ const changed = currentJson !== json || currentMarkdown !== markdown;
443
+ if (changed) {
444
+ await mkdir(path.dirname(payload.helperMapJsonPath), {
445
+ recursive: true
446
+ });
447
+ await writeFile(payload.helperMapJsonPath, json, "utf8");
448
+ await writeFile(payload.helperMapMarkdownPath, markdown, "utf8");
449
+ }
450
+ return {
451
+ ...payload,
452
+ changed,
453
+ markdown
454
+ };
455
+ }
456
+
457
+ export {
458
+ HELPER_MAP_JSON_RELATIVE_PATH,
459
+ HELPER_MAP_MARKDOWN_RELATIVE_PATH,
460
+ buildHelperMap,
461
+ readHelperMap,
462
+ updateHelperMap
463
+ };
@@ -0,0 +1,7 @@
1
+ const HELPER_MAP_JSON_RELATIVE_PATH = ".jskit/helper-map.json";
2
+ const HELPER_MAP_MARKDOWN_RELATIVE_PATH = ".jskit/helper-map.md";
3
+
4
+ export {
5
+ HELPER_MAP_JSON_RELATIVE_PATH,
6
+ HELPER_MAP_MARKDOWN_RELATIVE_PATH
7
+ };
@@ -0,0 +1,55 @@
1
+ import { access, stat } from "node:fs/promises";
2
+ import { constants as fsConstants } from "node:fs";
3
+ import path from "node:path";
4
+
5
+ const REQUIRED_READY_FILES = Object.freeze([
6
+ "package.json",
7
+ ".jskit/lock.json",
8
+ "config/public.js"
9
+ ]);
10
+ const REQUIRED_READY_DIRECTORIES = Object.freeze([
11
+ "src",
12
+ "packages"
13
+ ]);
14
+
15
+ async function pathExists(filePath) {
16
+ try {
17
+ await access(filePath, fsConstants.F_OK);
18
+ return true;
19
+ } catch {
20
+ return false;
21
+ }
22
+ }
23
+
24
+ async function directoryExists(filePath) {
25
+ try {
26
+ return (await stat(filePath)).isDirectory();
27
+ } catch {
28
+ return false;
29
+ }
30
+ }
31
+
32
+ async function inspectReadyJskitAppRoot(rootPath) {
33
+ const missing = [];
34
+ for (const relativePath of REQUIRED_READY_FILES) {
35
+ if (!await pathExists(path.join(rootPath, relativePath))) {
36
+ missing.push(relativePath);
37
+ }
38
+ }
39
+ for (const relativePath of REQUIRED_READY_DIRECTORIES) {
40
+ if (!await directoryExists(path.join(rootPath, relativePath))) {
41
+ missing.push(`${relativePath}/`);
42
+ }
43
+ }
44
+ return {
45
+ missing,
46
+ ok: missing.length < 1,
47
+ requiredDirectories: [...REQUIRED_READY_DIRECTORIES],
48
+ requiredFiles: [...REQUIRED_READY_FILES],
49
+ rootPath
50
+ };
51
+ }
52
+
53
+ export {
54
+ inspectReadyJskitAppRoot
55
+ };