@jskit-ai/jskit-cli 0.2.108 → 0.2.110

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.108",
3
+ "version": "0.2.110",
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.103",
24
- "@jskit-ai/kernel": "0.1.95",
25
- "@jskit-ai/shell-web": "0.1.94",
23
+ "@jskit-ai/jskit-catalog": "0.1.105",
24
+ "@jskit-ai/kernel": "0.1.98",
25
+ "@jskit-ai/shell-web": "0.1.97",
26
26
  "@vue/compiler-sfc": "^3.5.29",
27
27
  "ts-morph": "^28.0.0"
28
28
  },
@@ -10,6 +10,7 @@ import {
10
10
  } from "./appCommandCatalog.js";
11
11
  import { runAppAdoptManagedScriptsCommand } from "./appCommands/adoptManagedScripts.js";
12
12
  import { runAppLinkLocalPackagesCommand } from "./appCommands/linkLocalPackages.js";
13
+ import { runAppPreparePreviewUserCommand } from "./appCommands/preparePreviewUser.js";
13
14
  import { runAppReleaseCommand } from "./appCommands/release.js";
14
15
  import { runAppUpdatePackagesCommand } from "./appCommands/updatePackages.js";
15
16
  import { runAppVerifyCommand } from "./appCommands/verify.js";
@@ -151,6 +152,9 @@ function createAppCommands(ctx = {}) {
151
152
  if (definition.name === "adopt-managed-scripts") {
152
153
  return runAppAdoptManagedScriptsCommand(ctx, { appRoot, options, stdout, stderr });
153
154
  }
155
+ if (definition.name === "prepare-preview-user") {
156
+ return runAppPreparePreviewUserCommand(ctx, { appRoot, options, stdout, stderr });
157
+ }
154
158
 
155
159
  throw createCliError(`Unhandled app subcommand: ${definition.name}.`, {
156
160
  renderUsage: () => renderAppHelp(stderr, definition)
@@ -109,6 +109,46 @@ const APP_COMMAND_DEFINITIONS = Object.freeze({
109
109
  "Clears node_modules/.vite so Vite does not keep stale prebundled paths."
110
110
  ])
111
111
  }),
112
+ "prepare-preview-user": Object.freeze({
113
+ name: "prepare-preview-user",
114
+ summary: "Prepare the JSKIT preview auth user and optional first workspace.",
115
+ usage: "jskit app prepare-preview-user [--profile-file <path>] [--email <email>] [--username <username>] [--display-name <name>] [--auth-provider <provider>] [--auth-provider-user-sid <sid>] [--ensure-workspace]",
116
+ options: Object.freeze([
117
+ Object.freeze({
118
+ label: "--profile-file <path>",
119
+ description: "JSON profile path to write for the preview auth bridge. Defaults to VIBE64_PREVIEW_AUTH_PROFILE_FILE."
120
+ }),
121
+ Object.freeze({
122
+ label: "--email <email>",
123
+ description: "Email address for the preview user."
124
+ }),
125
+ Object.freeze({
126
+ label: "--username <username>",
127
+ description: "Preferred username for the preview user."
128
+ }),
129
+ Object.freeze({
130
+ label: "--display-name <name>",
131
+ description: "Display name for the preview user."
132
+ }),
133
+ Object.freeze({
134
+ label: "--auth-provider <provider>",
135
+ description: "Auth provider id stored on the preview user."
136
+ }),
137
+ Object.freeze({
138
+ label: "--auth-provider-user-sid <sid>",
139
+ description: "Provider user id stored on the preview user."
140
+ }),
141
+ Object.freeze({
142
+ label: "--ensure-workspace",
143
+ description: "When workspace tables and tenancy config exist, ensure the preview user owns a first workspace."
144
+ })
145
+ ]),
146
+ defaults: Object.freeze([
147
+ "Runs after migrations and before the app server starts.",
148
+ "Exits successfully without changes when the app has no knexfile.js or users table.",
149
+ "Uses the app knexfile and app-local JSKIT package provisioning contracts instead of booting the HTTP server."
150
+ ])
151
+ }),
112
152
  release: Object.freeze({
113
153
  name: "release",
114
154
  summary: "Run the JSKIT release helper for an app repository.",
@@ -198,6 +238,15 @@ function buildAppCommandOptionMeta(subcommandName = "") {
198
238
  if (definition.name === "link-local-packages") {
199
239
  optionMeta["repo-root"] = { inputType: "text" };
200
240
  }
241
+ if (definition.name === "prepare-preview-user") {
242
+ optionMeta["profile-file"] = { inputType: "text" };
243
+ optionMeta.email = { inputType: "text" };
244
+ optionMeta.username = { inputType: "text" };
245
+ optionMeta["display-name"] = { inputType: "text" };
246
+ optionMeta["auth-provider"] = { inputType: "text" };
247
+ optionMeta["auth-provider-user-sid"] = { inputType: "text" };
248
+ optionMeta["ensure-workspace"] = { inputType: "flag" };
249
+ }
201
250
 
202
251
  return optionMeta;
203
252
  }
@@ -0,0 +1,215 @@
1
+ import { access, mkdir, writeFile } from "node:fs/promises";
2
+ import { createRequire } from "node:module";
3
+ import path from "node:path";
4
+ import { pathToFileURL } from "node:url";
5
+ import { loadAppConfigFromAppRoot } from "@jskit-ai/kernel/server/support";
6
+
7
+ const TENANCY_MODE_NONE = "none";
8
+
9
+ function normalizeText(value = "") {
10
+ return String(value || "").trim();
11
+ }
12
+
13
+ function normalizeLowerText(value = "") {
14
+ return normalizeText(value).toLowerCase();
15
+ }
16
+
17
+ function profileFromOptions(options = {}) {
18
+ const inlineOptions = options?.inlineOptions && typeof options.inlineOptions === "object" ? options.inlineOptions : {};
19
+ const profile = {};
20
+
21
+ const email = normalizeLowerText(inlineOptions.email || process.env.JSKIT_PREVIEW_USER_EMAIL);
22
+ const authProvider = normalizeLowerText(inlineOptions["auth-provider"] || process.env.JSKIT_PREVIEW_AUTH_PROVIDER);
23
+ const username = normalizeLowerText(inlineOptions.username || process.env.JSKIT_PREVIEW_USER_USERNAME);
24
+ const displayName = normalizeText(inlineOptions["display-name"] || process.env.JSKIT_PREVIEW_USER_DISPLAY_NAME);
25
+ const authProviderUserSid = normalizeText(
26
+ inlineOptions["auth-provider-user-sid"] || process.env.JSKIT_PREVIEW_AUTH_PROVIDER_USER_SID
27
+ );
28
+
29
+ if (authProvider) {
30
+ profile.authProvider = authProvider;
31
+ }
32
+ if (authProviderUserSid) {
33
+ profile.authProviderUserSid = authProviderUserSid;
34
+ }
35
+ if (displayName) {
36
+ profile.displayName = displayName;
37
+ }
38
+ if (email) {
39
+ profile.email = email;
40
+ }
41
+ if (username) {
42
+ profile.username = username;
43
+ }
44
+
45
+ return profile;
46
+ }
47
+
48
+ async function fileExists(filePath = "") {
49
+ try {
50
+ await access(filePath);
51
+ return true;
52
+ } catch {
53
+ return false;
54
+ }
55
+ }
56
+
57
+ function createAppRequire(appRoot = "") {
58
+ return createRequire(path.join(appRoot, "package.json"));
59
+ }
60
+
61
+ async function importFreshModule(filePath = "") {
62
+ return import(`${pathToFileURL(filePath).href}?mtime=${Date.now()}`);
63
+ }
64
+
65
+ async function loadKnexConfig(appRoot = "") {
66
+ const knexfilePath = path.join(appRoot, "knexfile.js");
67
+ if (!(await fileExists(knexfilePath))) {
68
+ return null;
69
+ }
70
+
71
+ const requireFromApp = createAppRequire(appRoot);
72
+ const moduleValue = requireFromApp("knex");
73
+ const createKnex =
74
+ typeof moduleValue === "function"
75
+ ? moduleValue
76
+ : typeof moduleValue?.default === "function"
77
+ ? moduleValue.default
78
+ : null;
79
+ if (!createKnex) {
80
+ throw new Error("App-local knex package resolved but did not expose a callable factory.");
81
+ }
82
+
83
+ const knexfileModule = await importFreshModule(knexfilePath);
84
+ const exported = knexfileModule.default || knexfileModule.config || knexfileModule;
85
+ const knexfile = typeof exported === "function" ? await exported() : exported;
86
+ const environment = normalizeText(process.env.NODE_ENV) || "development";
87
+
88
+ return {
89
+ createKnex,
90
+ knexConfig: knexfile?.[environment] || knexfile
91
+ };
92
+ }
93
+
94
+ async function importAppPackageExport(appRoot = "", specifier = "", { required = true } = {}) {
95
+ const requireFromApp = createAppRequire(appRoot);
96
+ let resolvedPath = "";
97
+ try {
98
+ resolvedPath = requireFromApp.resolve(specifier);
99
+ } catch {
100
+ if (!required) {
101
+ return null;
102
+ }
103
+ throw new Error(
104
+ `Unable to load app-local ${specifier}. Upgrade the installed JSKIT package that owns this preview provisioning contract.`
105
+ );
106
+ }
107
+ return importFreshModule(resolvedPath);
108
+ }
109
+
110
+ async function writeProfile(profileFile = "", authProfile = {}) {
111
+ if (!profileFile) {
112
+ return false;
113
+ }
114
+ await mkdir(path.dirname(profileFile), { recursive: true });
115
+ await writeFile(profileFile, `${JSON.stringify(authProfile, null, 2)}\n`, "utf8");
116
+ return true;
117
+ }
118
+
119
+ function isTrueOption(value) {
120
+ const normalized = normalizeLowerText(value);
121
+ return value === true || normalized === "true" || normalized === "1" || normalized === "yes";
122
+ }
123
+
124
+ function normalizeTenancyMode(value = "") {
125
+ const normalized = normalizeLowerText(value);
126
+ return normalized === "personal" || normalized === "workspaces" ? normalized : TENANCY_MODE_NONE;
127
+ }
128
+
129
+ async function hasWorkspaceTables(db) {
130
+ return (
131
+ (await db.schema.hasTable("workspaces")) &&
132
+ (await db.schema.hasTable("workspace_memberships")) &&
133
+ (await db.schema.hasTable("workspace_settings"))
134
+ );
135
+ }
136
+
137
+ async function runAppPreparePreviewUserCommand(_ctx = {}, { appRoot = "", options = {}, stdout = process.stdout }) {
138
+ const inlineOptions = options?.inlineOptions && typeof options.inlineOptions === "object" ? options.inlineOptions : {};
139
+ const profileFile = normalizeText(inlineOptions["profile-file"] || process.env.VIBE64_PREVIEW_AUTH_PROFILE_FILE);
140
+ const ensureWorkspace = isTrueOption(inlineOptions["ensure-workspace"]);
141
+ const loadedKnex = await loadKnexConfig(appRoot);
142
+ if (!loadedKnex) {
143
+ stdout.write("[jskit:preview] skipped: knexfile.js was not found.\n");
144
+ return 0;
145
+ }
146
+
147
+ const db = loadedKnex.createKnex(loadedKnex.knexConfig);
148
+ try {
149
+ if (!(await db.schema.hasTable("users"))) {
150
+ stdout.write("[jskit:preview] skipped: users table was not found.\n");
151
+ return 0;
152
+ }
153
+
154
+ const usersPreviewModule = await importAppPackageExport(
155
+ appRoot,
156
+ "@jskit-ai/users-core/server/previewUserProvisioning"
157
+ );
158
+ if (typeof usersPreviewModule?.ensurePreviewUser !== "function") {
159
+ throw new Error("@jskit-ai/users-core preview provisioning export is missing ensurePreviewUser().");
160
+ }
161
+
162
+ const userResult = await usersPreviewModule.ensurePreviewUser(db, profileFromOptions(options));
163
+ if (userResult?.skipped) {
164
+ stdout.write(`[jskit:preview] skipped: ${userResult.skipped}.\n`);
165
+ return 0;
166
+ }
167
+
168
+ const authProfile = userResult.profile;
169
+ let workspace = null;
170
+
171
+ if (ensureWorkspace) {
172
+ const appConfig = await loadAppConfigFromAppRoot({ appRoot });
173
+ const tenancyMode = normalizeTenancyMode(appConfig?.tenancyMode);
174
+ if (tenancyMode === TENANCY_MODE_NONE) {
175
+ stdout.write("[jskit:preview] skipped workspace: workspace tenancy is disabled.\n");
176
+ } else if (!(await hasWorkspaceTables(db))) {
177
+ throw new Error(
178
+ `Workspace tenancy is "${tenancyMode}", but workspace tables were not found. Run migrations before prepare-preview-user.`
179
+ );
180
+ } else {
181
+ const workspacesPreviewModule = await importAppPackageExport(
182
+ appRoot,
183
+ "@jskit-ai/workspaces-core/server/previewWorkspaceProvisioning"
184
+ );
185
+ if (typeof workspacesPreviewModule?.ensurePreviewWorkspace !== "function") {
186
+ throw new Error("@jskit-ai/workspaces-core preview provisioning export is missing ensurePreviewWorkspace().");
187
+ }
188
+
189
+ const workspaceResult = await workspacesPreviewModule.ensurePreviewWorkspace(db, userResult.user, authProfile, {
190
+ appConfig,
191
+ tenancyMode
192
+ });
193
+ workspace = workspaceResult?.workspace || null;
194
+ if (workspaceResult?.skipped) {
195
+ stdout.write(`[jskit:preview] skipped workspace: ${workspaceResult.skipped}.\n`);
196
+ }
197
+ }
198
+ }
199
+
200
+ const outputProfile = {
201
+ ...authProfile,
202
+ ...(workspace ? { workspace } : {})
203
+ };
204
+ await writeProfile(profileFile, outputProfile);
205
+ stdout.write(`[jskit:preview] user is ready: ${outputProfile.email} (${outputProfile.id}).\n`);
206
+ if (workspace) {
207
+ stdout.write(`[jskit:preview] workspace is ready: ${workspace.slug} (${workspace.id}).\n`);
208
+ }
209
+ return 0;
210
+ } finally {
211
+ await db.destroy();
212
+ }
213
+ }
214
+
215
+ export { runAppPreparePreviewUserCommand };
@@ -137,6 +137,10 @@ function parseArgs(argv, { createCliError } = {}) {
137
137
  options.inlineOptions["skip-main-sync"] = "true";
138
138
  continue;
139
139
  }
140
+ if (token === "--ensure-workspace") {
141
+ options.inlineOptions["ensure-workspace"] = "true";
142
+ continue;
143
+ }
140
144
 
141
145
  if (token.startsWith("--")) {
142
146
  const withoutPrefix = token.slice(2);