rainskills 0.1.19 → 0.1.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +34 -39
  2. package/SKILL.md +12 -10
  3. package/bin/rainskills-tools.js +114 -240
  4. package/bin/rainskills.js +134 -591
  5. package/install.sh +68 -94
  6. package/marketplace/rainskills/.claude-plugin/plugin.json +1 -1
  7. package/marketplace/rainskills/.codex-plugin/plugin.json +1 -1
  8. package/marketplace/rainskills/skills/rainskills/SKILL.md +13 -11
  9. package/package.json +2 -2
  10. package/rainbond-app-assistant/SKILL.md +116 -151
  11. package/rainbond-app-version-assistant/SKILL.md +98 -98
  12. package/rainbond-delivery-verifier/SKILL.md +98 -68
  13. package/rainbond-env-sync/SKILL.md +98 -69
  14. package/rainbond-fullstack-bootstrap/SKILL.md +101 -89
  15. package/rainbond-fullstack-bootstrap/modules/10-context-loading.md +1 -1
  16. package/rainbond-fullstack-troubleshooter/SKILL.md +98 -68
  17. package/rainbond-opensource-app-deploy/SKILL.md +102 -81
  18. package/rainbond-platform-installer/SKILL.md +4 -4
  19. package/rainbond-platform-installer/scripts/auto-update.js +0 -9
  20. package/rainbond-platform-installer/scripts/installed-version.js +1 -1
  21. package/rainbond-platform-installer/scripts/platform-installer.js +3 -89
  22. package/rainbond-platform-installer/scripts/runtime-state.js +14 -251
  23. package/rainbond-platform-installer/scripts/single-runtime.js +114 -0
  24. package/rainbond-platform-installer/scripts/user-message.js +3 -49
  25. package/rainbond-platform-installer/scripts/windows-client-config.js +0 -51
  26. package/rainbond-platform-installer/scripts/windows-onboarding.js +0 -3
  27. package/rainbond-platform-installer/scripts/windows-platform.ps1 +1 -1
  28. package/rainbond-platform-query/SKILL.md +103 -61
  29. package/rainbond-project-init/SKILL.md +102 -79
  30. package/rainbond-template-installer/SKILL.md +102 -80
  31. package/rainbond-platform-installer/scripts/environment-credentials.js +0 -225
  32. package/rainbond-platform-installer/scripts/environment-registry.js +0 -349
  33. package/rainbond-platform-installer/scripts/local-runtime-commands.js +0 -180
  34. package/rainbond-platform-installer/scripts/local-runtime.js +0 -58
  35. package/rainbond-platform-installer/scripts/runtime-context-resolver.js +0 -189
  36. package/rainbond-platform-installer/scripts/runtime-credentials.js +0 -163
  37. package/rainbond-platform-installer/scripts/runtime-intents.js +0 -386
  38. package/rainbond-platform-installer/scripts/runtime-operations.js +0 -597
  39. package/rainbond-platform-installer/scripts/windows-client-config.ps1 +0 -9
  40. package/rainbond-platform-installer/scripts/windows-read-user-environment.ps1 +0 -16
@@ -1,189 +0,0 @@
1
- "use strict";
2
-
3
- const { createHash, randomUUID: nodeRandomUUID } = require("node:crypto");
4
-
5
- const MAX_OPTIONS = 256;
6
-
7
- function clone(value) {
8
- return JSON.parse(JSON.stringify(value));
9
- }
10
-
11
- function boundedString(value) {
12
- return typeof value === "string"
13
- && value.length > 0
14
- && value.length <= 128
15
- && !/[\u0000-\u001f\u007f-\u009f]/u.test(value)
16
- ? value
17
- : null;
18
- }
19
-
20
- function optionUuid(teamId, regionName) {
21
- const digest = createHash("sha256").update(`${teamId}\0${regionName}`, "utf8").digest("hex");
22
- return `${digest.slice(0, 8)}-${digest.slice(8, 12)}-4${digest.slice(13, 16)}-8${digest.slice(17, 20)}-${digest.slice(20, 32)}`;
23
- }
24
-
25
- function normalizeWorkspaceOptions(payload) {
26
- if (!payload || typeof payload !== "object" || !Array.isArray(payload.items)) {
27
- throw new Error("Rainbond 工作空间查询结果无效");
28
- }
29
- const options = [];
30
- for (const item of payload.items) {
31
- if (!item || typeof item !== "object") continue;
32
- const teamId = boundedString(item.team_id || item.tenant_id);
33
- const teamName = boundedString(item.team_name || item.tenant_name);
34
- const teamAlias = boundedString(item.team_alias || item.tenant_alias);
35
- if (!teamId || !teamName || !Array.isArray(item.region_list)) continue;
36
- for (const region of item.region_list) {
37
- const regionName = boundedString(region?.region_name);
38
- if (!regionName) continue;
39
- const regionLabel = boundedString(region.region_alias) || regionName;
40
- const workspaceLabel = teamAlias && teamAlias !== teamName
41
- ? `${teamAlias}(${teamName})`
42
- : teamName;
43
- options.push({
44
- id: optionUuid(teamId, regionName),
45
- label: `${workspaceLabel} / ${regionLabel}`,
46
- team_id: teamId,
47
- team_name: teamName,
48
- team_alias: teamAlias,
49
- region_name: regionName,
50
- });
51
- if (options.length > MAX_OPTIONS) throw new Error("candidate-set-too-large");
52
- }
53
- }
54
- return options.sort((left, right) => (
55
- left.team_id.localeCompare(right.team_id) || left.region_name.localeCompare(right.region_name)
56
- ));
57
- }
58
-
59
- function createRuntimeContextResolver({
60
- operationStore,
61
- queryTool,
62
- randomUUID = nodeRandomUUID,
63
- } = {}) {
64
- if (!operationStore || typeof operationStore.read !== "function"
65
- || typeof operationStore.updateContext !== "function") {
66
- throw new Error("runtime context resolver 缺少 operation store");
67
- }
68
- if (typeof queryTool !== "function") throw new Error("runtime context resolver 缺少只读查询 transport");
69
-
70
- async function resolve({ operationId, required = [] } = {}) {
71
- if (!Array.isArray(required) || required.some((item) => !["enterprise", "workspace"].includes(item))) {
72
- throw new Error("runtime context required dimensions 无效");
73
- }
74
- let operation = operationStore.read(operationId);
75
- if (!operation) throw new Error("runtime operation 不存在");
76
- if (operation.pending_selection) {
77
- return {
78
- schema: "rainskills.context-result.v1",
79
- state: "needs-selection",
80
- selection_id: operation.pending_selection.selection_id,
81
- dimension: operation.pending_selection.dimension,
82
- options: operation.pending_selection.options.map(({ id, label }) => ({ id, label })),
83
- };
84
- }
85
-
86
- if (required.includes("enterprise") && !operation.context.enterprise_id) {
87
- const identity = await queryTool("rainbond_get_current_user", {});
88
- const enterpriseId = boundedString(identity?.enterprise_id);
89
- if (!enterpriseId) {
90
- return {
91
- schema: "rainskills.context-result.v1",
92
- state: "blocked",
93
- reason: "no-current-enterprise",
94
- message_id: "context.no-current-enterprise",
95
- };
96
- }
97
- operation = operationStore.updateContext(operationId, {
98
- expectedRevision: operation.context_revision,
99
- values: { enterprise_id: enterpriseId },
100
- });
101
- }
102
-
103
- const workspaceResolved = operation.context.team_id && operation.context.region_name;
104
- if (required.includes("workspace") && !workspaceResolved) {
105
- const teams = await queryTool("rainbond_query_teams", {
106
- enterprise_id: operation.context.enterprise_id,
107
- });
108
- const options = normalizeWorkspaceOptions(teams);
109
- if (options.length === 0) {
110
- return {
111
- schema: "rainskills.context-result.v1",
112
- state: "blocked",
113
- reason: "no-accessible-workspace",
114
- message_id: "context.no-accessible-workspace",
115
- };
116
- }
117
- if (options.length === 1) {
118
- const [option] = options;
119
- operation = operationStore.updateContext(operationId, {
120
- expectedRevision: operation.context_revision,
121
- values: {
122
- team_id: option.team_id,
123
- team_name: option.team_name,
124
- region_name: option.region_name,
125
- },
126
- pendingSelection: null,
127
- });
128
- } else {
129
- const pendingSelection = {
130
- selection_id: randomUUID(),
131
- dimension: "workspace-region",
132
- options,
133
- context_revision: operation.context_revision + 1,
134
- };
135
- operationStore.updateContext(operationId, {
136
- expectedRevision: operation.context_revision,
137
- pendingSelection,
138
- });
139
- return {
140
- schema: "rainskills.context-result.v1",
141
- state: "needs-selection",
142
- selection_id: pendingSelection.selection_id,
143
- dimension: pendingSelection.dimension,
144
- options: options.map(({ id, label }) => ({ id, label })),
145
- };
146
- }
147
- }
148
-
149
- return {
150
- schema: "rainskills.context-result.v1",
151
- state: "resolved",
152
- context: clone(operation.context),
153
- context_revision: operation.context_revision,
154
- };
155
- }
156
-
157
- async function select({ operationId, selectionId, optionId } = {}) {
158
- const operation = operationStore.read(operationId);
159
- const pending = operation?.pending_selection;
160
- if (!pending || pending.selection_id !== selectionId
161
- || pending.context_revision !== operation.context_revision) {
162
- throw new Error("runtime context selection 已失效");
163
- }
164
- const option = pending.options.find((candidate) => candidate.id === optionId);
165
- if (!option) throw new Error("runtime context option 无效");
166
- const updated = operationStore.updateContext(operationId, {
167
- expectedRevision: operation.context_revision,
168
- values: {
169
- team_id: option.team_id,
170
- team_name: option.team_name,
171
- region_name: option.region_name,
172
- },
173
- pendingSelection: null,
174
- });
175
- return {
176
- schema: "rainskills.context-result.v1",
177
- state: "resolved",
178
- context: clone(updated.context),
179
- context_revision: updated.context_revision,
180
- };
181
- }
182
-
183
- return { resolve, select };
184
- }
185
-
186
- module.exports = {
187
- createRuntimeContextResolver,
188
- normalizeWorkspaceOptions,
189
- };
@@ -1,163 +0,0 @@
1
- "use strict";
2
-
3
- const crypto = require("node:crypto");
4
- const fs = require("node:fs");
5
- const os = require("node:os");
6
- const path = require("node:path");
7
- const { spawnSync } = require("node:child_process");
8
-
9
- const { normalizeConsoleOrigin } = require("./console-origin.js");
10
- const { createSecureStateStore } = require("./secure-state.js");
11
- const { looksLikeJwt } = require("./windows-auth.js");
12
-
13
- const MAX_CREDENTIAL_FILE_BYTES = 16384;
14
-
15
- function assertProtectedPosixDirectory(directory) {
16
- const info = fs.lstatSync(directory);
17
- if (info.isSymbolicLink() || !info.isDirectory()) {
18
- throw new Error("Rainbond 凭据目录必须是受保护的普通目录");
19
- }
20
- if (typeof process.getuid === "function" && info.uid !== process.getuid()) {
21
- throw new Error("Rainbond 凭据目录 owner 不匹配");
22
- }
23
- if ((info.mode & 0o777) !== 0o700) {
24
- throw new Error("Rainbond 凭据目录权限必须为 0700");
25
- }
26
- }
27
-
28
- function validateCredentialPayload(payload, expectedOrigin) {
29
- if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
30
- throw new Error("Rainbond 运行凭据无效");
31
- }
32
- if (Object.keys(payload).sort().join(",") !== "origin,token") {
33
- throw new Error("Rainbond 运行凭据字段无效");
34
- }
35
- if (!looksLikeJwt(payload.token)) throw new Error("Rainbond 运行凭据格式无效");
36
- const origin = normalizeConsoleOrigin(payload.origin);
37
- if (origin !== normalizeConsoleOrigin(expectedOrigin)) {
38
- throw new Error("Rainbond 运行凭据 origin 与 onboarding 不匹配");
39
- }
40
- return { token: payload.token, origin };
41
- }
42
-
43
- function readPosixRuntimeCredential({
44
- home = os.homedir(),
45
- expectedOrigin,
46
- stateStore = createSecureStateStore({ platform: process.platform, home }),
47
- } = {}) {
48
- const directory = path.join(home, ".rainbond");
49
- const credentialPath = path.join(directory, "mcp.env");
50
- assertProtectedPosixDirectory(directory);
51
- stateStore.assertProtectedRegularFile(credentialPath);
52
- const flags = fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0);
53
- const fd = fs.openSync(credentialPath, flags);
54
- let content;
55
- try {
56
- const info = fs.fstatSync(fd);
57
- if (!info.isFile()) throw new Error("Rainbond 凭据路径不是普通文件");
58
- if (typeof process.getuid === "function" && info.uid !== process.getuid()) {
59
- throw new Error("Rainbond 凭据文件 owner 不匹配");
60
- }
61
- if ((info.mode & 0o777) !== 0o600) throw new Error("Rainbond 凭据文件权限必须为 0600");
62
- if (info.size > MAX_CREDENTIAL_FILE_BYTES) throw new Error("Rainbond 凭据文件过大");
63
- content = fs.readFileSync(fd, "utf8");
64
- } finally {
65
- fs.closeSync(fd);
66
- }
67
- const match = content.match(
68
- /^export RAINBOND_JWT='([A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+)'\nexport RAINBOND_URL='([^'\r\n]+)'\n?$/
69
- );
70
- if (!match) throw new Error("Rainbond 凭据文件语法无效");
71
- return validateCredentialPayload({ token: match[1], origin: match[2] }, expectedOrigin);
72
- }
73
-
74
- function windowsReaderEnvironment(outputPath, source = process.env) {
75
- const environment = { RAINSKILLS_CREDENTIAL_OUTPUT_PATH: outputPath };
76
- for (const key of [
77
- "SystemRoot", "WINDIR", "PATH", "COMSPEC", "TEMP", "TMP", "USERPROFILE",
78
- ]) {
79
- if (typeof source[key] === "string") environment[key] = source[key];
80
- }
81
- return environment;
82
- }
83
-
84
- function readWindowsRuntimeCredential({
85
- home = process.env.USERPROFILE || os.homedir(),
86
- expectedOrigin,
87
- stateStore,
88
- spawnImpl = spawnSync,
89
- helperPath = path.join(__dirname, "windows-read-user-environment.ps1"),
90
- randomUUID = crypto.randomUUID,
91
- } = {}) {
92
- const store = stateStore || require("./windows-platform.js")
93
- .createWindowsSecureStateStore({ home });
94
- const directory = store.ensurePrivateDirectory(path.join(home, ".rainbond", "rainskills"));
95
- const outputPath = store.assertInsideHome(
96
- path.join(directory, `.credential-read-${randomUUID()}.json`),
97
- "凭据读取文件"
98
- );
99
- let fd;
100
- try {
101
- fd = fs.openSync(outputPath, "wx", 0o600);
102
- fs.closeSync(fd);
103
- fd = undefined;
104
- store.protectRegularFile(outputPath);
105
- const result = spawnImpl("powershell.exe", [
106
- "-NoProfile",
107
- "-NonInteractive",
108
- "-File",
109
- helperPath,
110
- ], {
111
- env: windowsReaderEnvironment(outputPath),
112
- stdio: ["ignore", "ignore", "ignore"],
113
- windowsHide: true,
114
- });
115
- if (result?.error || result?.signal || result?.status !== 0) {
116
- throw new Error("无法安全读取 Windows 用户级 Rainbond 凭据");
117
- }
118
- store.assertProtectedRegularFile(outputPath);
119
- const info = fs.lstatSync(outputPath);
120
- if (info.size > MAX_CREDENTIAL_FILE_BYTES) throw new Error("Rainbond 凭据文件过大");
121
- let payload;
122
- try {
123
- payload = JSON.parse(fs.readFileSync(outputPath, "utf8"));
124
- } catch {
125
- throw new Error("Windows 用户级 Rainbond 凭据无效");
126
- }
127
- return validateCredentialPayload(payload, expectedOrigin);
128
- } finally {
129
- if (fd !== undefined) fs.closeSync(fd);
130
- try {
131
- fs.unlinkSync(outputPath);
132
- } catch (error) {
133
- if (error.code !== "ENOENT") throw error;
134
- }
135
- }
136
- }
137
-
138
- function readRuntimeCredential({
139
- platform = process.platform,
140
- environmentId,
141
- environmentCredentialStore,
142
- ...options
143
- } = {}) {
144
- if (environmentId !== undefined) {
145
- const store = environmentCredentialStore || require("./environment-credentials.js")
146
- .createEnvironmentCredentialStore({ platform, ...options });
147
- return store.read({
148
- environmentId,
149
- expectedOrigin: options.expectedOrigin,
150
- });
151
- }
152
- return platform === "win32"
153
- ? readWindowsRuntimeCredential(options)
154
- : readPosixRuntimeCredential(options);
155
- }
156
-
157
- module.exports = {
158
- readPosixRuntimeCredential,
159
- readRuntimeCredential,
160
- readWindowsRuntimeCredential,
161
- validateCredentialPayload,
162
- windowsReaderEnvironment,
163
- };