@jskit-ai/jskit-cli 0.2.79 → 0.2.81

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jskit-ai/jskit-cli",
3
- "version": "0.2.79",
3
+ "version": "0.2.81",
4
4
  "description": "Bundle and package orchestration CLI for JSKIT apps.",
5
5
  "type": "module",
6
6
  "files": [
@@ -20,9 +20,9 @@
20
20
  "test": "node --test"
21
21
  },
22
22
  "dependencies": {
23
- "@jskit-ai/jskit-catalog": "0.1.78",
24
- "@jskit-ai/kernel": "0.1.70",
25
- "@jskit-ai/shell-web": "0.1.69"
23
+ "@jskit-ai/jskit-catalog": "0.1.80",
24
+ "@jskit-ai/kernel": "0.1.72",
25
+ "@jskit-ai/shell-web": "0.1.71"
26
26
  },
27
27
  "engines": {
28
28
  "node": ">=20 <23"
@@ -0,0 +1,126 @@
1
+ import { mkdir, readFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import {
4
+ PROMPT_DIRECTORY
5
+ } from "./sessionRuntime/constants.js";
6
+ import {
7
+ fileExists,
8
+ normalizeText,
9
+ readTextIfExists,
10
+ writeTextFile
11
+ } from "./sessionRuntime/io.js";
12
+ import {
13
+ renderTemplate
14
+ } from "./sessionRuntime/promptRenderer.js";
15
+
16
+ const APP_BLUEPRINT_RELATIVE_PATH = ".jskit/APP_BLUEPRINT.md";
17
+ const APP_PROMPT_OVERRIDE_RELATIVE_ROOT = ".jskit/prompts";
18
+
19
+ function resolveAppBlueprintPaths(targetRoot = process.cwd()) {
20
+ const normalizedTargetRoot = path.resolve(normalizeText(targetRoot) || process.cwd());
21
+ return Object.freeze({
22
+ appBlueprintPath: path.join(normalizedTargetRoot, APP_BLUEPRINT_RELATIVE_PATH),
23
+ promptOverrideRoot: path.join(normalizedTargetRoot, APP_PROMPT_OVERRIDE_RELATIVE_ROOT),
24
+ targetRoot: normalizedTargetRoot
25
+ });
26
+ }
27
+
28
+ function extractAppBlueprintText(value = "") {
29
+ const text = normalizeText(value);
30
+ const match = /\[app_blueprint\]([\s\S]*?)\[\/app_blueprint\]/u.exec(text);
31
+ return normalizeText(match ? match[1] : text);
32
+ }
33
+
34
+ async function readAppPromptTemplate(paths, templateName) {
35
+ const normalizedName = normalizeText(templateName);
36
+ const overridePath = path.join(paths.promptOverrideRoot, normalizedName);
37
+ if (await fileExists(overridePath)) {
38
+ return readTextIfExists(overridePath);
39
+ }
40
+ return readTextIfExists(path.join(PROMPT_DIRECTORY, normalizedName));
41
+ }
42
+
43
+ async function renderAppBlueprintPrompt({ targetRoot = process.cwd(), appBrief = "" } = {}) {
44
+ const paths = resolveAppBlueprintPaths(targetRoot);
45
+ const normalizedBrief = normalizeText(appBrief);
46
+ if (!normalizedBrief) {
47
+ return {
48
+ ok: false,
49
+ appBlueprintPath: paths.appBlueprintPath,
50
+ errors: [
51
+ {
52
+ code: "app_brief_required",
53
+ message: "jskit blueprint prompt requires --brief, --brief-file, or --brief -.",
54
+ repairCommand: "jskit blueprint prompt --brief \"<what app are we building>\""
55
+ }
56
+ ],
57
+ prompt: ""
58
+ };
59
+ }
60
+ const template = await readAppPromptTemplate(paths, "app_blueprint.md");
61
+ return {
62
+ ok: true,
63
+ appBlueprintPath: paths.appBlueprintPath,
64
+ errors: [],
65
+ prompt: renderTemplate(template, {
66
+ app_brief: normalizedBrief
67
+ }).trim()
68
+ };
69
+ }
70
+
71
+ async function readAppBlueprint({ targetRoot = process.cwd() } = {}) {
72
+ const paths = resolveAppBlueprintPaths(targetRoot);
73
+ const blueprintText = await readTextIfExists(paths.appBlueprintPath);
74
+ return {
75
+ ok: true,
76
+ appBlueprintPath: paths.appBlueprintPath,
77
+ blueprintText: blueprintText.trim(),
78
+ exists: Boolean(blueprintText.trim()),
79
+ errors: []
80
+ };
81
+ }
82
+
83
+ async function writeAppBlueprint({ targetRoot = process.cwd(), appBlueprint = "" } = {}) {
84
+ const paths = resolveAppBlueprintPaths(targetRoot);
85
+ const blueprintText = extractAppBlueprintText(appBlueprint);
86
+ if (!blueprintText) {
87
+ return {
88
+ ok: false,
89
+ appBlueprintPath: paths.appBlueprintPath,
90
+ blueprintText: "",
91
+ exists: false,
92
+ errors: [
93
+ {
94
+ code: "app_blueprint_required",
95
+ message: "jskit blueprint set requires --blueprint, --blueprint-file, or --blueprint -.",
96
+ repairCommand: "jskit blueprint set --blueprint -"
97
+ }
98
+ ]
99
+ };
100
+ }
101
+ await mkdir(path.dirname(paths.appBlueprintPath), { recursive: true });
102
+ await writeTextFile(paths.appBlueprintPath, blueprintText);
103
+ return {
104
+ ok: true,
105
+ appBlueprintPath: paths.appBlueprintPath,
106
+ blueprintText,
107
+ exists: true,
108
+ errors: []
109
+ };
110
+ }
111
+
112
+ async function readTextInputFile(cwd, inputPath) {
113
+ const resolvedPath = path.resolve(cwd, normalizeText(inputPath));
114
+ return readFile(resolvedPath, "utf8");
115
+ }
116
+
117
+ export {
118
+ APP_BLUEPRINT_RELATIVE_PATH,
119
+ APP_PROMPT_OVERRIDE_RELATIVE_ROOT,
120
+ extractAppBlueprintText,
121
+ readAppBlueprint,
122
+ readTextInputFile,
123
+ renderAppBlueprintPrompt,
124
+ resolveAppBlueprintPaths,
125
+ writeAppBlueprint
126
+ };
@@ -0,0 +1,151 @@
1
+ import {
2
+ readAppBlueprint,
3
+ readTextInputFile,
4
+ renderAppBlueprintPrompt,
5
+ writeAppBlueprint
6
+ } from "../appBlueprint.js";
7
+
8
+ function writeJson(stdout, payload) {
9
+ stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
10
+ }
11
+
12
+ function writeBlueprintText(stdout, payload) {
13
+ if (payload.prompt) {
14
+ stdout.write(`${payload.prompt}\n`);
15
+ return;
16
+ }
17
+ if (payload.blueprintText) {
18
+ stdout.write(`${payload.blueprintText}\n`);
19
+ return;
20
+ }
21
+ stdout.write(`No app blueprint set at ${payload.appBlueprintPath}.\n`);
22
+ }
23
+
24
+ async function readStream(stream) {
25
+ const chunks = [];
26
+ for await (const chunk of stream) {
27
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
28
+ }
29
+ return Buffer.concat(chunks).toString("utf8");
30
+ }
31
+
32
+ async function resolveTextInput({
33
+ cwd,
34
+ fileOption,
35
+ inlineOptions = {},
36
+ io = {},
37
+ stdinOption = "-",
38
+ textOption
39
+ }) {
40
+ if (Object.hasOwn(inlineOptions, fileOption)) {
41
+ const inputFile = String(inlineOptions[fileOption] || "").trim();
42
+ return inputFile ? readTextInputFile(cwd, inputFile) : "";
43
+ }
44
+ if (Object.hasOwn(inlineOptions, textOption)) {
45
+ const textValue = String(inlineOptions[textOption] ?? "");
46
+ return textValue === stdinOption ? readStream(io.stdin) : textValue;
47
+ }
48
+ return "";
49
+ }
50
+
51
+ function createBlueprintCommands(ctx = {}) {
52
+ const { resolveAppRootFromCwd } = ctx;
53
+
54
+ async function commandBlueprint({
55
+ positional = [],
56
+ options = {},
57
+ cwd,
58
+ stdout,
59
+ io = {}
60
+ } = {}) {
61
+ const appRoot = await resolveAppRootFromCwd(cwd);
62
+ const inlineOptions = options.inlineOptions || {};
63
+ const subcommand = String(positional[0] || "").trim();
64
+ let payload;
65
+
66
+ try {
67
+ if (positional.length > 1) {
68
+ payload = {
69
+ ok: false,
70
+ appBlueprintPath: "",
71
+ errors: [
72
+ {
73
+ code: "unexpected_blueprint_argument",
74
+ message: `Unexpected blueprint argument: ${positional.slice(1).join(" ")}`,
75
+ repairCommand: "jskit blueprint"
76
+ }
77
+ ]
78
+ };
79
+ } else if (!subcommand) {
80
+ payload = await readAppBlueprint({ targetRoot: appRoot });
81
+ } else if (subcommand === "prompt") {
82
+ const appBrief = await resolveTextInput({
83
+ cwd,
84
+ fileOption: "brief-file",
85
+ inlineOptions,
86
+ io,
87
+ textOption: "brief"
88
+ });
89
+ payload = await renderAppBlueprintPrompt({
90
+ targetRoot: appRoot,
91
+ appBrief
92
+ });
93
+ } else if (subcommand === "set") {
94
+ const appBlueprint = await resolveTextInput({
95
+ cwd,
96
+ fileOption: "blueprint-file",
97
+ inlineOptions,
98
+ io,
99
+ textOption: "blueprint"
100
+ });
101
+ payload = await writeAppBlueprint({
102
+ targetRoot: appRoot,
103
+ appBlueprint
104
+ });
105
+ } else {
106
+ payload = {
107
+ ok: false,
108
+ appBlueprintPath: "",
109
+ errors: [
110
+ {
111
+ code: "unknown_blueprint_subcommand",
112
+ message: `Unknown blueprint subcommand: ${subcommand}`,
113
+ repairCommand: "jskit blueprint"
114
+ }
115
+ ]
116
+ };
117
+ }
118
+ } catch (error) {
119
+ payload = {
120
+ ok: false,
121
+ appBlueprintPath: "",
122
+ errors: [
123
+ {
124
+ code: "blueprint_input_read_failed",
125
+ message: String(error?.message || error),
126
+ repairCommand: "jskit blueprint"
127
+ }
128
+ ]
129
+ };
130
+ }
131
+
132
+ if (options.json) {
133
+ writeJson(stdout, payload);
134
+ } else if (payload.ok === false) {
135
+ for (const error of payload.errors || []) {
136
+ stdout.write(`[${error.code}] ${error.message}\n`);
137
+ if (error.repairCommand) {
138
+ stdout.write(`Repair: ${error.repairCommand}\n`);
139
+ }
140
+ }
141
+ } else {
142
+ writeBlueprintText(stdout, payload);
143
+ }
144
+
145
+ return payload.ok === false ? 1 : 0;
146
+ }
147
+
148
+ return { commandBlueprint };
149
+ }
150
+
151
+ export { createBlueprintCommands };
@@ -0,0 +1,305 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import {
3
+ abandonSession,
4
+ adoptCodexThreadId,
5
+ buildSessionErrorResponse,
6
+ createSession,
7
+ inspectSessionDiff,
8
+ inspectSessionDetails,
9
+ listSessions,
10
+ runSessionStep
11
+ } from "../sessionRuntime.js";
12
+
13
+ function writeJson(stdout, payload) {
14
+ stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
15
+ }
16
+
17
+ function writeSessionText(stdout, payload) {
18
+ if (payload.sessions) {
19
+ stdout.write("JSKIT sessions\n");
20
+ if (payload.sessions.length < 1) {
21
+ stdout.write("No sessions found.\n");
22
+ return;
23
+ }
24
+ for (const session of payload.sessions) {
25
+ stdout.write(`- ${session.sessionId} ${session.status} ${session.currentStep || "done"}\n`);
26
+ }
27
+ return;
28
+ }
29
+
30
+ stdout.write(`Session: ${payload.sessionId || "unknown"}\n`);
31
+ stdout.write(`Status: ${payload.status || "unknown"}\n`);
32
+ stdout.write(`Current step: ${payload.currentStep || "done"}\n`);
33
+ if (payload.issueUrl) {
34
+ stdout.write(`Issue: ${payload.issueUrl}\n`);
35
+ }
36
+ if (payload.prUrl) {
37
+ stdout.write(`PR: ${payload.prUrl}\n`);
38
+ }
39
+ if (payload.branch) {
40
+ stdout.write(`Branch: ${payload.branch}\n`);
41
+ }
42
+ if (payload.worktree) {
43
+ stdout.write(`Worktree: ${payload.worktree}\n`);
44
+ }
45
+ if (payload.completedSteps?.length) {
46
+ stdout.write("Done steps:\n");
47
+ for (const step of payload.completedSteps) {
48
+ stdout.write(`- ${step}\n`);
49
+ }
50
+ }
51
+ if (payload.prompt) {
52
+ stdout.write("\n");
53
+ stdout.write(payload.prompt);
54
+ stdout.write("\n");
55
+ }
56
+ if (payload.gitStatus !== undefined && payload.unstagedDiff !== undefined) {
57
+ stdout.write("\nGit status:\n");
58
+ stdout.write(payload.gitStatus || "No changes.");
59
+ stdout.write("\n");
60
+ const diff = [payload.stagedDiff, payload.unstagedDiff, payload.untrackedDiff].filter(Boolean).join("\n");
61
+ if (diff) {
62
+ stdout.write("\nDiff:\n");
63
+ stdout.write(diff);
64
+ stdout.write("\n");
65
+ }
66
+ }
67
+ if (payload.errors?.length) {
68
+ stdout.write("Errors:\n");
69
+ for (const error of payload.errors) {
70
+ stdout.write(`- [${error.code}] ${error.message}\n`);
71
+ if (error.repairCommand) {
72
+ stdout.write(` Repair: ${error.repairCommand}\n`);
73
+ }
74
+ }
75
+ }
76
+ if (payload.nextCommand) {
77
+ stdout.write(`Next: ${payload.nextCommand}\n`);
78
+ }
79
+ }
80
+
81
+ async function readStream(stream) {
82
+ const chunks = [];
83
+ for await (const chunk of stream) {
84
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
85
+ }
86
+ return Buffer.concat(chunks).toString("utf8");
87
+ }
88
+
89
+ function resolveInputFilePath(cwd, filePath) {
90
+ return filePath.startsWith("/") ? filePath : `${cwd}/${filePath}`;
91
+ }
92
+
93
+ async function resolveTextInput({
94
+ codePrefix,
95
+ fileOption,
96
+ inlineOptions = {},
97
+ io = {},
98
+ repairCommand,
99
+ cwd,
100
+ stdinOption,
101
+ textOption,
102
+ sessionId
103
+ }) {
104
+ if (Object.hasOwn(inlineOptions, fileOption)) {
105
+ const inputFile = String(inlineOptions[fileOption] || "").trim();
106
+ if (!inputFile) {
107
+ return { ok: true, value: "" };
108
+ }
109
+ const resolvedInputFile = resolveInputFilePath(cwd, inputFile);
110
+ try {
111
+ return {
112
+ ok: true,
113
+ value: await readFile(resolvedInputFile, "utf8")
114
+ };
115
+ } catch (error) {
116
+ return {
117
+ ok: false,
118
+ payload: buildSessionErrorResponse({
119
+ targetRoot: cwd,
120
+ sessionId,
121
+ code: `${codePrefix}_file_read_failed`,
122
+ message: `Could not read ${codePrefix.replaceAll("_", " ")} file ${resolvedInputFile}: ${error.message}`,
123
+ repairCommand
124
+ })
125
+ };
126
+ }
127
+ }
128
+ if (Object.hasOwn(inlineOptions, textOption)) {
129
+ const textValue = String(inlineOptions[textOption] ?? "");
130
+ if (textValue === stdinOption) {
131
+ return {
132
+ ok: true,
133
+ value: await readStream(io.stdin)
134
+ };
135
+ }
136
+ return {
137
+ ok: true,
138
+ value: textValue
139
+ };
140
+ }
141
+ return { ok: true, value: "" };
142
+ }
143
+
144
+ async function resolveStepInputs({
145
+ inlineOptions = {},
146
+ io = {},
147
+ cwd,
148
+ sessionId
149
+ }) {
150
+ const issueTitle = await resolveTextInput({
151
+ codePrefix: "issue_title",
152
+ fileOption: "issue-title-file",
153
+ inlineOptions,
154
+ io,
155
+ repairCommand: `jskit session ${sessionId} step --issue-title "<title>" --issue -`,
156
+ cwd,
157
+ sessionId,
158
+ stdinOption: "-",
159
+ textOption: "issue-title"
160
+ });
161
+ if (issueTitle.ok === false) {
162
+ return issueTitle;
163
+ }
164
+
165
+ const issue = await resolveTextInput({
166
+ codePrefix: "issue",
167
+ fileOption: "issue-file",
168
+ inlineOptions,
169
+ io,
170
+ repairCommand: `jskit session ${sessionId} step --issue -`,
171
+ cwd,
172
+ sessionId,
173
+ stdinOption: "-",
174
+ textOption: "issue"
175
+ });
176
+ if (issue.ok === false) {
177
+ return issue;
178
+ }
179
+
180
+ const plan = await resolveTextInput({
181
+ codePrefix: "plan",
182
+ fileOption: "plan-file",
183
+ inlineOptions,
184
+ io,
185
+ repairCommand: `jskit session ${sessionId} step --plan -`,
186
+ cwd,
187
+ sessionId,
188
+ stdinOption: "-",
189
+ textOption: "plan"
190
+ });
191
+ if (plan.ok === false) {
192
+ return plan;
193
+ }
194
+
195
+ return {
196
+ issue: issue.value,
197
+ issueTitle: issueTitle.value,
198
+ ok: true,
199
+ plan: plan.value
200
+ };
201
+ }
202
+
203
+ function normalizeStepOptions(inlineOptions = {}) {
204
+ return {
205
+ ...inlineOptions,
206
+ prompt: inlineOptions.prompt,
207
+ userCheck: inlineOptions["user-check"] || inlineOptions.userCheck
208
+ };
209
+ }
210
+
211
+ function resolveListArchiveOption(options = {}) {
212
+ const archives = [];
213
+ if (options.abandoned) {
214
+ archives.push("abandoned");
215
+ }
216
+ if (options.completed) {
217
+ archives.push("completed");
218
+ }
219
+ if (options.all) {
220
+ archives.push("all");
221
+ }
222
+ return archives.length > 0 ? archives : "active";
223
+ }
224
+
225
+ function createSessionCommands() {
226
+ return {
227
+ async commandSession({
228
+ positional = [],
229
+ options = {},
230
+ cwd,
231
+ stdout,
232
+ io = {}
233
+ } = {}) {
234
+ const [first, second] = positional;
235
+ const inlineOptions = options.inlineOptions || {};
236
+ let payload;
237
+
238
+ if (!first) {
239
+ payload = await listSessions({
240
+ targetRoot: cwd,
241
+ archive: resolveListArchiveOption(options)
242
+ });
243
+ } else if (first === "create") {
244
+ payload = await createSession({ targetRoot: cwd });
245
+ } else if (second === "step") {
246
+ const stepInputs = await resolveStepInputs({
247
+ inlineOptions,
248
+ io,
249
+ cwd,
250
+ sessionId: first
251
+ });
252
+ payload = stepInputs.ok === false
253
+ ? stepInputs.payload
254
+ : await runSessionStep({
255
+ targetRoot: cwd,
256
+ sessionId: first,
257
+ options: {
258
+ ...normalizeStepOptions(inlineOptions),
259
+ issue: stepInputs.issue,
260
+ issueTitle: stepInputs.issueTitle,
261
+ plan: stepInputs.plan
262
+ }
263
+ });
264
+ } else if (second === "abandon") {
265
+ payload = await abandonSession({
266
+ targetRoot: cwd,
267
+ sessionId: first
268
+ });
269
+ } else if (second === "diff") {
270
+ payload = await inspectSessionDiff({
271
+ targetRoot: cwd,
272
+ sessionId: first
273
+ });
274
+ } else if (second === "adopt-codex-thread") {
275
+ payload = await adoptCodexThreadId({
276
+ targetRoot: cwd,
277
+ sessionId: first,
278
+ codexThreadId: inlineOptions["codex-thread-id"] || inlineOptions.codexThreadId
279
+ });
280
+ } else if (!second) {
281
+ payload = await inspectSessionDetails({
282
+ targetRoot: cwd,
283
+ sessionId: first
284
+ });
285
+ } else {
286
+ payload = buildSessionErrorResponse({
287
+ targetRoot: cwd,
288
+ sessionId: first,
289
+ code: "unknown_session_subcommand",
290
+ message: `Unknown session subcommand: ${second}`,
291
+ repairCommand: `jskit session ${first}`
292
+ });
293
+ }
294
+
295
+ if (options.json) {
296
+ writeJson(stdout, payload);
297
+ } else {
298
+ writeSessionText(stdout, payload);
299
+ }
300
+ return payload.ok === false ? 1 : 0;
301
+ }
302
+ };
303
+ }
304
+
305
+ export { createSessionCommands };
@@ -23,6 +23,8 @@ function parseArgs(argv, { createCliError } = {}) {
23
23
  verbose: false,
24
24
  json: false,
25
25
  all: false,
26
+ abandoned: false,
27
+ completed: false,
26
28
  concrete: false,
27
29
  help: true,
28
30
  inlineOptions: {}
@@ -50,6 +52,8 @@ function parseArgs(argv, { createCliError } = {}) {
50
52
  verbose: false,
51
53
  json: false,
52
54
  all: false,
55
+ abandoned: false,
56
+ completed: false,
53
57
  concrete: false,
54
58
  help: false,
55
59
  inlineOptions: {}
@@ -109,6 +113,14 @@ function parseArgs(argv, { createCliError } = {}) {
109
113
  options.all = true;
110
114
  continue;
111
115
  }
116
+ if (token === "--abandoned") {
117
+ options.abandoned = true;
118
+ continue;
119
+ }
120
+ if (token === "--completed") {
121
+ options.completed = true;
122
+ continue;
123
+ }
112
124
  if (token === "--concrete") {
113
125
  options.concrete = true;
114
126
  continue;
@@ -143,7 +155,7 @@ function parseArgs(argv, { createCliError } = {}) {
143
155
  } else {
144
156
  const hasNextStringToken = typeof args[0] === "string";
145
157
  const nextToken = hasNextStringToken ? String(args[0]) : "";
146
- if (hasNextStringToken && !nextToken.startsWith("-")) {
158
+ if (hasNextStringToken && (!nextToken.startsWith("-") || nextToken === "-")) {
147
159
  optionValueRaw = args.shift();
148
160
  }
149
161
  }
@@ -153,7 +165,7 @@ function parseArgs(argv, { createCliError } = {}) {
153
165
  }
154
166
  if (typeof optionValueRaw === "string") {
155
167
  const optionValue = optionValueRaw.trim();
156
- if (!hasInlineValue && optionValue.startsWith("-")) {
168
+ if (!hasInlineValue && optionValue.startsWith("-") && optionValue !== "-") {
157
169
  throw createCliError(`--${optionName} requires a value.`, { showUsage: true });
158
170
  }
159
171
  options.inlineOptions[optionName] = optionValue;