@jskit-ai/jskit-cli 0.2.50 → 0.2.51
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 +4 -4
- package/src/server/cliRuntime/appState.js +23 -1
- package/src/server/cliRuntime/completion.js +33 -0
- package/src/server/commandHandlers/app.js +161 -0
- package/src/server/commandHandlers/appCommandCatalog.js +170 -0
- package/src/server/commandHandlers/appCommands/adoptManagedScripts.js +132 -0
- package/src/server/commandHandlers/appCommands/linkLocalPackages.js +63 -0
- package/src/server/commandHandlers/appCommands/release.js +174 -0
- package/src/server/commandHandlers/appCommands/shared.js +275 -0
- package/src/server/commandHandlers/appCommands/updatePackages.js +102 -0
- package/src/server/commandHandlers/appCommands/verify.js +35 -0
- package/src/server/core/commandCatalog.js +41 -0
- package/src/server/core/createCommandHandlers.js +3 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jskit-ai/jskit-cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.51",
|
|
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.
|
|
24
|
-
"@jskit-ai/kernel": "0.1.
|
|
25
|
-
"@jskit-ai/shell-web": "0.1.
|
|
23
|
+
"@jskit-ai/jskit-catalog": "0.1.50",
|
|
24
|
+
"@jskit-ai/kernel": "0.1.42",
|
|
25
|
+
"@jskit-ai/shell-web": "0.1.41"
|
|
26
26
|
},
|
|
27
27
|
"engines": {
|
|
28
28
|
"node": "20.x"
|
|
@@ -11,19 +11,41 @@ import {
|
|
|
11
11
|
|
|
12
12
|
const LOCK_RELATIVE_PATH = ".jskit/lock.json";
|
|
13
13
|
const LOCK_VERSION = 1;
|
|
14
|
+
const APP_ROOT_MARKER_RELATIVE_PATHS = Object.freeze([
|
|
15
|
+
LOCK_RELATIVE_PATH,
|
|
16
|
+
"app.json"
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
async function directoryLooksLikeJskitAppRoot(directoryPath) {
|
|
20
|
+
for (const relativePath of APP_ROOT_MARKER_RELATIVE_PATHS) {
|
|
21
|
+
if (await fileExists(path.join(directoryPath, relativePath))) {
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
14
27
|
|
|
15
28
|
async function resolveAppRootFromCwd(cwd) {
|
|
16
29
|
const startDirectory = path.resolve(String(cwd || process.cwd()));
|
|
17
30
|
let currentDirectory = startDirectory;
|
|
31
|
+
let fallbackPackageRoot = "";
|
|
18
32
|
|
|
19
33
|
while (true) {
|
|
20
34
|
const packageJsonPath = path.join(currentDirectory, "package.json");
|
|
21
35
|
if (await fileExists(packageJsonPath)) {
|
|
22
|
-
|
|
36
|
+
if (!fallbackPackageRoot) {
|
|
37
|
+
fallbackPackageRoot = currentDirectory;
|
|
38
|
+
}
|
|
39
|
+
if (await directoryLooksLikeJskitAppRoot(currentDirectory)) {
|
|
40
|
+
return currentDirectory;
|
|
41
|
+
}
|
|
23
42
|
}
|
|
24
43
|
|
|
25
44
|
const parentDirectory = path.dirname(currentDirectory);
|
|
26
45
|
if (parentDirectory === currentDirectory) {
|
|
46
|
+
if (fallbackPackageRoot) {
|
|
47
|
+
return fallbackPackageRoot;
|
|
48
|
+
}
|
|
27
49
|
throw createCliError(
|
|
28
50
|
`Could not locate package.json starting from ${startDirectory}. Run jskit from an app directory (or a child directory of one).`
|
|
29
51
|
);
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { pathToFileURL } from "node:url";
|
|
3
3
|
import { access, readdir, readFile } from "node:fs/promises";
|
|
4
|
+
import {
|
|
5
|
+
buildAppCommandOptionMeta,
|
|
6
|
+
listAppCommandDefinitions
|
|
7
|
+
} from "../commandHandlers/appCommandCatalog.js";
|
|
4
8
|
import {
|
|
5
9
|
COMMAND_IDS,
|
|
6
10
|
isKnownCommandName,
|
|
@@ -1028,6 +1032,35 @@ async function completeCommand({ appRoot, words, cword, catalogModule }) {
|
|
|
1028
1032
|
if (command === "generate") {
|
|
1029
1033
|
return completeGenerateCommand({ appRoot, words, cword, catalogModule });
|
|
1030
1034
|
}
|
|
1035
|
+
if (command === "app") {
|
|
1036
|
+
const appCommandNames = listAppCommandDefinitions().map((entry) => entry.name);
|
|
1037
|
+
const currentTokenForApp = words[cword] ?? "";
|
|
1038
|
+
const subcommandName = normalizeText(words[2]);
|
|
1039
|
+
if (cword <= 2) {
|
|
1040
|
+
return filterByPrefix(appCommandNames, currentTokenForApp);
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
if (!subcommandName || !appCommandNames.includes(subcommandName)) {
|
|
1044
|
+
return filterByPrefix(appCommandNames, currentTokenForApp);
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
const previousTokenForApp = words[cword - 1] ?? "";
|
|
1048
|
+
const optionMetaForApp = buildAppCommandOptionMeta(subcommandName);
|
|
1049
|
+
return completeGenericContext({
|
|
1050
|
+
appRoot,
|
|
1051
|
+
currentToken: currentTokenForApp,
|
|
1052
|
+
previousToken: previousTokenForApp,
|
|
1053
|
+
optionMeta: optionMetaForApp,
|
|
1054
|
+
tokensBeforeCurrent: words.slice(3, cword),
|
|
1055
|
+
optionNames: uniqueSorted(Object.keys(optionMetaForApp)),
|
|
1056
|
+
positionalCompleter: async ({ positionalIndex, currentToken: positionalCurrent }) => {
|
|
1057
|
+
if (positionalIndex === 0) {
|
|
1058
|
+
return filterByPrefix(["help"], positionalCurrent);
|
|
1059
|
+
}
|
|
1060
|
+
return [];
|
|
1061
|
+
}
|
|
1062
|
+
});
|
|
1063
|
+
}
|
|
1031
1064
|
|
|
1032
1065
|
const optionMeta = buildCommandOptionMeta(command, catalogModule);
|
|
1033
1066
|
const optionNames = uniqueSorted(Object.keys(optionMeta));
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createColorFormatter,
|
|
3
|
+
writeWrappedLines
|
|
4
|
+
} from "../shared/outputFormatting.js";
|
|
5
|
+
import {
|
|
6
|
+
APP_SCRIPT_WRAPPERS,
|
|
7
|
+
buildAppCommandOptionMeta,
|
|
8
|
+
listAppCommandDefinitions,
|
|
9
|
+
resolveAppCommandDefinition
|
|
10
|
+
} from "./appCommandCatalog.js";
|
|
11
|
+
import { runAppAdoptManagedScriptsCommand } from "./appCommands/adoptManagedScripts.js";
|
|
12
|
+
import { runAppLinkLocalPackagesCommand } from "./appCommands/linkLocalPackages.js";
|
|
13
|
+
import { runAppReleaseCommand } from "./appCommands/release.js";
|
|
14
|
+
import { runAppUpdatePackagesCommand } from "./appCommands/updatePackages.js";
|
|
15
|
+
import { runAppVerifyCommand } from "./appCommands/verify.js";
|
|
16
|
+
|
|
17
|
+
function renderAppHelp(stream, definition = null) {
|
|
18
|
+
const color = createColorFormatter(stream);
|
|
19
|
+
const lines = [];
|
|
20
|
+
|
|
21
|
+
if (!definition) {
|
|
22
|
+
lines.push(`Command: ${color.emphasis("app")}`);
|
|
23
|
+
lines.push("");
|
|
24
|
+
lines.push(color.heading("1) Minimal use"));
|
|
25
|
+
lines.push(" jskit app <subcommand>");
|
|
26
|
+
lines.push("");
|
|
27
|
+
lines.push(color.heading("2) Subcommands"));
|
|
28
|
+
for (const entry of listAppCommandDefinitions()) {
|
|
29
|
+
lines.push(` - ${color.item(entry.name)}: ${entry.summary}`);
|
|
30
|
+
}
|
|
31
|
+
lines.push("");
|
|
32
|
+
lines.push(color.heading("3) Notes"));
|
|
33
|
+
lines.push(" - The scaffold keeps npm run shortcuts such as verify and jskit:update,");
|
|
34
|
+
lines.push(" but the maintained behavior lives here in jskit app.");
|
|
35
|
+
lines.push(" - Use jskit app <subcommand> help for subcommand-specific usage.");
|
|
36
|
+
lines.push("");
|
|
37
|
+
lines.push(color.heading("4) Scaffold wrappers"));
|
|
38
|
+
for (const [scriptName, scriptCommand] of Object.entries(APP_SCRIPT_WRAPPERS)) {
|
|
39
|
+
lines.push(` - ${scriptName}: ${scriptCommand}`);
|
|
40
|
+
}
|
|
41
|
+
writeWrappedLines({
|
|
42
|
+
stdout: stream,
|
|
43
|
+
lines
|
|
44
|
+
});
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
lines.push(`App subcommand: ${color.emphasis(definition.name)}`);
|
|
49
|
+
lines.push("");
|
|
50
|
+
lines.push(color.heading("1) Summary"));
|
|
51
|
+
lines.push(` ${definition.summary}`);
|
|
52
|
+
lines.push("");
|
|
53
|
+
lines.push(color.heading("2) Use"));
|
|
54
|
+
lines.push(` ${definition.usage}`);
|
|
55
|
+
|
|
56
|
+
if (definition.options.length > 0) {
|
|
57
|
+
lines.push("");
|
|
58
|
+
lines.push(color.heading("3) Options"));
|
|
59
|
+
for (const optionRow of definition.options) {
|
|
60
|
+
lines.push(` - ${optionRow.label}: ${optionRow.description}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (definition.defaults.length > 0) {
|
|
65
|
+
lines.push("");
|
|
66
|
+
lines.push(color.heading(definition.options.length > 0 ? "4) Defaults" : "3) Defaults"));
|
|
67
|
+
for (const defaultLine of definition.defaults) {
|
|
68
|
+
lines.push(` - ${defaultLine}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
writeWrappedLines({
|
|
73
|
+
stdout: stream,
|
|
74
|
+
lines
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function createAppCommands(ctx = {}) {
|
|
79
|
+
const {
|
|
80
|
+
createCliError,
|
|
81
|
+
resolveAppRootFromCwd
|
|
82
|
+
} = ctx;
|
|
83
|
+
|
|
84
|
+
async function commandApp({ positional = [], options = {}, cwd = "", stdout, stderr }) {
|
|
85
|
+
const appRoot = await resolveAppRootFromCwd(cwd);
|
|
86
|
+
const firstToken = String(positional[0] || "").trim();
|
|
87
|
+
const secondToken = String(positional[1] || "").trim();
|
|
88
|
+
|
|
89
|
+
if (!firstToken) {
|
|
90
|
+
renderAppHelp(stdout);
|
|
91
|
+
return 0;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (firstToken === "help") {
|
|
95
|
+
renderAppHelp(stdout, resolveAppCommandDefinition(secondToken));
|
|
96
|
+
return 0;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const definition = resolveAppCommandDefinition(firstToken);
|
|
100
|
+
if (!definition) {
|
|
101
|
+
throw createCliError(`Unknown app subcommand: ${firstToken}.`, {
|
|
102
|
+
renderUsage: () => renderAppHelp(stderr)
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (secondToken === "help") {
|
|
107
|
+
renderAppHelp(stdout, definition);
|
|
108
|
+
return 0;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const optionMeta = buildAppCommandOptionMeta(definition.name);
|
|
112
|
+
const supportedOptionNames = new Set(Object.keys(optionMeta));
|
|
113
|
+
const inlineOptionNames = Object.keys(options?.inlineOptions && typeof options.inlineOptions === "object" ? options.inlineOptions : {});
|
|
114
|
+
const unknownInlineOptionNames = inlineOptionNames.filter((optionName) => !supportedOptionNames.has(optionName));
|
|
115
|
+
if (unknownInlineOptionNames.length > 0) {
|
|
116
|
+
throw createCliError(
|
|
117
|
+
`Unknown option${unknownInlineOptionNames.length === 1 ? "" : "s"} for jskit app ${definition.name}: ${unknownInlineOptionNames.map((optionName) => `--${optionName}`).join(", ")}.`,
|
|
118
|
+
{
|
|
119
|
+
renderUsage: () => renderAppHelp(stderr, definition)
|
|
120
|
+
}
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
if (options?.dryRun === true && !supportedOptionNames.has("dry-run")) {
|
|
124
|
+
throw createCliError(`Unknown option for jskit app ${definition.name}: --dry-run.`, {
|
|
125
|
+
renderUsage: () => renderAppHelp(stderr, definition)
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (positional.length > 1) {
|
|
130
|
+
throw createCliError(`Unexpected positional arguments for jskit app ${definition.name}: ${positional.slice(1).join(" ")}`, {
|
|
131
|
+
renderUsage: () => renderAppHelp(stderr, definition)
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (definition.name === "verify") {
|
|
136
|
+
return runAppVerifyCommand(ctx, { appRoot, options, stdout, stderr });
|
|
137
|
+
}
|
|
138
|
+
if (definition.name === "update-packages") {
|
|
139
|
+
return runAppUpdatePackagesCommand(ctx, { appRoot, options, stdout, stderr });
|
|
140
|
+
}
|
|
141
|
+
if (definition.name === "link-local-packages") {
|
|
142
|
+
return runAppLinkLocalPackagesCommand(ctx, { appRoot, options, stdout, stderr });
|
|
143
|
+
}
|
|
144
|
+
if (definition.name === "release") {
|
|
145
|
+
return runAppReleaseCommand(ctx, { appRoot, options, stdout, stderr });
|
|
146
|
+
}
|
|
147
|
+
if (definition.name === "adopt-managed-scripts") {
|
|
148
|
+
return runAppAdoptManagedScriptsCommand(ctx, { appRoot, options, stdout, stderr });
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
throw createCliError(`Unhandled app subcommand: ${definition.name}.`, {
|
|
152
|
+
renderUsage: () => renderAppHelp(stderr, definition)
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
commandApp
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export { createAppCommands };
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
const APP_SCRIPT_WRAPPERS = Object.freeze({
|
|
2
|
+
verify: "jskit app verify && npm run --if-present verify:app",
|
|
3
|
+
"jskit:update": "jskit app update-packages",
|
|
4
|
+
devlinks: "jskit app link-local-packages",
|
|
5
|
+
release: "jskit app release"
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
const LEGACY_APP_SCRIPT_VALUES = Object.freeze({
|
|
9
|
+
verify: Object.freeze([
|
|
10
|
+
"npm run lint && npm run test && npm run test:client && npm run build && npx jskit doctor"
|
|
11
|
+
]),
|
|
12
|
+
"jskit:update": Object.freeze([
|
|
13
|
+
"bash ./scripts/update-jskit-packages.sh"
|
|
14
|
+
]),
|
|
15
|
+
devlinks: Object.freeze([
|
|
16
|
+
"bash ./scripts/link-local-jskit-packages.sh"
|
|
17
|
+
]),
|
|
18
|
+
release: Object.freeze([
|
|
19
|
+
"bash ./scripts/release.sh"
|
|
20
|
+
])
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const LEGACY_APP_SCRIPT_FILES = Object.freeze([
|
|
24
|
+
"scripts/update-jskit-packages.sh",
|
|
25
|
+
"scripts/link-local-jskit-packages.sh",
|
|
26
|
+
"scripts/release.sh"
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
const APP_COMMAND_DEFINITIONS = Object.freeze({
|
|
30
|
+
verify: Object.freeze({
|
|
31
|
+
name: "verify",
|
|
32
|
+
summary: "Run the JSKIT baseline app verification flow.",
|
|
33
|
+
usage: "jskit app verify",
|
|
34
|
+
options: Object.freeze([]),
|
|
35
|
+
defaults: Object.freeze([
|
|
36
|
+
"Runs npm scripts lint, test, test:client, and build only when those scripts are present.",
|
|
37
|
+
"Runs jskit doctor after the normal app checks.",
|
|
38
|
+
"The scaffolded npm run verify wrapper can append npm run --if-present verify:app afterwards."
|
|
39
|
+
])
|
|
40
|
+
}),
|
|
41
|
+
"update-packages": Object.freeze({
|
|
42
|
+
name: "update-packages",
|
|
43
|
+
summary: "Update installed @jskit-ai dependencies and refresh managed migrations.",
|
|
44
|
+
usage: "jskit app update-packages [--registry <url>] [--dry-run]",
|
|
45
|
+
options: Object.freeze([
|
|
46
|
+
Object.freeze({
|
|
47
|
+
label: "--registry <url>",
|
|
48
|
+
description: "Use a custom npm registry when resolving and installing @jskit-ai packages."
|
|
49
|
+
}),
|
|
50
|
+
Object.freeze({
|
|
51
|
+
label: "--dry-run",
|
|
52
|
+
description: "Show the npm install plan without mutating package.json, lockfiles, or migrations."
|
|
53
|
+
})
|
|
54
|
+
]),
|
|
55
|
+
defaults: Object.freeze([
|
|
56
|
+
"Runtime and dev @jskit-ai dependencies are updated separately so package.json sections stay correct.",
|
|
57
|
+
"Each package is moved to the latest available major.x range and npm saves the resolved exact version.",
|
|
58
|
+
"Managed migrations are refreshed afterwards unless --dry-run is used."
|
|
59
|
+
])
|
|
60
|
+
}),
|
|
61
|
+
"link-local-packages": Object.freeze({
|
|
62
|
+
name: "link-local-packages",
|
|
63
|
+
summary: "Link local @jskit-ai workspace packages into the current app for live development.",
|
|
64
|
+
usage: "jskit app link-local-packages [--repo-root <path>]",
|
|
65
|
+
options: Object.freeze([
|
|
66
|
+
Object.freeze({
|
|
67
|
+
label: "--repo-root <path>",
|
|
68
|
+
description: "Explicit jskit-ai monorepo checkout to link from. If omitted, JSKIT_REPO_ROOT or nearby jskit-ai directories are used."
|
|
69
|
+
})
|
|
70
|
+
]),
|
|
71
|
+
defaults: Object.freeze([
|
|
72
|
+
"Links packages from both the monorepo packages/ and tooling/ directories.",
|
|
73
|
+
"Refreshes node_modules/.bin entries for linked packages that publish binaries.",
|
|
74
|
+
"Clears node_modules/.vite so Vite does not keep stale prebundled paths."
|
|
75
|
+
])
|
|
76
|
+
}),
|
|
77
|
+
release: Object.freeze({
|
|
78
|
+
name: "release",
|
|
79
|
+
summary: "Run the JSKIT release helper for an app repository.",
|
|
80
|
+
usage: "jskit app release [--registry <url>] [--dry-run]",
|
|
81
|
+
options: Object.freeze([
|
|
82
|
+
Object.freeze({
|
|
83
|
+
label: "--registry <url>",
|
|
84
|
+
description: "Use a custom npm registry for the internal package-refresh step before opening a release PR."
|
|
85
|
+
}),
|
|
86
|
+
Object.freeze({
|
|
87
|
+
label: "--dry-run",
|
|
88
|
+
description: "Preview the release flow without syncing main, updating packages, or opening a PR."
|
|
89
|
+
})
|
|
90
|
+
]),
|
|
91
|
+
defaults: Object.freeze([
|
|
92
|
+
"Requires a clean worktree, the main branch, git, npm, and gh auth.",
|
|
93
|
+
"Syncs local main, runs jskit app update-packages, commits resulting changes, opens a PR, merges it, then re-syncs local main.",
|
|
94
|
+
"If update-packages produces no changes, release exits without opening a PR."
|
|
95
|
+
])
|
|
96
|
+
}),
|
|
97
|
+
"adopt-managed-scripts": Object.freeze({
|
|
98
|
+
name: "adopt-managed-scripts",
|
|
99
|
+
summary: "Rewrite legacy scaffolded maintenance scripts to the modern jskit app wrappers.",
|
|
100
|
+
usage: "jskit app adopt-managed-scripts [--dry-run] [--force]",
|
|
101
|
+
options: Object.freeze([
|
|
102
|
+
Object.freeze({
|
|
103
|
+
label: "--dry-run",
|
|
104
|
+
description: "Preview the script rewrites without writing package.json."
|
|
105
|
+
}),
|
|
106
|
+
Object.freeze({
|
|
107
|
+
label: "--force",
|
|
108
|
+
description: "Replace customized script values too, and remove the legacy copied maintenance scripts if they exist."
|
|
109
|
+
})
|
|
110
|
+
]),
|
|
111
|
+
defaults: Object.freeze([
|
|
112
|
+
"Known scaffolded script values are rewritten automatically.",
|
|
113
|
+
"Customized script values are reported and left alone unless --force is used.",
|
|
114
|
+
"This command is the migration path for existing apps that still carry copied JSKIT maintenance scripts."
|
|
115
|
+
])
|
|
116
|
+
})
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
function listAppCommandDefinitions() {
|
|
120
|
+
return Object.values(APP_COMMAND_DEFINITIONS)
|
|
121
|
+
.sort((left, right) => left.name.localeCompare(right.name));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function resolveAppCommandDefinition(rawName = "") {
|
|
125
|
+
const normalizedName = String(rawName || "").trim();
|
|
126
|
+
if (!normalizedName) {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
return APP_COMMAND_DEFINITIONS[normalizedName] || null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function buildAppCommandOptionMeta(subcommandName = "") {
|
|
133
|
+
const definition = resolveAppCommandDefinition(subcommandName);
|
|
134
|
+
const optionMeta = {
|
|
135
|
+
help: { inputType: "flag" }
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
if (!definition) {
|
|
139
|
+
return optionMeta;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (
|
|
143
|
+
definition.name === "update-packages" ||
|
|
144
|
+
definition.name === "adopt-managed-scripts" ||
|
|
145
|
+
definition.name === "release"
|
|
146
|
+
) {
|
|
147
|
+
optionMeta["dry-run"] = { inputType: "flag" };
|
|
148
|
+
}
|
|
149
|
+
if (definition.name === "adopt-managed-scripts") {
|
|
150
|
+
optionMeta.force = { inputType: "flag" };
|
|
151
|
+
}
|
|
152
|
+
if (definition.name === "update-packages" || definition.name === "release") {
|
|
153
|
+
optionMeta.registry = { inputType: "text" };
|
|
154
|
+
}
|
|
155
|
+
if (definition.name === "link-local-packages") {
|
|
156
|
+
optionMeta["repo-root"] = { inputType: "text" };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return optionMeta;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export {
|
|
163
|
+
APP_SCRIPT_WRAPPERS,
|
|
164
|
+
LEGACY_APP_SCRIPT_VALUES,
|
|
165
|
+
LEGACY_APP_SCRIPT_FILES,
|
|
166
|
+
APP_COMMAND_DEFINITIONS,
|
|
167
|
+
listAppCommandDefinitions,
|
|
168
|
+
resolveAppCommandDefinition,
|
|
169
|
+
buildAppCommandOptionMeta
|
|
170
|
+
};
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { rm } from "node:fs/promises";
|
|
3
|
+
import {
|
|
4
|
+
APP_SCRIPT_WRAPPERS,
|
|
5
|
+
LEGACY_APP_SCRIPT_FILES,
|
|
6
|
+
LEGACY_APP_SCRIPT_VALUES
|
|
7
|
+
} from "../appCommandCatalog.js";
|
|
8
|
+
import { fileExists, isTruthyFlag } from "./shared.js";
|
|
9
|
+
|
|
10
|
+
function shouldRewriteScript(currentValue = "", scriptName = "", force = false) {
|
|
11
|
+
const desiredValue = APP_SCRIPT_WRAPPERS[scriptName];
|
|
12
|
+
const normalizedCurrentValue = String(currentValue || "").trim();
|
|
13
|
+
if (!normalizedCurrentValue) {
|
|
14
|
+
return {
|
|
15
|
+
rewrite: true,
|
|
16
|
+
reason: "missing"
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
if (normalizedCurrentValue === desiredValue) {
|
|
20
|
+
return {
|
|
21
|
+
rewrite: false,
|
|
22
|
+
reason: "already-current"
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
if ((LEGACY_APP_SCRIPT_VALUES[scriptName] || []).includes(normalizedCurrentValue)) {
|
|
26
|
+
return {
|
|
27
|
+
rewrite: true,
|
|
28
|
+
reason: "legacy"
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
if (force) {
|
|
32
|
+
return {
|
|
33
|
+
rewrite: true,
|
|
34
|
+
reason: "force"
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
rewrite: false,
|
|
39
|
+
reason: "customized"
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function runAppAdoptManagedScriptsCommand(ctx = {}, { appRoot = "", options = {}, stdout }) {
|
|
44
|
+
const {
|
|
45
|
+
loadAppPackageJson,
|
|
46
|
+
writeJsonFile
|
|
47
|
+
} = ctx;
|
|
48
|
+
|
|
49
|
+
const dryRun = options?.dryRun === true;
|
|
50
|
+
const force = isTruthyFlag(options?.inlineOptions?.force);
|
|
51
|
+
const {
|
|
52
|
+
packageJsonPath,
|
|
53
|
+
packageJson
|
|
54
|
+
} = await loadAppPackageJson(appRoot);
|
|
55
|
+
|
|
56
|
+
const packageJsonClone = JSON.parse(JSON.stringify(packageJson || {}));
|
|
57
|
+
const scripts = packageJsonClone.scripts && typeof packageJsonClone.scripts === "object"
|
|
58
|
+
? packageJsonClone.scripts
|
|
59
|
+
: {};
|
|
60
|
+
packageJsonClone.scripts = scripts;
|
|
61
|
+
|
|
62
|
+
const changedScripts = [];
|
|
63
|
+
const skippedScripts = [];
|
|
64
|
+
|
|
65
|
+
for (const [scriptName, desiredValue] of Object.entries(APP_SCRIPT_WRAPPERS)) {
|
|
66
|
+
const currentValue = String(scripts[scriptName] || "");
|
|
67
|
+
const rewritePolicy = shouldRewriteScript(currentValue, scriptName, force);
|
|
68
|
+
if (!rewritePolicy.rewrite) {
|
|
69
|
+
if (rewritePolicy.reason === "customized") {
|
|
70
|
+
skippedScripts.push({
|
|
71
|
+
scriptName,
|
|
72
|
+
currentValue
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
scripts[scriptName] = desiredValue;
|
|
79
|
+
changedScripts.push({
|
|
80
|
+
scriptName,
|
|
81
|
+
previousValue: currentValue,
|
|
82
|
+
nextValue: desiredValue,
|
|
83
|
+
reason: rewritePolicy.reason
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const removableLegacyFiles = [];
|
|
88
|
+
if (force) {
|
|
89
|
+
for (const relativePath of LEGACY_APP_SCRIPT_FILES) {
|
|
90
|
+
const absolutePath = path.join(appRoot, relativePath);
|
|
91
|
+
if (await fileExists(absolutePath)) {
|
|
92
|
+
removableLegacyFiles.push({
|
|
93
|
+
relativePath,
|
|
94
|
+
absolutePath
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (!dryRun && changedScripts.length > 0) {
|
|
101
|
+
await writeJsonFile(packageJsonPath, packageJsonClone);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (!dryRun && force) {
|
|
105
|
+
for (const { absolutePath } of removableLegacyFiles) {
|
|
106
|
+
await rm(absolutePath, { recursive: true, force: true });
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (changedScripts.length < 1 && skippedScripts.length < 1 && removableLegacyFiles.length < 1) {
|
|
111
|
+
stdout.write("[adopt-managed-scripts] package.json already uses the managed JSKIT wrappers.\n");
|
|
112
|
+
return 0;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
for (const record of changedScripts) {
|
|
116
|
+
stdout.write(`[adopt-managed-scripts] ${dryRun ? "would rewrite" : "rewrote"} script ${record.scriptName} (${record.reason}).\n`);
|
|
117
|
+
}
|
|
118
|
+
for (const record of skippedScripts) {
|
|
119
|
+
stdout.write(`[adopt-managed-scripts] kept customized script ${record.scriptName}: ${record.currentValue}\n`);
|
|
120
|
+
}
|
|
121
|
+
for (const record of removableLegacyFiles) {
|
|
122
|
+
stdout.write(`[adopt-managed-scripts] ${dryRun ? "would remove" : "removed"} ${record.relativePath}\n`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (skippedScripts.length > 0 && !force) {
|
|
126
|
+
stdout.write("[adopt-managed-scripts] rerun with --force to replace customized maintenance wrappers too.\n");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return 0;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export { runAppAdoptManagedScriptsCommand };
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { mkdir, rm, symlink } from "node:fs/promises";
|
|
3
|
+
import {
|
|
4
|
+
discoverLocalPackageMap,
|
|
5
|
+
fileExists,
|
|
6
|
+
linkPackageBinEntries,
|
|
7
|
+
resolveLocalRepoRoot,
|
|
8
|
+
resolveSymlinkType
|
|
9
|
+
} from "./shared.js";
|
|
10
|
+
|
|
11
|
+
async function runAppLinkLocalPackagesCommand(ctx = {}, { appRoot = "", options = {}, stdout }) {
|
|
12
|
+
const { createCliError } = ctx;
|
|
13
|
+
const explicitRepoRoot = String(options?.inlineOptions?.["repo-root"] || "").trim();
|
|
14
|
+
const scopeDirectory = path.join(appRoot, "node_modules", "@jskit-ai");
|
|
15
|
+
const viteCacheDirectory = path.join(appRoot, "node_modules", ".vite");
|
|
16
|
+
|
|
17
|
+
if (!(await fileExists(scopeDirectory))) {
|
|
18
|
+
throw createCliError(`[link-local] @jskit-ai scope not found at ${scopeDirectory} (run npm install first).`, {
|
|
19
|
+
exitCode: 1
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const repoRoot = await resolveLocalRepoRoot({ appRoot, explicitRepoRoot });
|
|
24
|
+
if (!repoRoot) {
|
|
25
|
+
throw createCliError("[link-local] no JSKIT repository found. Set JSKIT_REPO_ROOT or use --repo-root.", {
|
|
26
|
+
exitCode: 1
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (!(await fileExists(path.join(repoRoot, "packages"))) || !(await fileExists(path.join(repoRoot, "tooling")))) {
|
|
31
|
+
throw createCliError(`[link-local] JSKIT repo root is not valid: ${repoRoot}`, {
|
|
32
|
+
exitCode: 1
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const packageMap = await discoverLocalPackageMap(repoRoot);
|
|
37
|
+
let linkedCount = 0;
|
|
38
|
+
await mkdir(scopeDirectory, { recursive: true });
|
|
39
|
+
|
|
40
|
+
for (const [packageDirName, sourceDir] of [...packageMap.entries()].sort((left, right) => left[0].localeCompare(right[0]))) {
|
|
41
|
+
const targetPath = path.join(scopeDirectory, packageDirName);
|
|
42
|
+
await rm(targetPath, { recursive: true, force: true });
|
|
43
|
+
await symlink(sourceDir, targetPath, resolveSymlinkType());
|
|
44
|
+
stdout.write(`[link-local] linked @jskit-ai/${packageDirName} -> ${sourceDir}\n`);
|
|
45
|
+
await linkPackageBinEntries({
|
|
46
|
+
appRoot,
|
|
47
|
+
packageDirName,
|
|
48
|
+
sourceDir,
|
|
49
|
+
stdout
|
|
50
|
+
});
|
|
51
|
+
linkedCount += 1;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (await fileExists(viteCacheDirectory)) {
|
|
55
|
+
await rm(viteCacheDirectory, { recursive: true, force: true });
|
|
56
|
+
stdout.write(`[link-local] cleared Vite cache at ${viteCacheDirectory}\n`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
stdout.write(`[link-local] done. linked=${linkedCount}\n`);
|
|
60
|
+
return 0;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export { runAppLinkLocalPackagesCommand };
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { runAppUpdatePackagesCommand } from "./updatePackages.js";
|
|
2
|
+
import {
|
|
3
|
+
formatUtcReleaseTimestamp,
|
|
4
|
+
runExternalCommand
|
|
5
|
+
} from "./shared.js";
|
|
6
|
+
|
|
7
|
+
async function runAppReleaseCommand(ctx = {}, { appRoot = "", options = {}, stdout, stderr }) {
|
|
8
|
+
const { createCliError } = ctx;
|
|
9
|
+
|
|
10
|
+
runExternalCommand("gh", ["auth", "status"], {
|
|
11
|
+
cwd: appRoot,
|
|
12
|
+
stdout,
|
|
13
|
+
stderr,
|
|
14
|
+
quiet: true,
|
|
15
|
+
createCliError
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const worktreeStatus = runExternalCommand("git", ["status", "--porcelain"], {
|
|
19
|
+
cwd: appRoot,
|
|
20
|
+
stdout,
|
|
21
|
+
stderr,
|
|
22
|
+
quiet: true,
|
|
23
|
+
createCliError
|
|
24
|
+
});
|
|
25
|
+
if (String(worktreeStatus.stdout || "").trim()) {
|
|
26
|
+
throw createCliError("[release] worktree is not clean. Commit or stash changes first.", {
|
|
27
|
+
exitCode: 1
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const branchResult = runExternalCommand("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
|
|
32
|
+
cwd: appRoot,
|
|
33
|
+
stdout,
|
|
34
|
+
stderr,
|
|
35
|
+
quiet: true,
|
|
36
|
+
createCliError
|
|
37
|
+
});
|
|
38
|
+
const currentBranch = String(branchResult.stdout || "").trim();
|
|
39
|
+
if (currentBranch !== "main") {
|
|
40
|
+
throw createCliError(`[release] current branch is "${currentBranch}". Switch to main first.`, {
|
|
41
|
+
exitCode: 1
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (options?.dryRun) {
|
|
46
|
+
stdout.write("[release] dry-run mode: would sync main, run jskit app update-packages, and open a PR if changes appear.\n");
|
|
47
|
+
return 0;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
stdout.write("[release] syncing local main...\n");
|
|
51
|
+
runExternalCommand("git", ["fetch", "origin", "main"], {
|
|
52
|
+
cwd: appRoot,
|
|
53
|
+
stdout,
|
|
54
|
+
stderr,
|
|
55
|
+
createCliError
|
|
56
|
+
});
|
|
57
|
+
runExternalCommand("git", ["pull", "--ff-only", "origin", "main"], {
|
|
58
|
+
cwd: appRoot,
|
|
59
|
+
stdout,
|
|
60
|
+
stderr,
|
|
61
|
+
createCliError
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
stdout.write("[release] running package refresh...\n");
|
|
65
|
+
const registryUrl = String(options?.inlineOptions?.registry || "").trim();
|
|
66
|
+
await runAppUpdatePackagesCommand(ctx, {
|
|
67
|
+
appRoot,
|
|
68
|
+
options: {
|
|
69
|
+
...options,
|
|
70
|
+
dryRun: false,
|
|
71
|
+
inlineOptions: registryUrl
|
|
72
|
+
? { registry: registryUrl }
|
|
73
|
+
: {}
|
|
74
|
+
},
|
|
75
|
+
stdout,
|
|
76
|
+
stderr
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const postUpdateStatus = runExternalCommand("git", ["status", "--porcelain"], {
|
|
80
|
+
cwd: appRoot,
|
|
81
|
+
stdout,
|
|
82
|
+
stderr,
|
|
83
|
+
quiet: true,
|
|
84
|
+
createCliError
|
|
85
|
+
});
|
|
86
|
+
if (!String(postUpdateStatus.stdout || "").trim()) {
|
|
87
|
+
stdout.write("[release] no changes produced by jskit app update-packages. Nothing to release.\n");
|
|
88
|
+
return 0;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const { branchStamp, pretty } = formatUtcReleaseTimestamp();
|
|
92
|
+
const branchName = `release/${branchStamp}`;
|
|
93
|
+
const commitMessage = `chore: release ${pretty}`;
|
|
94
|
+
const prTitle = `Release ${pretty}`;
|
|
95
|
+
const prBody = "Automated release commit generated by `jskit app release`.";
|
|
96
|
+
|
|
97
|
+
stdout.write(`[release] creating branch ${branchName}...\n`);
|
|
98
|
+
runExternalCommand("git", ["switch", "-c", branchName], {
|
|
99
|
+
cwd: appRoot,
|
|
100
|
+
stdout,
|
|
101
|
+
stderr,
|
|
102
|
+
createCliError
|
|
103
|
+
});
|
|
104
|
+
runExternalCommand("git", ["add", "-A"], {
|
|
105
|
+
cwd: appRoot,
|
|
106
|
+
stdout,
|
|
107
|
+
stderr,
|
|
108
|
+
createCliError
|
|
109
|
+
});
|
|
110
|
+
runExternalCommand("git", ["commit", "-m", commitMessage], {
|
|
111
|
+
cwd: appRoot,
|
|
112
|
+
stdout,
|
|
113
|
+
stderr,
|
|
114
|
+
createCliError
|
|
115
|
+
});
|
|
116
|
+
runExternalCommand("git", ["push", "-u", "origin", branchName], {
|
|
117
|
+
cwd: appRoot,
|
|
118
|
+
stdout,
|
|
119
|
+
stderr,
|
|
120
|
+
createCliError
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
const prCreateResult = runExternalCommand("gh", [
|
|
124
|
+
"pr",
|
|
125
|
+
"create",
|
|
126
|
+
"--base",
|
|
127
|
+
"main",
|
|
128
|
+
"--head",
|
|
129
|
+
branchName,
|
|
130
|
+
"--title",
|
|
131
|
+
prTitle,
|
|
132
|
+
"--body",
|
|
133
|
+
prBody
|
|
134
|
+
], {
|
|
135
|
+
cwd: appRoot,
|
|
136
|
+
stdout,
|
|
137
|
+
stderr,
|
|
138
|
+
quiet: true,
|
|
139
|
+
createCliError
|
|
140
|
+
});
|
|
141
|
+
const prUrl = String(prCreateResult.stdout || "").trim();
|
|
142
|
+
if (!prUrl) {
|
|
143
|
+
throw createCliError("[release] gh pr create did not return a PR URL.", {
|
|
144
|
+
exitCode: 1
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
stdout.write(`[release] created PR: ${prUrl}\n`);
|
|
149
|
+
runExternalCommand("gh", ["pr", "merge", prUrl, "--merge", "--delete-branch"], {
|
|
150
|
+
cwd: appRoot,
|
|
151
|
+
stdout,
|
|
152
|
+
stderr,
|
|
153
|
+
createCliError
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
stdout.write("[release] merged. syncing local main...\n");
|
|
157
|
+
runExternalCommand("git", ["switch", "main"], {
|
|
158
|
+
cwd: appRoot,
|
|
159
|
+
stdout,
|
|
160
|
+
stderr,
|
|
161
|
+
createCliError
|
|
162
|
+
});
|
|
163
|
+
runExternalCommand("git", ["pull", "--ff-only", "origin", "main"], {
|
|
164
|
+
cwd: appRoot,
|
|
165
|
+
stdout,
|
|
166
|
+
stderr,
|
|
167
|
+
createCliError
|
|
168
|
+
});
|
|
169
|
+
stdout.write("[release] done.\n");
|
|
170
|
+
|
|
171
|
+
return 0;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export { runAppReleaseCommand };
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { access, mkdir, readFile, readdir, rm, symlink } from "node:fs/promises";
|
|
4
|
+
|
|
5
|
+
function normalizeText(value = "") {
|
|
6
|
+
return String(value || "").trim();
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
async function fileExists(filePath = "") {
|
|
10
|
+
try {
|
|
11
|
+
await access(filePath);
|
|
12
|
+
return true;
|
|
13
|
+
} catch {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function isTruthyFlag(rawValue = "") {
|
|
19
|
+
const normalizedValue = normalizeText(rawValue).toLowerCase();
|
|
20
|
+
return normalizedValue === "true" || normalizedValue === "1" || normalizedValue === "yes";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function ensureCommandSucceeded(result, label, { createCliError, cwd = "", stdout, stderr, quiet = false } = {}) {
|
|
24
|
+
if (!quiet) {
|
|
25
|
+
const capturedStdout = String(result?.stdout || "");
|
|
26
|
+
const capturedStderr = String(result?.stderr || "");
|
|
27
|
+
if (capturedStdout) {
|
|
28
|
+
stdout?.write(capturedStdout);
|
|
29
|
+
}
|
|
30
|
+
if (capturedStderr) {
|
|
31
|
+
stderr?.write(capturedStderr);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (result?.error) {
|
|
36
|
+
if (result.error.code === "ENOENT") {
|
|
37
|
+
throw createCliError(`${label} is not available in PATH.`, {
|
|
38
|
+
exitCode: 1
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
throw result.error;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (result?.status !== 0) {
|
|
45
|
+
const suffix = cwd ? ` (cwd: ${cwd})` : "";
|
|
46
|
+
throw createCliError(`${label} failed with exit code ${result?.status ?? 1}${suffix}.`, {
|
|
47
|
+
exitCode: Number.isInteger(result?.status) ? result.status : 1
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return result;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function runExternalCommand(
|
|
55
|
+
command,
|
|
56
|
+
args = [],
|
|
57
|
+
{
|
|
58
|
+
cwd = "",
|
|
59
|
+
env = {},
|
|
60
|
+
stdout,
|
|
61
|
+
stderr,
|
|
62
|
+
quiet = false,
|
|
63
|
+
createCliError
|
|
64
|
+
} = {}
|
|
65
|
+
) {
|
|
66
|
+
const result = spawnSync(command, Array.isArray(args) ? args : [], {
|
|
67
|
+
cwd: cwd || process.cwd(),
|
|
68
|
+
encoding: "utf8",
|
|
69
|
+
env: {
|
|
70
|
+
...process.env,
|
|
71
|
+
...env
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
return ensureCommandSucceeded(result, command, {
|
|
75
|
+
createCliError,
|
|
76
|
+
cwd,
|
|
77
|
+
stdout,
|
|
78
|
+
stderr,
|
|
79
|
+
quiet
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function formatUtcReleaseTimestamp(date = new Date()) {
|
|
84
|
+
const year = date.getUTCFullYear();
|
|
85
|
+
const month = String(date.getUTCMonth() + 1).padStart(2, "0");
|
|
86
|
+
const day = String(date.getUTCDate()).padStart(2, "0");
|
|
87
|
+
const hours = String(date.getUTCHours()).padStart(2, "0");
|
|
88
|
+
const minutes = String(date.getUTCMinutes()).padStart(2, "0");
|
|
89
|
+
const seconds = String(date.getUTCSeconds()).padStart(2, "0");
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
branchStamp: `${year}${month}${day}-${hours}${minutes}${seconds}`,
|
|
93
|
+
pretty: `${year}-${month}-${day} ${hours}:${minutes} UTC`
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function resolveLocalJskitBin(appRoot = "") {
|
|
98
|
+
const binName = process.platform === "win32" ? "jskit.cmd" : "jskit";
|
|
99
|
+
return path.join(appRoot, "node_modules", ".bin", binName);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function runLocalJskit(
|
|
103
|
+
appRoot,
|
|
104
|
+
args = [],
|
|
105
|
+
{
|
|
106
|
+
stdout,
|
|
107
|
+
stderr,
|
|
108
|
+
createCliError,
|
|
109
|
+
quiet = false
|
|
110
|
+
} = {}
|
|
111
|
+
) {
|
|
112
|
+
const localJskitBin = resolveLocalJskitBin(appRoot);
|
|
113
|
+
if (!(await fileExists(localJskitBin))) {
|
|
114
|
+
throw createCliError(`Local jskit binary not found at ${path.relative(appRoot, localJskitBin)}. Run npm install first.`, {
|
|
115
|
+
exitCode: 1
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return runExternalCommand(localJskitBin, args, {
|
|
120
|
+
cwd: appRoot,
|
|
121
|
+
stdout,
|
|
122
|
+
stderr,
|
|
123
|
+
quiet,
|
|
124
|
+
createCliError
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function resolveLocalRepoRoot({ appRoot = "", explicitRepoRoot = "" } = {}) {
|
|
129
|
+
const explicit = normalizeText(explicitRepoRoot);
|
|
130
|
+
if (explicit) {
|
|
131
|
+
return explicit;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const envRepoRoot = normalizeText(process.env.JSKIT_REPO_ROOT);
|
|
135
|
+
if (envRepoRoot) {
|
|
136
|
+
return envRepoRoot;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
let currentDirectory = path.dirname(appRoot);
|
|
140
|
+
while (true) {
|
|
141
|
+
const candidateRoot = path.join(currentDirectory, "jskit-ai");
|
|
142
|
+
if (await fileExists(path.join(candidateRoot, "packages")) && await fileExists(path.join(candidateRoot, "tooling"))) {
|
|
143
|
+
return candidateRoot;
|
|
144
|
+
}
|
|
145
|
+
const parentDirectory = path.dirname(currentDirectory);
|
|
146
|
+
if (parentDirectory === currentDirectory) {
|
|
147
|
+
return "";
|
|
148
|
+
}
|
|
149
|
+
currentDirectory = parentDirectory;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function discoverLocalPackageMap(repoRoot = "") {
|
|
154
|
+
const packageMap = new Map();
|
|
155
|
+
const parentDirectories = [
|
|
156
|
+
path.join(repoRoot, "packages"),
|
|
157
|
+
path.join(repoRoot, "tooling")
|
|
158
|
+
];
|
|
159
|
+
|
|
160
|
+
for (const parentDirectory of parentDirectories) {
|
|
161
|
+
if (!(await fileExists(parentDirectory))) {
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const entries = await readdir(parentDirectory, { withFileTypes: true });
|
|
166
|
+
entries.sort((left, right) => left.name.localeCompare(right.name));
|
|
167
|
+
|
|
168
|
+
for (const entry of entries) {
|
|
169
|
+
if (!entry.isDirectory() || entry.name.startsWith(".")) {
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const packageRoot = path.join(parentDirectory, entry.name);
|
|
174
|
+
const packageJsonPath = path.join(packageRoot, "package.json");
|
|
175
|
+
if (!(await fileExists(packageJsonPath))) {
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
let packageJson = {};
|
|
180
|
+
try {
|
|
181
|
+
packageJson = JSON.parse(await readFile(packageJsonPath, "utf8"));
|
|
182
|
+
} catch {
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const packageId = normalizeText(packageJson?.name);
|
|
187
|
+
if (!packageId.startsWith("@jskit-ai/")) {
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const packageDirName = packageId.slice("@jskit-ai/".length);
|
|
192
|
+
if (!packageDirName || packageDirName.includes("/")) {
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (!packageMap.has(packageDirName)) {
|
|
197
|
+
packageMap.set(packageDirName, packageRoot);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return packageMap;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async function linkPackageBinEntries({
|
|
206
|
+
appRoot,
|
|
207
|
+
packageDirName,
|
|
208
|
+
sourceDir,
|
|
209
|
+
stdout
|
|
210
|
+
} = {}) {
|
|
211
|
+
const packageJsonPath = path.join(sourceDir, "package.json");
|
|
212
|
+
if (!(await fileExists(packageJsonPath))) {
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
let packageJson = {};
|
|
217
|
+
try {
|
|
218
|
+
packageJson = JSON.parse(await readFile(packageJsonPath, "utf8"));
|
|
219
|
+
} catch {
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const rawBin = packageJson?.bin;
|
|
224
|
+
let binEntries = [];
|
|
225
|
+
if (typeof rawBin === "string") {
|
|
226
|
+
binEntries = [[packageDirName, rawBin]];
|
|
227
|
+
} else if (rawBin && typeof rawBin === "object" && !Array.isArray(rawBin)) {
|
|
228
|
+
binEntries = Object.entries(rawBin);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (binEntries.length < 1) {
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const binDirectory = path.join(appRoot, "node_modules", ".bin");
|
|
236
|
+
const packageRoot = path.join(appRoot, "node_modules", "@jskit-ai", packageDirName);
|
|
237
|
+
await mkdir(binDirectory, { recursive: true });
|
|
238
|
+
|
|
239
|
+
for (const [rawBinName, rawBinTarget] of binEntries) {
|
|
240
|
+
const binName = normalizeText(rawBinName);
|
|
241
|
+
const binTarget = normalizeText(rawBinTarget);
|
|
242
|
+
if (!binName || !binTarget) {
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const absoluteTarget = path.join(packageRoot, binTarget);
|
|
247
|
+
if (!(await fileExists(absoluteTarget))) {
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const binPath = path.join(binDirectory, binName);
|
|
252
|
+
await rm(binPath, { recursive: true, force: true });
|
|
253
|
+
const relativeTarget = path.relative(binDirectory, absoluteTarget) || absoluteTarget;
|
|
254
|
+
await symlink(relativeTarget, binPath);
|
|
255
|
+
stdout?.write(`[link-local] linked bin ${binName} -> ${relativeTarget}\n`);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function resolveSymlinkType() {
|
|
260
|
+
return process.platform === "win32" ? "junction" : "dir";
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export {
|
|
264
|
+
fileExists,
|
|
265
|
+
normalizeText,
|
|
266
|
+
isTruthyFlag,
|
|
267
|
+
runExternalCommand,
|
|
268
|
+
formatUtcReleaseTimestamp,
|
|
269
|
+
resolveLocalJskitBin,
|
|
270
|
+
runLocalJskit,
|
|
271
|
+
resolveLocalRepoRoot,
|
|
272
|
+
discoverLocalPackageMap,
|
|
273
|
+
linkPackageBinEntries,
|
|
274
|
+
resolveSymlinkType
|
|
275
|
+
};
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { runLocalJskit, runExternalCommand } from "./shared.js";
|
|
2
|
+
|
|
3
|
+
function collectJskitPackageNames(packageMap = {}) {
|
|
4
|
+
return Object.keys(packageMap && typeof packageMap === "object" ? packageMap : {})
|
|
5
|
+
.filter((name) => String(name || "").startsWith("@jskit-ai/"))
|
|
6
|
+
.sort((left, right) => left.localeCompare(right));
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function resolveMajorRangeFromVersion(packageName = "", rawVersion = "", createCliError) {
|
|
10
|
+
const normalizedVersion = String(rawVersion || "").trim();
|
|
11
|
+
const match = normalizedVersion.match(/^(\d+)\.\d+\.\d+(?:[.+-][0-9A-Za-z.-]+)?$/u);
|
|
12
|
+
if (!match) {
|
|
13
|
+
throw createCliError(`Invalid latest version for ${packageName}: ${normalizedVersion || "<empty>"}.`, {
|
|
14
|
+
exitCode: 1
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
return `${match[1]}.x`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function resolveRegistryArgs(registryUrl = "") {
|
|
21
|
+
const normalizedRegistryUrl = String(registryUrl || "").trim();
|
|
22
|
+
if (!normalizedRegistryUrl) {
|
|
23
|
+
return [];
|
|
24
|
+
}
|
|
25
|
+
return ["--registry", normalizedRegistryUrl];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function resolveInstallSpecs(packageNames = [], resolveMajorRange) {
|
|
29
|
+
return packageNames.map((packageName) => `${packageName}@${resolveMajorRange(packageName)}`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function runAppUpdatePackagesCommand(ctx = {}, { appRoot = "", options = {}, stdout, stderr }) {
|
|
33
|
+
const {
|
|
34
|
+
createCliError,
|
|
35
|
+
loadAppPackageJson
|
|
36
|
+
} = ctx;
|
|
37
|
+
|
|
38
|
+
const { packageJson } = await loadAppPackageJson(appRoot);
|
|
39
|
+
const runtimePackages = collectJskitPackageNames(packageJson?.dependencies);
|
|
40
|
+
const devPackages = collectJskitPackageNames(packageJson?.devDependencies);
|
|
41
|
+
const registryUrl = String(options?.inlineOptions?.registry || "").trim();
|
|
42
|
+
const registryArgs = resolveRegistryArgs(registryUrl);
|
|
43
|
+
const dryRun = options?.dryRun === true;
|
|
44
|
+
|
|
45
|
+
if (runtimePackages.length < 1 && devPackages.length < 1) {
|
|
46
|
+
stdout.write("[jskit:update] no @jskit-ai packages found in dependencies.\n");
|
|
47
|
+
return 0;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const resolveMajorRange = (packageName) => {
|
|
51
|
+
const result = runExternalCommand("npm", ["view", ...registryArgs, packageName, "version"], {
|
|
52
|
+
cwd: appRoot,
|
|
53
|
+
stdout,
|
|
54
|
+
stderr,
|
|
55
|
+
quiet: true,
|
|
56
|
+
createCliError
|
|
57
|
+
});
|
|
58
|
+
return resolveMajorRangeFromVersion(packageName, result.stdout, createCliError);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const runtimeSpecs = resolveInstallSpecs(runtimePackages, resolveMajorRange);
|
|
62
|
+
const devSpecs = resolveInstallSpecs(devPackages, resolveMajorRange);
|
|
63
|
+
const dryRunArgs = dryRun ? ["--dry-run"] : [];
|
|
64
|
+
|
|
65
|
+
if (dryRun) {
|
|
66
|
+
stdout.write("[jskit:update] dry-run mode enabled.\n");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (runtimeSpecs.length > 0) {
|
|
70
|
+
stdout.write(`[jskit:update] updating runtime packages: ${runtimeSpecs.join(" ")}\n`);
|
|
71
|
+
runExternalCommand("npm", ["install", "--save-exact", ...registryArgs, ...dryRunArgs, ...runtimeSpecs], {
|
|
72
|
+
cwd: appRoot,
|
|
73
|
+
stdout,
|
|
74
|
+
stderr,
|
|
75
|
+
createCliError
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (devSpecs.length > 0) {
|
|
80
|
+
stdout.write(`[jskit:update] updating dev packages: ${devSpecs.join(" ")}\n`);
|
|
81
|
+
runExternalCommand("npm", ["install", "--save-dev", "--save-exact", ...registryArgs, ...dryRunArgs, ...devSpecs], {
|
|
82
|
+
cwd: appRoot,
|
|
83
|
+
stdout,
|
|
84
|
+
stderr,
|
|
85
|
+
createCliError
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (!dryRun) {
|
|
90
|
+
stdout.write("[jskit:update] generating managed migrations for changed packages.\n");
|
|
91
|
+
await runLocalJskit(appRoot, ["migrations", "changed"], {
|
|
92
|
+
stdout,
|
|
93
|
+
stderr,
|
|
94
|
+
createCliError
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
stdout.write("[jskit:update] done.\n");
|
|
99
|
+
return 0;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export { runAppUpdatePackagesCommand };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { runExternalCommand, runLocalJskit } from "./shared.js";
|
|
2
|
+
|
|
3
|
+
const BASELINE_VERIFY_SCRIPTS = Object.freeze([
|
|
4
|
+
"lint",
|
|
5
|
+
"test",
|
|
6
|
+
"test:client",
|
|
7
|
+
"build"
|
|
8
|
+
]);
|
|
9
|
+
|
|
10
|
+
async function runAppVerifyCommand(ctx = {}, { appRoot = "", options = {}, stdout, stderr }) {
|
|
11
|
+
const { createCliError } = ctx;
|
|
12
|
+
|
|
13
|
+
if (options?.dryRun) {
|
|
14
|
+
throw createCliError("jskit app verify does not support --dry-run.", { exitCode: 1 });
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
for (const scriptName of BASELINE_VERIFY_SCRIPTS) {
|
|
18
|
+
runExternalCommand("npm", ["run", "--if-present", scriptName], {
|
|
19
|
+
cwd: appRoot,
|
|
20
|
+
stdout,
|
|
21
|
+
stderr,
|
|
22
|
+
createCliError
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
await runLocalJskit(appRoot, ["doctor"], {
|
|
27
|
+
stdout,
|
|
28
|
+
stderr,
|
|
29
|
+
createCliError
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
return 0;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export { runAppVerifyCommand };
|
|
@@ -126,6 +126,47 @@ const COMMAND_DESCRIPTORS = Object.freeze({
|
|
|
126
126
|
inlineOptionMode: "enumerated",
|
|
127
127
|
allowedValueOptionNames: Object.freeze(["scope", "package-id", "description"])
|
|
128
128
|
}),
|
|
129
|
+
app: Object.freeze({
|
|
130
|
+
command: "app",
|
|
131
|
+
aliases: Object.freeze([]),
|
|
132
|
+
showInOverview: true,
|
|
133
|
+
summary: "Run JSKIT-managed app maintenance helpers.",
|
|
134
|
+
minimalUse: "jskit app verify",
|
|
135
|
+
parameters: Object.freeze([
|
|
136
|
+
Object.freeze({
|
|
137
|
+
name: "<subcommand>",
|
|
138
|
+
description: "verify | update-packages | link-local-packages | release | adopt-managed-scripts."
|
|
139
|
+
})
|
|
140
|
+
]),
|
|
141
|
+
defaults: Object.freeze([
|
|
142
|
+
"The scaffold keeps npm run shortcuts such as verify and jskit:update, but their maintained behavior lives under jskit app.",
|
|
143
|
+
"Use jskit app <subcommand> help for subcommand-specific usage.",
|
|
144
|
+
"--dry-run is accepted by update-packages, adopt-managed-scripts, and release."
|
|
145
|
+
]),
|
|
146
|
+
examples: Object.freeze([
|
|
147
|
+
Object.freeze({
|
|
148
|
+
label: "Scaffolded maintenance shortcuts",
|
|
149
|
+
lines: Object.freeze([
|
|
150
|
+
"jskit app verify",
|
|
151
|
+
"jskit app update-packages"
|
|
152
|
+
])
|
|
153
|
+
}),
|
|
154
|
+
Object.freeze({
|
|
155
|
+
label: "Existing app migration",
|
|
156
|
+
lines: Object.freeze([
|
|
157
|
+
"jskit app adopt-managed-scripts --dry-run",
|
|
158
|
+
"jskit app adopt-managed-scripts --force"
|
|
159
|
+
])
|
|
160
|
+
})
|
|
161
|
+
]),
|
|
162
|
+
fullUse: "jskit app <subcommand> [help] [--dry-run] [--<option> <value>...]",
|
|
163
|
+
showHelpOnBareInvocation: true,
|
|
164
|
+
handlerName: "commandApp",
|
|
165
|
+
allowedFlagKeys: Object.freeze(["dryRun"]),
|
|
166
|
+
inlineOptionMode: "delegate",
|
|
167
|
+
allowedValueOptionNames: Object.freeze([]),
|
|
168
|
+
canDelegateInlineOptions: (positional = []) => Array.isArray(positional) && positional.length > 0
|
|
169
|
+
}),
|
|
129
170
|
add: Object.freeze({
|
|
130
171
|
command: "add",
|
|
131
172
|
aliases: Object.freeze([]),
|
|
@@ -2,6 +2,7 @@ import { createCommandHandlerShared } from "../commandHandlers/shared.js";
|
|
|
2
2
|
import { createListCommands } from "../commandHandlers/list.js";
|
|
3
3
|
import { createShowCommand } from "../commandHandlers/show.js";
|
|
4
4
|
import { createPackageCommands } from "../commandHandlers/package.js";
|
|
5
|
+
import { createAppCommands } from "../commandHandlers/app.js";
|
|
5
6
|
import { createHealthCommands } from "../commandHandlers/health.js";
|
|
6
7
|
import { createCompletionCommands } from "../commandHandlers/completion.js";
|
|
7
8
|
|
|
@@ -23,6 +24,7 @@ function createCommandHandlers(deps = {}) {
|
|
|
23
24
|
commandPosition,
|
|
24
25
|
commandRemove
|
|
25
26
|
} = createPackageCommands(commandContext);
|
|
27
|
+
const { commandApp } = createAppCommands(commandContext);
|
|
26
28
|
const { commandDoctor, commandLintDescriptors } = createHealthCommands(commandContext);
|
|
27
29
|
const { commandCompletion } = createCompletionCommands(commandContext);
|
|
28
30
|
|
|
@@ -32,6 +34,7 @@ function createCommandHandlers(deps = {}) {
|
|
|
32
34
|
commandListLinkItems,
|
|
33
35
|
commandCompletion,
|
|
34
36
|
commandShow,
|
|
37
|
+
commandApp,
|
|
35
38
|
commandCreate,
|
|
36
39
|
commandAdd,
|
|
37
40
|
commandGenerate,
|