@jskit-ai/jskit-cli 0.2.128 → 0.2.130

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.
@@ -90,7 +90,23 @@ const APP_COMMAND_DEFINITIONS = Object.freeze({
90
90
  defaults: Object.freeze([
91
91
  "Runtime and dev @jskit-ai dependencies are updated separately so package.json sections stay correct.",
92
92
  "Each package is moved to the latest available major.x range and npm saves the resolved exact version.",
93
- "Managed migrations are refreshed afterwards unless --dry-run is used."
93
+ "Managed migrations and the composed CI workflow are refreshed afterwards unless --dry-run is used."
94
+ ])
95
+ }),
96
+ "sync-ci": Object.freeze({
97
+ name: "sync-ci",
98
+ summary: "Regenerate the JSKIT-managed GitHub verification workflow from installed package contracts.",
99
+ usage: "jskit app sync-ci [--force]",
100
+ options: Object.freeze([
101
+ Object.freeze({
102
+ label: "--force",
103
+ description: "Replace a modified workflow that is already recorded as JSKIT-managed."
104
+ })
105
+ ]),
106
+ defaults: Object.freeze([
107
+ "Recomposes CI requirements from installed package descriptors and records the generated content hash in .jskit/lock.json.",
108
+ "Refuses to replace a modified workflow unless --force is explicit; application-specific CI belongs in a separate workflow.",
109
+ "Never claims an unrecorded jskit-verify.yml or removes a customized legacy verify.yml."
94
110
  ])
95
111
  }),
96
112
  "link-local-packages": Object.freeze({
@@ -109,46 +125,6 @@ const APP_COMMAND_DEFINITIONS = Object.freeze({
109
125
  "Clears node_modules/.vite so Vite does not keep stale prebundled paths."
110
126
  ])
111
127
  }),
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
- }),
152
128
  release: Object.freeze({
153
129
  name: "release",
154
130
  summary: "Run the JSKIT release helper for an app repository.",
@@ -241,6 +217,9 @@ function buildAppCommandOptionMeta(subcommandName = "") {
241
217
  if (definition.name === "adopt-managed-scripts") {
242
218
  optionMeta.force = { inputType: "flag" };
243
219
  }
220
+ if (definition.name === "sync-ci") {
221
+ optionMeta.force = { inputType: "flag" };
222
+ }
244
223
  if (definition.name === "update-packages" || definition.name === "release") {
245
224
  optionMeta.registry = { inputType: "text" };
246
225
  }
@@ -255,16 +234,6 @@ function buildAppCommandOptionMeta(subcommandName = "") {
255
234
  if (definition.name === "link-local-packages") {
256
235
  optionMeta["repo-root"] = { inputType: "text" };
257
236
  }
258
- if (definition.name === "prepare-preview-user") {
259
- optionMeta["profile-file"] = { inputType: "text" };
260
- optionMeta.email = { inputType: "text" };
261
- optionMeta.username = { inputType: "text" };
262
- optionMeta["display-name"] = { inputType: "text" };
263
- optionMeta["auth-provider"] = { inputType: "text" };
264
- optionMeta["auth-provider-user-sid"] = { inputType: "text" };
265
- optionMeta["ensure-workspace"] = { inputType: "flag" };
266
- }
267
-
268
237
  return optionMeta;
269
238
  }
270
239
 
@@ -0,0 +1,40 @@
1
+ async function runAppSyncCiCommand(ctx = {}, { appRoot = "", options = {}, stdout }) {
2
+ const {
3
+ createCliError,
4
+ synchronizeAppCiWorkflow
5
+ } = ctx;
6
+ const force = String(options?.inlineOptions?.force || "").trim().toLowerCase() === "true";
7
+ const result = await synchronizeAppCiWorkflow({
8
+ appRoot,
9
+ allowManagedOverwrite: force
10
+ });
11
+ if (!result.applicable) {
12
+ throw createCliError("jskit app sync-ci only works in a JSKIT app root.");
13
+ }
14
+
15
+ if (options.json) {
16
+ stdout.write(`${JSON.stringify({
17
+ path: result.path,
18
+ hash: result.hash,
19
+ changed: result.changed,
20
+ removedLegacyWorkflow: result.removedLegacyWorkflow,
21
+ replacedModifiedWorkflow: result.replacedModifiedWorkflow
22
+ }, null, 2)}\n`);
23
+ return 0;
24
+ }
25
+
26
+ if (result.replacedModifiedWorkflow) {
27
+ stdout.write(`Replaced the modified JSKIT-managed CI workflow: ${result.path}\n`);
28
+ } else if (result.changed) {
29
+ stdout.write(`Synchronized the JSKIT-managed CI workflow: ${result.path}\n`);
30
+ } else {
31
+ stdout.write(`The JSKIT-managed CI workflow is already current: ${result.path}\n`);
32
+ }
33
+ if (result.removedLegacyWorkflow) {
34
+ stdout.write("Removed the superseded .github/workflows/verify.yml scaffold.\n");
35
+ }
36
+ stdout.write(`Recorded content hash: ${result.hash}\n`);
37
+ return 0;
38
+ }
39
+
40
+ export { runAppSyncCiCommand };
@@ -32,9 +32,11 @@ function resolveInstallSpecs(packageNames = [], resolveMajorRange) {
32
32
  async function runAppUpdatePackagesCommand(ctx = {}, { appRoot = "", options = {}, stdout, stderr }) {
33
33
  const {
34
34
  createCliError,
35
- loadAppPackageJson
35
+ loadAppPackageJson,
36
+ assertAppManagedCiWorkflowUnmodified
36
37
  } = ctx;
37
38
 
39
+ await assertAppManagedCiWorkflowUnmodified({ appRoot });
38
40
  const { packageJson } = await loadAppPackageJson(appRoot);
39
41
  const runtimePackages = collectJskitPackageNames(packageJson?.dependencies);
40
42
  const devPackages = collectJskitPackageNames(packageJson?.devDependencies);
@@ -93,6 +95,12 @@ async function runAppUpdatePackagesCommand(ctx = {}, { appRoot = "", options = {
93
95
  stderr,
94
96
  createCliError
95
97
  });
98
+ stdout.write("[jskit:update] synchronizing the managed CI workflow.\n");
99
+ await runLocalJskit(appRoot, ["app", "sync-ci"], {
100
+ stdout,
101
+ stderr,
102
+ createCliError
103
+ });
96
104
  }
97
105
 
98
106
  stdout.write("[jskit:update] done.\n");
@@ -8,7 +8,10 @@ const BASELINE_VERIFY_SCRIPTS = Object.freeze([
8
8
  ]);
9
9
 
10
10
  async function runAppVerifyCommand(ctx = {}, { appRoot = "", options = {}, stdout, stderr }) {
11
- const { createCliError } = ctx;
11
+ const {
12
+ createCliError,
13
+ validateAppCiWorkflow
14
+ } = ctx;
12
15
  const inlineOptions =
13
16
  options?.inlineOptions && typeof options.inlineOptions === "object" ? options.inlineOptions : {};
14
17
  const against = String(inlineOptions.against || "").trim();
@@ -17,6 +20,14 @@ async function runAppVerifyCommand(ctx = {}, { appRoot = "", options = {}, stdou
17
20
  throw createCliError("jskit app verify does not support --dry-run.", { exitCode: 1 });
18
21
  }
19
22
 
23
+ const ciValidation = await validateAppCiWorkflow({ appRoot });
24
+ if (!ciValidation.valid) {
25
+ throw createCliError(
26
+ ciValidation.issues.map((issue) => issue.message).join("\n"),
27
+ { exitCode: 1 }
28
+ );
29
+ }
30
+
20
31
  for (const scriptName of BASELINE_VERIFY_SCRIPTS) {
21
32
  runExternalCommand("npm", ["run", "--if-present", scriptName], {
22
33
  cwd: appRoot,
@@ -26,6 +26,7 @@ function createHealthCommands(ctx = {}) {
26
26
  loadAppLocalPackageRegistry,
27
27
  mergePackageRegistries,
28
28
  hydratePackageRegistryFromInstalledNodeModules,
29
+ validateManagedCiWorkflow,
29
30
  inspectPackageOfferings,
30
31
  fileExists,
31
32
  normalizeRelativePath,
@@ -2130,6 +2131,13 @@ function createHealthCommands(ctx = {}) {
2130
2131
  seedPackageIds: Object.keys(installed)
2131
2132
  });
2132
2133
 
2134
+ const ciValidation = await validateManagedCiWorkflow({
2135
+ appRoot,
2136
+ lock,
2137
+ packageRegistry: combinedPackageRegistry
2138
+ });
2139
+ issues.push(...ciValidation.issues.map((issue) => issue.message));
2140
+
2133
2141
  for (const [packageId, lockEntryValue] of Object.entries(installed)) {
2134
2142
  const lockEntry = ensureObject(lockEntryValue);
2135
2143
  if (!combinedPackageRegistry.has(packageId)) {
@@ -360,6 +360,9 @@ async function runPackageAddCommand(ctx = {}, { positional, options, cwd, io })
360
360
  resolvePackageOptions,
361
361
  applyPackageInstall,
362
362
  adoptAppLocalPackageDependencies,
363
+ assertManagedCiWorkflowUnmodified,
364
+ composeInstalledPackageCi,
365
+ synchronizeManagedCiWorkflow,
363
366
  writeJsonFile,
364
367
  readFileBufferIfExists,
365
368
  runNpmInstall,
@@ -485,6 +488,10 @@ async function runPackageAddCommand(ctx = {}, { positional, options, cwd, io })
485
488
  const combinedPackageRegistry = mergePackageRegistries(packageRegistry, appLocalRegistry);
486
489
  const { packageJsonPath, packageJson } = await loadAppPackageJson(appRoot);
487
490
  const { lockPath, lock } = await loadLockFile(appRoot);
491
+ await assertManagedCiWorkflowUnmodified({
492
+ appRoot,
493
+ lock
494
+ });
488
495
  const resolvedTargetPackageId = targetType === "package"
489
496
  ? await resolvePackageIdFromRegistryOrNodeModules({
490
497
  appRoot,
@@ -556,6 +563,19 @@ async function runPackageAddCommand(ctx = {}, { positional, options, cwd, io })
556
563
  combinedPackageRegistry,
557
564
  `${invocationMode} ${targetType} ${targetId}`
558
565
  );
566
+ for (const packageId of plannedInstalledPackageIds) {
567
+ const packageEntry = combinedPackageRegistry.get(packageId);
568
+ if (!packageEntry) {
569
+ throw createCliError(
570
+ `[ci:descriptor-missing] Installed package descriptor not found for ${packageId}. Restore the package in node_modules or the JSKIT catalog before changing installed package requirements.`
571
+ );
572
+ }
573
+ }
574
+ composeInstalledPackageCi({
575
+ lock,
576
+ packageRegistry: combinedPackageRegistry,
577
+ installedPackageIds: plannedInstalledPackageIds
578
+ });
559
579
 
560
580
  if (targetType === "bundle") {
561
581
  validateInlineOptionsForBundle({
@@ -766,6 +786,14 @@ async function runPackageAddCommand(ctx = {}, { positional, options, cwd, io })
766
786
  });
767
787
  }
768
788
 
789
+ await synchronizeManagedCiWorkflow({
790
+ appRoot,
791
+ lock,
792
+ packageRegistry: combinedPackageRegistry,
793
+ touchedFiles,
794
+ dryRun: options.dryRun === true
795
+ });
796
+
769
797
  const touchedFileList = sortStrings([...touchedFiles]);
770
798
  const successLabel = invocationMode === "generate"
771
799
  ? "Generated with"
@@ -27,6 +27,8 @@ async function runPackageRemoveCommand(ctx = {}, { positional, options, cwd, io
27
27
  removeEnvValue,
28
28
  writeFile,
29
29
  removeManagedViteProxyEntries,
30
+ assertManagedCiWorkflowUnmodified,
31
+ synchronizeManagedCiWorkflow,
30
32
  hashBuffer,
31
33
  rm,
32
34
  writeJsonFile,
@@ -45,6 +47,10 @@ async function runPackageRemoveCommand(ctx = {}, { positional, options, cwd, io
45
47
  const combinedPackageRegistry = mergePackageRegistries(packageRegistry, appLocalRegistry);
46
48
  const { packageJsonPath, packageJson } = await loadAppPackageJson(appRoot);
47
49
  const { lockPath, lock } = await loadLockFile(appRoot);
50
+ await assertManagedCiWorkflowUnmodified({
51
+ appRoot,
52
+ lock
53
+ });
48
54
  const installed = ensureObject(lock.installedPackages);
49
55
  await hydratePackageRegistryFromInstalledNodeModules({
50
56
  appRoot,
@@ -159,6 +165,13 @@ async function runPackageRemoveCommand(ctx = {}, { positional, options, cwd, io
159
165
  }
160
166
 
161
167
  delete installed[resolvedTargetId];
168
+ await synchronizeManagedCiWorkflow({
169
+ appRoot,
170
+ lock,
171
+ packageRegistry: combinedPackageRegistry,
172
+ touchedFiles,
173
+ dryRun: options.dryRun === true
174
+ });
162
175
  const touchedFileList = sortStrings([...touchedFiles]);
163
176
 
164
177
  if (!options.dryRun) {
@@ -137,11 +137,6 @@ 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
- }
144
-
145
140
  if (token.startsWith("--")) {
146
141
  const withoutPrefix = token.slice(2);
147
142
  const hasInlineValue = withoutPrefix.includes("=");
@@ -27,6 +27,13 @@ function createCommandHandlerDeps(deps = {}) {
27
27
  applyPackageMigrationsOnly: deps.applyPackageMigrationsOnly,
28
28
  applyPackagePositioning: deps.applyPackagePositioning,
29
29
  adoptAppLocalPackageDependencies: deps.adoptAppLocalPackageDependencies,
30
+ assertAppManagedCiWorkflowUnmodified: deps.assertAppManagedCiWorkflowUnmodified,
31
+ assertManagedCiWorkflowUnmodified: deps.assertManagedCiWorkflowUnmodified,
32
+ composeInstalledPackageCi: deps.composeInstalledPackageCi,
33
+ synchronizeAppCiWorkflow: deps.synchronizeAppCiWorkflow,
34
+ synchronizeManagedCiWorkflow: deps.synchronizeManagedCiWorkflow,
35
+ validateAppCiWorkflow: deps.validateAppCiWorkflow,
36
+ validateManagedCiWorkflow: deps.validateManagedCiWorkflow,
30
37
  loadAppPackageJson: deps.loadAppPackageJson,
31
38
  resolveLocalPackageId: deps.resolveLocalPackageId,
32
39
  createLocalPackageScaffoldFiles: deps.createLocalPackageScaffoldFiles,
@@ -93,6 +93,15 @@ import {
93
93
  import {
94
94
  removeManagedViteProxyEntries
95
95
  } from "../cliRuntime/viteProxy.js";
96
+ import {
97
+ assertAppManagedCiWorkflowUnmodified,
98
+ assertManagedCiWorkflowUnmodified,
99
+ composeInstalledPackageCi,
100
+ synchronizeAppCiWorkflow,
101
+ synchronizeManagedCiWorkflow,
102
+ validateAppCiWorkflow,
103
+ validateManagedCiWorkflow
104
+ } from "../cliRuntime/ci/managedWorkflow.js";
96
105
 
97
106
  const commandHandlers = createCommandHandlers(
98
107
  createCommandHandlerDeps({
@@ -123,6 +132,13 @@ const commandHandlers = createCommandHandlers(
123
132
  applyPackageMigrationsOnly,
124
133
  applyPackagePositioning,
125
134
  adoptAppLocalPackageDependencies,
135
+ assertAppManagedCiWorkflowUnmodified,
136
+ assertManagedCiWorkflowUnmodified,
137
+ composeInstalledPackageCi,
138
+ synchronizeAppCiWorkflow,
139
+ synchronizeManagedCiWorkflow,
140
+ validateAppCiWorkflow,
141
+ validateManagedCiWorkflow,
126
142
  loadAppPackageJson,
127
143
  resolveLocalPackageId,
128
144
  createLocalPackageScaffoldFiles,
@@ -1,2 +1,13 @@
1
1
  export { runCli } from "./core/createCliRunner.js";
2
2
  export * from "./appBlueprint.js";
3
+ export {
4
+ synchronizeAppCiWorkflow,
5
+ validateAppCiWorkflow
6
+ } from "./cliRuntime/ci/managedWorkflow.js";
7
+ export {
8
+ composeCiContributions
9
+ } from "./cliRuntime/ci/composer.js";
10
+ export {
11
+ JSKIT_CI_WORKFLOW_RELATIVE_PATH,
12
+ renderGithubWorkflow
13
+ } from "./cliRuntime/ci/githubWorkflow.js";
@@ -1,215 +0,0 @@
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 };