@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.
- package/package.json +6 -4
- package/src/server/appBlueprint.js +1 -1
- package/src/server/commandHandlers/helperMap.js +104 -0
- package/src/server/commandHandlers/session.js +179 -4
- package/src/server/commandHandlers/show.js +169 -34
- package/src/server/core/argParser.js +20 -0
- package/src/server/core/commandCatalog.js +70 -7
- package/src/server/core/createCommandHandlers.js +4 -1
- package/src/server/helperMap.js +463 -0
- package/src/server/helperMapPaths.js +7 -0
- package/src/server/sessionRuntime/appReadiness.js +55 -0
- package/src/server/sessionRuntime/constants.js +298 -135
- package/src/server/sessionRuntime/paths.js +2 -4
- package/src/server/sessionRuntime/preconditions.js +393 -26
- package/src/server/sessionRuntime/promptRenderer.js +15 -2
- package/src/server/sessionRuntime/prompts/app_blueprint.md +26 -2
- package/src/server/sessionRuntime/prompts/automated_checks.md +42 -0
- package/src/server/sessionRuntime/prompts/deep_ui_check.md +53 -0
- package/src/server/sessionRuntime/prompts/doctor_failure.md +21 -1
- package/src/server/sessionRuntime/prompts/execute_plan.md +61 -0
- package/src/server/sessionRuntime/prompts/final_comment.md +3 -1
- package/src/server/sessionRuntime/prompts/issue_details.md +52 -0
- package/src/server/sessionRuntime/prompts/new_issue.md +34 -3
- package/src/server/sessionRuntime/prompts/plan_issue.md +81 -0
- package/src/server/sessionRuntime/prompts/pr_failure.md +14 -1
- package/src/server/sessionRuntime/prompts/review_changes.md +77 -15
- package/src/server/sessionRuntime/prompts/update_blueprint.md +36 -0
- package/src/server/sessionRuntime/prompts/user_check.md +22 -4
- package/src/server/sessionRuntime/responses.js +877 -30
- package/src/server/sessionRuntime/worktrees.js +31 -0
- package/src/server/sessionRuntime.js +2070 -244
- 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/
|
|
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
|
|
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,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
|
+
};
|