flowdular 0.2.0 → 0.2.2

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/README.md CHANGED
@@ -71,3 +71,16 @@ Release tooling can import the source distribution API through `flowdular/distri
71
71
  - [Documentation](https://github.com/Flowdular/flowdular/tree/main/docs) · [Source and issues](https://github.com/Flowdular/flowdular) · [Website](https://flowdular.com)
72
72
 
73
73
  Licensed under MIT. Repository documentation and source require repository access while the repositories are private.
74
+
75
+ ## Interactive setup
76
+
77
+ Run `pnpm flowdular setup` in a terminal. The wizard offers a local demo, existing
78
+ PostgreSQL connection settings, or a configuration check. Local resets require
79
+ confirmation and a stopped application; custom or hosted databases are refused.
80
+ PostgreSQL URLs are masked and saved to `.env` with TLS verification; this does
81
+ not initialize, reset or connect to the hosted database.
82
+
83
+ Without an interactive terminal, `setup` remains a configuration check. Use
84
+ `setup check --json` for machine-readable results and
85
+ `setup quick --apply --confirm reset-local-auth` for an explicitly authorized
86
+ local reset in a script. Ctrl+C or declining confirmation leaves data unchanged.
@@ -3405,7 +3405,7 @@ import { dirname as dirname6, join as join6, relative as relative5 } from "node:
3405
3405
  import { createRequire } from "node:module";
3406
3406
  import { readFile as readFile5 } from "node:fs/promises";
3407
3407
  import { dirname as dirname4, join as join3 } from "node:path";
3408
- var SDK_VERSION = "0.2.0";
3408
+ var SDK_VERSION = "0.2.2";
3409
3409
  function npmPackage(specifier) {
3410
3410
  return specifier.startsWith("@") ? specifier.split("/").slice(0, 2).join("/") : specifier.split("/")[0];
3411
3411
  }
package/dist/index.js CHANGED
@@ -1992,22 +1992,66 @@ function stringFlag(arguments_, name) {
1992
1992
  }
1993
1993
 
1994
1994
  // src/output.ts
1995
+ import { styleText } from "node:util";
1995
1996
  function isCapabilityList(values2) {
1996
1997
  return values2.every(
1997
1998
  (value) => typeof value === "object" && value !== null && typeof value.id === "string" && typeof value.risk === "string" && typeof value.summary === "string"
1998
1999
  );
1999
2000
  }
2000
- function renderOutput(envelope, json2) {
2001
+ function renderOutput(envelope, json2, color = false) {
2001
2002
  if (json2) return JSON.stringify(envelope, null, 2);
2002
- if (!envelope.ok)
2003
- return `ERROR ${envelope.error?.code ?? "UNKNOWN"}
2003
+ const paint = (text, format) => color ? styleText(format, text, { validateStream: false }) : text;
2004
+ if (!envelope.ok) {
2005
+ const heading = `ERROR ${envelope.error?.code ?? "UNKNOWN"}
2004
2006
  ${envelope.error?.message ?? "Command failed."}`;
2007
+ if (envelope.error?.code !== "DOCTOR_FAILED") return heading;
2008
+ const details = envelope.error.details;
2009
+ if (!Array.isArray(details?.checks)) return heading;
2010
+ const failures = details.checks.filter(
2011
+ (check) => check && check.status === "fail" && typeof check.id === "string" && typeof check.message === "string"
2012
+ );
2013
+ return [
2014
+ heading,
2015
+ ...failures.map((check) => `FAIL ${check.id} ${check.message}`)
2016
+ ].join("\n");
2017
+ }
2005
2018
  const decorate = (body) => [
2006
2019
  body,
2007
2020
  ...envelope.warnings.map((warning) => `WARNING ${warning}`),
2008
2021
  ...envelope.evidence.map((item) => `EVIDENCE ${item}`)
2009
2022
  ].join("\n");
2010
2023
  const data2 = envelope.data;
2024
+ if (data2?.cancelled === true) return decorate("Setup cancelled.");
2025
+ if (data2?.setup === "postgresql")
2026
+ return [
2027
+ "",
2028
+ ` ${paint("FLOWDULAR", "bold")} ${paint("PostgreSQL configured", "green")}`,
2029
+ "",
2030
+ " PostgreSQL settings saved to .env.",
2031
+ "",
2032
+ ` Start app ${paint("pnpm dev", "bold")}`,
2033
+ ` Local URL ${paint("http://localhost:4310", "cyan")}`,
2034
+ " Open the app to create an account.",
2035
+ "",
2036
+ ...envelope.warnings.map((warning) => ` ${warning}`),
2037
+ ""
2038
+ ].join("\n");
2039
+ if (data2?.setup === "local") {
2040
+ const admin = data2.accounts?.admin;
2041
+ return [
2042
+ "",
2043
+ ` ${paint("FLOWDULAR", "bold")} ${paint("Local demo is ready.", "green")}`,
2044
+ "",
2045
+ ` Start app ${paint("pnpm dev", "bold")}`,
2046
+ ` Local URL ${paint("http://localhost:4310", "cyan")}`,
2047
+ "",
2048
+ ...admin?.email ? [` Demo login ${admin.email}`] : [],
2049
+ ...admin?.password ? [` Password ${admin.password}`] : [],
2050
+ "",
2051
+ ...envelope.warnings.map((warning) => ` ${warning}`),
2052
+ ""
2053
+ ].join("\n");
2054
+ }
2011
2055
  if (data2 && Array.isArray(data2.checks)) {
2012
2056
  return decorate(
2013
2057
  data2.checks.map((check) => {
@@ -2026,143 +2070,53 @@ ${envelope.error?.message ?? "Command failed."}`;
2026
2070
  return decorate(JSON.stringify(data2, null, 2));
2027
2071
  }
2028
2072
 
2029
- // ../kernel/src/errors.ts
2030
- var RegistryError = class extends Error {
2031
- code;
2032
- constructor(code, message) {
2033
- super(message);
2034
- this.name = "RegistryError";
2035
- this.code = code;
2036
- }
2037
- };
2038
-
2039
- // ../kernel/src/module-compatibility.ts
2040
- var import_semver = __toESM(require_semver2(), 1);
2041
- var PLATFORM_API_VERSION = "0.1.0";
2042
- function satisfiesModuleVersion(version, range) {
2043
- return (0, import_semver.valid)(version) !== null && (0, import_semver.validRange)(range) !== null && (0, import_semver.satisfies)(version, range);
2044
- }
2045
- function compareModuleVersions(left, right) {
2046
- return (0, import_semver.rcompare)(left, right);
2047
- }
2048
- function assertModuleCompatibility(manifest2, platformVersion = PLATFORM_API_VERSION) {
2049
- if (!(0, import_semver.valid)(manifest2.version))
2050
- throw new RegistryError(
2051
- "MODULE_VERSION_INVALID",
2052
- `Invalid version for ${manifest2.id}: ${manifest2.version}`
2053
- );
2054
- if (manifest2.platformApi !== void 0 && (!(0, import_semver.validRange)(manifest2.platformApi) || platformVersion !== null && !satisfiesModuleVersion(platformVersion, manifest2.platformApi))) {
2055
- throw new RegistryError(
2056
- "MODULE_PLATFORM_INCOMPATIBLE",
2057
- `${manifest2.id} requires platform API ${manifest2.platformApi}; available ${PLATFORM_API_VERSION}.`
2058
- );
2059
- }
2060
- const ids = /* @__PURE__ */ new Set();
2061
- for (const dependency of manifest2.dependencies) {
2062
- if (!dependency.range.trim() || !(0, import_semver.validRange)(dependency.range))
2063
- throw new RegistryError(
2064
- "MODULE_RANGE_INVALID",
2065
- `${manifest2.id} declares an invalid range for ${dependency.id}: ${dependency.range}`
2066
- );
2067
- if (ids.has(dependency.id))
2068
- throw new RegistryError(
2069
- "MODULE_DEPENDENCY_DUPLICATE",
2070
- `${manifest2.id} declares ${dependency.id} more than once.`
2071
- );
2072
- ids.add(dependency.id);
2073
- }
2074
- }
2075
- function assertModuleDependency(consumer, dependency, version) {
2076
- if (!satisfiesModuleVersion(version, dependency.range)) {
2077
- throw new RegistryError(
2078
- "MODULE_DEPENDENCY_INCOMPATIBLE",
2079
- `${consumer} requires ${dependency.id} ${dependency.range}; available ${version}.`
2080
- );
2081
- }
2082
- }
2083
-
2084
- // ../kernel/src/module-registry.ts
2085
- function compareModules(left, right) {
2086
- return left.manifest.id.localeCompare(right.manifest.id);
2087
- }
2088
- function visitModule(id, modules, visiting, visited, ordered) {
2089
- if (visited.has(id)) return;
2090
- if (visiting.has(id)) {
2091
- throw new RegistryError(
2092
- "MODULE_DEPENDENCY_CYCLE",
2093
- `Dependency cycle detected at "${id}".`
2094
- );
2095
- }
2096
- const current = modules.get(id);
2097
- if (!current) {
2098
- throw new RegistryError(
2099
- "MODULE_NOT_FOUND",
2100
- `Module "${id}" is not registered.`
2101
- );
2102
- }
2103
- visiting.add(id);
2104
- for (const dependency of [...current.manifest.dependencies].sort(
2105
- (a, b) => a.id.localeCompare(b.id)
2106
- )) {
2107
- if (!modules.has(dependency.id)) {
2108
- throw new RegistryError(
2109
- "MODULE_DEPENDENCY_MISSING",
2110
- `Module "${id}" requires missing module "${dependency.id}".`
2073
+ // src/workspace.ts
2074
+ import { access, readFile, realpath } from "node:fs/promises";
2075
+ import { dirname, isAbsolute, relative, resolve } from "node:path";
2076
+ async function findWorkspace(start) {
2077
+ let directory = resolve(start);
2078
+ for (; ; ) {
2079
+ const configPath = resolve(directory, "flowdular.json");
2080
+ try {
2081
+ await access(configPath);
2082
+ const config = JSON.parse(await readFile(configPath, "utf8"));
2083
+ return { root: directory, configPath, config };
2084
+ } catch (error) {
2085
+ if (error instanceof SyntaxError) throw error;
2086
+ }
2087
+ const parent = dirname(directory);
2088
+ if (parent === directory) {
2089
+ throw new Error(
2090
+ "No flowdular.json was found in this directory or any parent."
2111
2091
  );
2112
2092
  }
2113
- assertModuleDependency(
2114
- id,
2115
- dependency,
2116
- modules.get(dependency.id).manifest.version
2117
- );
2118
- visitModule(dependency.id, modules, visiting, visited, ordered);
2093
+ directory = parent;
2119
2094
  }
2120
- visiting.delete(id);
2121
- visited.add(id);
2122
- ordered.push(current);
2123
2095
  }
2124
- function createModuleRegistry(input) {
2125
- const byId = /* @__PURE__ */ new Map();
2126
- const navigationIds = /* @__PURE__ */ new Set();
2127
- for (const module of input) {
2128
- assertModuleCompatibility(module.manifest);
2129
- if (byId.has(module.manifest.id)) {
2130
- throw new RegistryError(
2131
- "MODULE_DUPLICATE",
2132
- `Module "${module.manifest.id}" is registered more than once.`
2133
- );
2134
- }
2135
- byId.set(module.manifest.id, module);
2136
- for (const item of module.navigation ?? []) {
2137
- if (navigationIds.has(item.id)) {
2138
- throw new RegistryError(
2139
- "NAVIGATION_DUPLICATE",
2140
- `Navigation contribution "${item.id}" is registered more than once.`
2141
- );
2142
- }
2143
- navigationIds.add(item.id);
2144
- }
2096
+ function resolveInside(root, candidate) {
2097
+ const absolute = isAbsolute(candidate) ? resolve(candidate) : resolve(root, candidate);
2098
+ const pathFromRoot = relative(root, absolute);
2099
+ if (pathFromRoot.startsWith("..") || isAbsolute(pathFromRoot)) {
2100
+ throw new Error(`Path escapes the workspace: ${candidate}`);
2145
2101
  }
2146
- const ordered = [];
2147
- const visiting = /* @__PURE__ */ new Set();
2148
- const visited = /* @__PURE__ */ new Set();
2149
- for (const module of [...input].sort(compareModules)) {
2150
- visitModule(module.manifest.id, byId, visiting, visited, ordered);
2102
+ return absolute;
2103
+ }
2104
+ async function resolveExistingInside(root, candidate) {
2105
+ const canonicalRoot = await realpath(root);
2106
+ const canonicalCandidate = await realpath(resolveInside(root, candidate));
2107
+ const pathFromRoot = relative(canonicalRoot, canonicalCandidate);
2108
+ if (pathFromRoot.startsWith("..") || isAbsolute(pathFromRoot)) {
2109
+ throw new Error(`Path escapes the workspace through a link: ${candidate}`);
2151
2110
  }
2152
- return Object.freeze({
2153
- modules: Object.freeze(ordered),
2154
- get: (id) => byId.get(id),
2155
- has: (id) => byId.has(id),
2156
- navigation: (permissions) => Object.freeze(
2157
- ordered.flatMap((module) => module.navigation ?? []).filter(
2158
- (item) => !item.permission || permissions?.has(item.permission)
2159
- ).sort(
2160
- (left, right) => (left.order ?? 1e3) - (right.order ?? 1e3) || left.id.localeCompare(right.id)
2161
- )
2162
- )
2163
- });
2111
+ return canonicalCandidate;
2164
2112
  }
2165
2113
 
2114
+ // src/validation.ts
2115
+ import { readdir, readFile as readFile2 } from "node:fs/promises";
2116
+ import { basename, dirname as dirname2, join, relative as relative2 } from "node:path";
2117
+ import Ajv2020 from "ajv/dist/2020.js";
2118
+ import { parse as parseYaml } from "yaml";
2119
+
2166
2120
  // ../contracts/schemas/blueprint.schema.json
2167
2121
  var blueprint_schema_default = {
2168
2122
  $schema: "https://json-schema.org/draft/2020-12/schema",
@@ -2985,19 +2939,7 @@ var projectSchema = project_schema_default;
2985
2939
  var moduleCatalogSchema = module_catalog_schema_default;
2986
2940
  var moduleArtifactSchema = module_artifact_schema_default;
2987
2941
 
2988
- // src/module-catalog.ts
2989
- import { readFile as readFile3, stat } from "node:fs/promises";
2990
- import { dirname as dirname3, resolve as resolve2 } from "node:path";
2991
-
2992
- // src/module-artifact.ts
2993
- import { parse as parseYaml2 } from "yaml";
2994
- import { createHash } from "node:crypto";
2995
-
2996
2942
  // src/validation.ts
2997
- import { readdir, readFile } from "node:fs/promises";
2998
- import { basename, dirname, join, relative } from "node:path";
2999
- import Ajv2020 from "ajv/dist/2020.js";
3000
- import { parse as parseYaml } from "yaml";
3001
2943
  var ajv = new Ajv2020({ allErrors: true, strict: true });
3002
2944
  var validators = {
3003
2945
  application: ajv.compile(projectSchema.properties.application),
@@ -3020,7 +2962,7 @@ function issuesFrom(errors) {
3020
2962
  }));
3021
2963
  }
3022
2964
  async function parseFile(path) {
3023
- const source = await readFile(path, "utf8");
2965
+ const source = await readFile2(path, "utf8");
3024
2966
  return path.endsWith(".yaml") || path.endsWith(".yml") ? parseYaml(source) : JSON.parse(source);
3025
2967
  }
3026
2968
  async function validateFile(path, validator) {
@@ -3084,7 +3026,7 @@ var blueprintFiles = [
3084
3026
  ];
3085
3027
  async function validateBlueprint(path) {
3086
3028
  const report = await validateFile(path, validators.blueprint);
3087
- const directory = dirname(path);
3029
+ const directory = dirname2(path);
3088
3030
  const entries = new Set(
3089
3031
  (await readdir(directory)).filter((name) => basename(name) === name)
3090
3032
  );
@@ -3095,14 +3037,177 @@ async function validateBlueprint(path) {
3095
3037
  ...missing.map((name) => ({
3096
3038
  code: "BLUEPRINT_FILE_MISSING",
3097
3039
  message: `Required blueprint file is missing: ${name}`,
3098
- path: relative(process.cwd(), join(directory, name)),
3040
+ path: relative2(process.cwd(), join(directory, name)),
3099
3041
  severity: "error"
3100
3042
  }))
3101
3043
  ];
3102
3044
  return { ...report, valid: false, issues };
3103
3045
  }
3104
3046
 
3047
+ // src/agent-resources.ts
3048
+ function agentResource(workspace, key) {
3049
+ const defaults = {
3050
+ policy: ".ai/policies/capabilities.yaml",
3051
+ modelRouting: ".ai/policies/model-routing.yaml",
3052
+ blueprints: ".ai/blueprints"
3053
+ };
3054
+ const agent = workspace.config.agent;
3055
+ const path = agent?.[key] ?? defaults[key];
3056
+ if (typeof path !== "string" || !path)
3057
+ throw new Error(`Invalid agent.${key} path.`);
3058
+ return resolveInside(workspace.root, path);
3059
+ }
3060
+ function findBlueprintFiles(workspace) {
3061
+ return findNamedFiles(
3062
+ agentResource(workspace, "blueprints"),
3063
+ "blueprint.json"
3064
+ );
3065
+ }
3066
+
3067
+ // ../kernel/src/errors.ts
3068
+ var RegistryError = class extends Error {
3069
+ code;
3070
+ constructor(code, message) {
3071
+ super(message);
3072
+ this.name = "RegistryError";
3073
+ this.code = code;
3074
+ }
3075
+ };
3076
+
3077
+ // ../kernel/src/module-compatibility.ts
3078
+ var import_semver = __toESM(require_semver2(), 1);
3079
+ var PLATFORM_API_VERSION = "0.1.0";
3080
+ function satisfiesModuleVersion(version, range) {
3081
+ return (0, import_semver.valid)(version) !== null && (0, import_semver.validRange)(range) !== null && (0, import_semver.satisfies)(version, range);
3082
+ }
3083
+ function compareModuleVersions(left, right) {
3084
+ return (0, import_semver.rcompare)(left, right);
3085
+ }
3086
+ function assertModuleCompatibility(manifest2, platformVersion = PLATFORM_API_VERSION) {
3087
+ if (!(0, import_semver.valid)(manifest2.version))
3088
+ throw new RegistryError(
3089
+ "MODULE_VERSION_INVALID",
3090
+ `Invalid version for ${manifest2.id}: ${manifest2.version}`
3091
+ );
3092
+ if (manifest2.platformApi !== void 0 && (!(0, import_semver.validRange)(manifest2.platformApi) || platformVersion !== null && !satisfiesModuleVersion(platformVersion, manifest2.platformApi))) {
3093
+ throw new RegistryError(
3094
+ "MODULE_PLATFORM_INCOMPATIBLE",
3095
+ `${manifest2.id} requires platform API ${manifest2.platformApi}; available ${PLATFORM_API_VERSION}.`
3096
+ );
3097
+ }
3098
+ const ids = /* @__PURE__ */ new Set();
3099
+ for (const dependency of manifest2.dependencies) {
3100
+ if (!dependency.range.trim() || !(0, import_semver.validRange)(dependency.range))
3101
+ throw new RegistryError(
3102
+ "MODULE_RANGE_INVALID",
3103
+ `${manifest2.id} declares an invalid range for ${dependency.id}: ${dependency.range}`
3104
+ );
3105
+ if (ids.has(dependency.id))
3106
+ throw new RegistryError(
3107
+ "MODULE_DEPENDENCY_DUPLICATE",
3108
+ `${manifest2.id} declares ${dependency.id} more than once.`
3109
+ );
3110
+ ids.add(dependency.id);
3111
+ }
3112
+ }
3113
+ function assertModuleDependency(consumer, dependency, version) {
3114
+ if (!satisfiesModuleVersion(version, dependency.range)) {
3115
+ throw new RegistryError(
3116
+ "MODULE_DEPENDENCY_INCOMPATIBLE",
3117
+ `${consumer} requires ${dependency.id} ${dependency.range}; available ${version}.`
3118
+ );
3119
+ }
3120
+ }
3121
+
3122
+ // ../kernel/src/module-registry.ts
3123
+ function compareModules(left, right) {
3124
+ return left.manifest.id.localeCompare(right.manifest.id);
3125
+ }
3126
+ function visitModule(id, modules, visiting, visited, ordered) {
3127
+ if (visited.has(id)) return;
3128
+ if (visiting.has(id)) {
3129
+ throw new RegistryError(
3130
+ "MODULE_DEPENDENCY_CYCLE",
3131
+ `Dependency cycle detected at "${id}".`
3132
+ );
3133
+ }
3134
+ const current = modules.get(id);
3135
+ if (!current) {
3136
+ throw new RegistryError(
3137
+ "MODULE_NOT_FOUND",
3138
+ `Module "${id}" is not registered.`
3139
+ );
3140
+ }
3141
+ visiting.add(id);
3142
+ for (const dependency of [...current.manifest.dependencies].sort(
3143
+ (a, b) => a.id.localeCompare(b.id)
3144
+ )) {
3145
+ if (!modules.has(dependency.id)) {
3146
+ throw new RegistryError(
3147
+ "MODULE_DEPENDENCY_MISSING",
3148
+ `Module "${id}" requires missing module "${dependency.id}".`
3149
+ );
3150
+ }
3151
+ assertModuleDependency(
3152
+ id,
3153
+ dependency,
3154
+ modules.get(dependency.id).manifest.version
3155
+ );
3156
+ visitModule(dependency.id, modules, visiting, visited, ordered);
3157
+ }
3158
+ visiting.delete(id);
3159
+ visited.add(id);
3160
+ ordered.push(current);
3161
+ }
3162
+ function createModuleRegistry(input) {
3163
+ const byId = /* @__PURE__ */ new Map();
3164
+ const navigationIds = /* @__PURE__ */ new Set();
3165
+ for (const module of input) {
3166
+ assertModuleCompatibility(module.manifest);
3167
+ if (byId.has(module.manifest.id)) {
3168
+ throw new RegistryError(
3169
+ "MODULE_DUPLICATE",
3170
+ `Module "${module.manifest.id}" is registered more than once.`
3171
+ );
3172
+ }
3173
+ byId.set(module.manifest.id, module);
3174
+ for (const item of module.navigation ?? []) {
3175
+ if (navigationIds.has(item.id)) {
3176
+ throw new RegistryError(
3177
+ "NAVIGATION_DUPLICATE",
3178
+ `Navigation contribution "${item.id}" is registered more than once.`
3179
+ );
3180
+ }
3181
+ navigationIds.add(item.id);
3182
+ }
3183
+ }
3184
+ const ordered = [];
3185
+ const visiting = /* @__PURE__ */ new Set();
3186
+ const visited = /* @__PURE__ */ new Set();
3187
+ for (const module of [...input].sort(compareModules)) {
3188
+ visitModule(module.manifest.id, byId, visiting, visited, ordered);
3189
+ }
3190
+ return Object.freeze({
3191
+ modules: Object.freeze(ordered),
3192
+ get: (id) => byId.get(id),
3193
+ has: (id) => byId.has(id),
3194
+ navigation: (permissions) => Object.freeze(
3195
+ ordered.flatMap((module) => module.navigation ?? []).filter(
3196
+ (item) => !item.permission || permissions?.has(item.permission)
3197
+ ).sort(
3198
+ (left, right) => (left.order ?? 1e3) - (right.order ?? 1e3) || left.id.localeCompare(right.id)
3199
+ )
3200
+ )
3201
+ });
3202
+ }
3203
+
3204
+ // src/module-catalog.ts
3205
+ import { readFile as readFile3, stat } from "node:fs/promises";
3206
+ import { dirname as dirname3, resolve as resolve2 } from "node:path";
3207
+
3105
3208
  // src/module-artifact.ts
3209
+ import { parse as parseYaml2 } from "yaml";
3210
+ import { createHash } from "node:crypto";
3106
3211
  var MAX_ARTIFACT_BYTES = 48 * 1024 * 1024;
3107
3212
  var MAX_SOURCE_BYTES = 32 * 1024 * 1024;
3108
3213
  var MAX_FILES = 4096;
@@ -3261,47 +3366,6 @@ function validateModuleArtifact(value) {
3261
3366
  return artifact;
3262
3367
  }
3263
3368
 
3264
- // src/workspace.ts
3265
- import { access, readFile as readFile2, realpath } from "node:fs/promises";
3266
- import { dirname as dirname2, isAbsolute, relative as relative2, resolve } from "node:path";
3267
- async function findWorkspace(start) {
3268
- let directory = resolve(start);
3269
- for (; ; ) {
3270
- const configPath = resolve(directory, "flowdular.json");
3271
- try {
3272
- await access(configPath);
3273
- const config = JSON.parse(await readFile2(configPath, "utf8"));
3274
- return { root: directory, configPath, config };
3275
- } catch (error) {
3276
- if (error instanceof SyntaxError) throw error;
3277
- }
3278
- const parent = dirname2(directory);
3279
- if (parent === directory) {
3280
- throw new Error(
3281
- "No flowdular.json was found in this directory or any parent."
3282
- );
3283
- }
3284
- directory = parent;
3285
- }
3286
- }
3287
- function resolveInside(root, candidate) {
3288
- const absolute = isAbsolute(candidate) ? resolve(candidate) : resolve(root, candidate);
3289
- const pathFromRoot = relative2(root, absolute);
3290
- if (pathFromRoot.startsWith("..") || isAbsolute(pathFromRoot)) {
3291
- throw new Error(`Path escapes the workspace: ${candidate}`);
3292
- }
3293
- return absolute;
3294
- }
3295
- async function resolveExistingInside(root, candidate) {
3296
- const canonicalRoot = await realpath(root);
3297
- const canonicalCandidate = await realpath(resolveInside(root, candidate));
3298
- const pathFromRoot = relative2(canonicalRoot, canonicalCandidate);
3299
- if (pathFromRoot.startsWith("..") || isAbsolute(pathFromRoot)) {
3300
- throw new Error(`Path escapes the workspace through a link: ${candidate}`);
3301
- }
3302
- return canonicalCandidate;
3303
- }
3304
-
3305
3369
  // src/module-catalog.ts
3306
3370
  var OFFICIAL_CATALOG = "https://raw.githubusercontent.com/Flowdular/official-modules/main/registry/index.json";
3307
3371
  var OFFICIAL_PREFIX = "https://raw.githubusercontent.com/Flowdular/official-modules/";
@@ -3499,7 +3563,7 @@ import { dirname as dirname6, join as join5, relative as relative5 } from "node:
3499
3563
  import { createRequire } from "node:module";
3500
3564
  import { readFile as readFile4 } from "node:fs/promises";
3501
3565
  import { dirname as dirname4, join as join2 } from "node:path";
3502
- var SDK_VERSION = "0.2.0";
3566
+ var SDK_VERSION = "0.2.2";
3503
3567
  var LIBRARIES = /* @__PURE__ */ new Set([
3504
3568
  "ai-provider",
3505
3569
  "cli-protocol",
@@ -3591,14 +3655,14 @@ import { createRequire as createRequire2 } from "node:module";
3591
3655
  import { lstat, readFile as readFile5 } from "node:fs/promises";
3592
3656
  import { join as join3, relative as relative3 } from "node:path";
3593
3657
  function moduleRoots(workspace) {
3594
- const roots = workspace.config.modules?.roots ?? ["modules"];
3595
- if (!Array.isArray(roots) || !roots.length || roots.some(
3658
+ const roots2 = workspace.config.modules?.roots ?? ["modules"];
3659
+ if (!Array.isArray(roots2) || !roots2.length || roots2.some(
3596
3660
  (root) => typeof root !== "string" || !root || root.startsWith(".") || root.includes("\\") || root.split("/").some((part) => part === ".." || !part)
3597
3661
  ))
3598
3662
  throw new Error(
3599
3663
  "Invalid modules.roots. Use relative workspace directories."
3600
3664
  );
3601
- return roots.map((root) => {
3665
+ return roots2.map((root) => {
3602
3666
  const path = resolveInside(workspace.root, root);
3603
3667
  if (!relative3(workspace.root, path))
3604
3668
  throw new Error("The workspace root cannot be a module root.");
@@ -4007,10 +4071,10 @@ async function validateModules(workspace, options = {}) {
4007
4071
  );
4008
4072
  }
4009
4073
  try {
4010
- const registered = manifests.map(
4074
+ const registered2 = manifests.map(
4011
4075
  (manifest2) => ({ manifest: manifest2 })
4012
4076
  );
4013
- const registry = createModuleRegistry(registered);
4077
+ const registry = createModuleRegistry(registered2);
4014
4078
  const enabled = config.modules?.enabled ?? [];
4015
4079
  const missingEnabled = enabled.filter((id) => !registry.has(id));
4016
4080
  if (missingEnabled.length > 0) {
@@ -6004,19 +6068,35 @@ function createPgliteCluster(options = {}) {
6004
6068
  const close = () => {
6005
6069
  closePromise ??= (async () => {
6006
6070
  if (!databasePromise) return;
6007
- const instance = await databasePromise;
6071
+ const instance = await databasePromise.catch(() => void 0);
6008
6072
  databasePromise = void 0;
6009
- await instance.close();
6073
+ await instance?.close();
6010
6074
  })();
6011
6075
  return closePromise;
6012
6076
  };
6013
6077
  const database = async () => {
6014
6078
  databasePromise ??= (async () => {
6079
+ if (options.dataDirectory) {
6080
+ const { mkdir: mkdir6 } = await import("node:fs/promises");
6081
+ await mkdir6(options.dataDirectory, { recursive: true, mode: 448 });
6082
+ }
6015
6083
  const created = options.dataDirectory ? await PGlite.create({
6016
6084
  dataDir: options.dataDirectory,
6017
6085
  parsers: SERVER_PARSERS
6018
6086
  }) : await PGlite.create({ parsers: SERVER_PARSERS });
6019
- if (options.bootstrap) await created.exec(options.bootstrap);
6087
+ try {
6088
+ if (options.bootstrap) await created.exec(options.bootstrap);
6089
+ } catch (error) {
6090
+ try {
6091
+ await created.close();
6092
+ } catch (closeError) {
6093
+ throw new AggregateError(
6094
+ [error, closeError],
6095
+ "PGlite bootstrap failed and cleanup also failed."
6096
+ );
6097
+ }
6098
+ throw error;
6099
+ }
6020
6100
  return created;
6021
6101
  })();
6022
6102
  return databasePromise;
@@ -6394,8 +6474,8 @@ async function loadManifests(workspace) {
6394
6474
  }
6395
6475
  return descriptors;
6396
6476
  }
6397
- function moduleOrder(manifests, roots, includeDependencies) {
6398
- const requested = new Set(roots);
6477
+ function moduleOrder(manifests, roots2, includeDependencies) {
6478
+ const requested = new Set(roots2);
6399
6479
  const visiting = [];
6400
6480
  const visited = /* @__PURE__ */ new Set();
6401
6481
  const ordered = [];
@@ -6739,14 +6819,15 @@ async function runDoctor(workspace) {
6739
6819
  message: project.valid ? "Project contract is valid." : project.issues.map((issue2) => issue2.message).join("; "),
6740
6820
  evidence: relative6(workspace.root, workspace.configPath)
6741
6821
  });
6742
- for (const [id, path] of [
6743
- ["policy.capabilities", ".ai/policies/capabilities.yaml"],
6744
- ["policy.model-routing", ".ai/policies/model-routing.yaml"],
6745
- ["blueprints.root", ".ai/blueprints"],
6746
- ["modules.root", "modules"],
6747
- ["packages.root", "packages"],
6748
- ["platform.root", "platform"]
6822
+ for (const [id, resource] of [
6823
+ ["policy.capabilities", agentResource(workspace, "policy")],
6824
+ ["policy.model-routing", agentResource(workspace, "modelRouting")],
6825
+ ["blueprints.root", agentResource(workspace, "blueprints")],
6826
+ ["modules.root", join9(workspace.root, "modules")],
6827
+ ...(await sdkModules(workspace)).size ? [] : [["packages.root", join9(workspace.root, "packages")]],
6828
+ ["platform.root", join9(workspace.root, "platform")]
6749
6829
  ]) {
6830
+ const path = relative6(workspace.root, resource);
6750
6831
  checks.push({
6751
6832
  id,
6752
6833
  status: await exists3(join9(workspace.root, path)) ? "pass" : "fail",
@@ -6763,7 +6844,7 @@ async function runDoctor(workspace) {
6763
6844
  message: packageJson2.packageManager ? `Pinned package manager: ${packageJson2.packageManager}` : "packageManager must pin pnpm.",
6764
6845
  evidence: "package.json"
6765
6846
  });
6766
- const blueprints = await findNamedFiles(workspace.root, "blueprint.json");
6847
+ const blueprints = await findBlueprintFiles(workspace);
6767
6848
  checks.push({
6768
6849
  id: "blueprints.discovered",
6769
6850
  status: blueprints.length > 0 ? "pass" : "fail",
@@ -6792,8 +6873,56 @@ async function runDoctor(workspace) {
6792
6873
 
6793
6874
  // src/extensions.ts
6794
6875
  import { readFile as readFile11 } from "node:fs/promises";
6795
- import { dirname as dirname9, relative as relative7 } from "node:path";
6796
- import { pathToFileURL as pathToFileURL2 } from "node:url";
6876
+ import { dirname as dirname10, relative as relative7 } from "node:path";
6877
+
6878
+ // src/module-import.ts
6879
+ import { readFileSync as readFileSync2, realpathSync } from "node:fs";
6880
+ import {
6881
+ createRequire as createRequire4,
6882
+ registerHooks,
6883
+ stripTypeScriptTypes
6884
+ } from "node:module";
6885
+ import { dirname as dirname9, sep as sep2 } from "node:path";
6886
+ import { fileURLToPath, pathToFileURL as pathToFileURL2 } from "node:url";
6887
+ var roots = /* @__PURE__ */ new Set();
6888
+ var registered = false;
6889
+ async function importModuleSource(entry, moduleRoot) {
6890
+ roots.add(realpathSync(moduleRoot));
6891
+ try {
6892
+ const require2 = createRequire4(pathToFileURL2(entry));
6893
+ roots.add(
6894
+ realpathSync(dirname9(require2.resolve("@flowdular/sdk/package.json")))
6895
+ );
6896
+ } catch (error) {
6897
+ if (!["MODULE_NOT_FOUND", "ERR_PACKAGE_PATH_NOT_EXPORTED"].includes(
6898
+ error.code ?? ""
6899
+ ))
6900
+ throw error;
6901
+ }
6902
+ if (!registered) {
6903
+ registerHooks({
6904
+ load(url, context, nextLoad) {
6905
+ if (!url.startsWith("file:") || !fileURLToPath(url).endsWith(".ts"))
6906
+ return nextLoad(url, context);
6907
+ const path = realpathSync(fileURLToPath(url));
6908
+ if (![...roots].some((root) => path.startsWith(root + sep2)))
6909
+ return nextLoad(url, context);
6910
+ return {
6911
+ format: "module",
6912
+ source: stripTypeScriptTypes(readFileSync2(path, "utf8"), {
6913
+ mode: "transform",
6914
+ sourceUrl: url
6915
+ }),
6916
+ shortCircuit: true
6917
+ };
6918
+ }
6919
+ });
6920
+ registered = true;
6921
+ }
6922
+ return import(pathToFileURL2(entry).href);
6923
+ }
6924
+
6925
+ // src/extensions.ts
6797
6926
  var reservedGroups = /* @__PURE__ */ new Set([
6798
6927
  "help",
6799
6928
  "doctor",
@@ -6849,7 +6978,10 @@ function commandKey(command) {
6849
6978
  return JSON.stringify({ path: command.path, capability: command.capability });
6850
6979
  }
6851
6980
  async function loadCliCommand(loaded) {
6852
- const imported = await import(pathToFileURL2(loaded.entry).href);
6981
+ const imported = await importModuleSource(
6982
+ loaded.entry,
6983
+ loaded.moduleRoot
6984
+ );
6853
6985
  const extension = imported.default ?? imported.cliExtension;
6854
6986
  if (!extension) {
6855
6987
  throw new Error(
@@ -6882,7 +7014,7 @@ async function loadCliExtensions(workspace) {
6882
7014
  await readFile11(manifestPath, "utf8")
6883
7015
  );
6884
7016
  if (!enabled.has(manifest2.id) || !manifest2.cli) continue;
6885
- const moduleRoot = dirname9(manifestPath);
7017
+ const moduleRoot = dirname10(manifestPath);
6886
7018
  const entry = await resolveExistingInside(moduleRoot, manifest2.cli.entry);
6887
7019
  const catalogPath = await resolveExistingInside(
6888
7020
  moduleRoot,
@@ -6919,9 +7051,8 @@ async function loadCliExtensions(workspace) {
6919
7051
  }
6920
7052
 
6921
7053
  // src/migration.ts
6922
- import { readFile as readFile13 } from "node:fs/promises";
6923
- import { dirname as dirname10, join as join11 } from "node:path";
6924
- import { pathToFileURL as pathToFileURL3 } from "node:url";
7054
+ import { readFile as readFile13, stat as stat2 } from "node:fs/promises";
7055
+ import { dirname as dirname11, join as join11 } from "node:path";
6925
7056
 
6926
7057
  // src/migration-audit.ts
6927
7058
  import { readdir as readdir5, readFile as readFile12 } from "node:fs/promises";
@@ -7085,13 +7216,17 @@ async function loadMigrationModules(workspace, moduleId) {
7085
7216
  );
7086
7217
  if (!enabled.has(manifest2.id)) continue;
7087
7218
  if (moduleId && manifest2.id !== moduleId) continue;
7088
- const entry = join11(dirname10(manifestPath), "src/services/migration.ts");
7089
- let databaseMigrations;
7219
+ const entry = join11(dirname11(manifestPath), "src/services/migration.ts");
7090
7220
  try {
7091
- ({ databaseMigrations } = await import(pathToFileURL3(entry).href));
7092
- } catch {
7093
- continue;
7221
+ await stat2(entry);
7222
+ } catch (error) {
7223
+ if (error.code === "ENOENT") continue;
7224
+ throw error;
7094
7225
  }
7226
+ const { databaseMigrations } = await importModuleSource(
7227
+ entry,
7228
+ dirname11(manifestPath)
7229
+ );
7095
7230
  if (!Array.isArray(databaseMigrations)) {
7096
7231
  unmanaged.push({
7097
7232
  moduleId: manifest2.id,
@@ -7101,7 +7236,7 @@ async function loadMigrationModules(workspace, moduleId) {
7101
7236
  }
7102
7237
  managed.push({
7103
7238
  moduleId: manifest2.id,
7104
- moduleDirectory: dirname10(manifestPath),
7239
+ moduleDirectory: dirname11(manifestPath),
7105
7240
  databaseMigrations
7106
7241
  });
7107
7242
  }
@@ -7328,7 +7463,7 @@ import {
7328
7463
  rm as rm3,
7329
7464
  writeFile as writeFile4
7330
7465
  } from "node:fs/promises";
7331
- import { dirname as dirname11, join as join12, relative as relative8, resolve as resolve5 } from "node:path";
7466
+ import { dirname as dirname12, join as join12, relative as relative8, resolve as resolve5 } from "node:path";
7332
7467
  import { parse as parseYaml4 } from "yaml";
7333
7468
 
7334
7469
  // src/module-templates.ts
@@ -8697,7 +8832,7 @@ async function writeScaffold(moduleDirectory, files, target) {
8697
8832
  for (const [path, source] of files) {
8698
8833
  if (target.present.has(path)) continue;
8699
8834
  const absolute = join12(moduleDirectory, path);
8700
- const created = await mkdir4(dirname11(absolute), { recursive: true });
8835
+ const created = await mkdir4(dirname12(absolute), { recursive: true });
8701
8836
  if (created) createdDirectories.push(created);
8702
8837
  await writeFile4(absolute, source, { encoding: "utf8", flag: "wx" });
8703
8838
  written.push(absolute);
@@ -8797,9 +8932,9 @@ import {
8797
8932
  mkdir as mkdir5,
8798
8933
  readFile as readFile15,
8799
8934
  rm as rm4,
8800
- stat as stat2
8935
+ stat as stat3
8801
8936
  } from "node:fs/promises";
8802
- import { dirname as dirname12, join as join13, relative as relative9 } from "node:path";
8937
+ import { dirname as dirname13, join as join13, relative as relative9 } from "node:path";
8803
8938
  import { createHash as createHash3 } from "node:crypto";
8804
8939
 
8805
8940
  // ../kernel/src/legacy-local-state.ts
@@ -8923,7 +9058,7 @@ async function ensurePrivateDirectory(path) {
8923
9058
  await chmod(path, 448);
8924
9059
  }
8925
9060
  async function copyPlan(plan) {
8926
- await ensurePrivateDirectory(dirname12(plan.destinationDirectory));
9061
+ await ensurePrivateDirectory(dirname13(plan.destinationDirectory));
8927
9062
  await ensurePrivateDirectory(plan.destinationDirectory);
8928
9063
  const created = [];
8929
9064
  try {
@@ -8932,14 +9067,14 @@ async function copyPlan(plan) {
8932
9067
  if (sourceState.isSymbolicLink() || !sourceState.isFile()) {
8933
9068
  throw new Error(`${file.name} is no longer a regular file.`);
8934
9069
  }
8935
- const before = await stat2(file.source);
9070
+ const before = await stat3(file.source);
8936
9071
  await copyFile(file.source, file.destination, constants.COPYFILE_EXCL);
8937
9072
  created.push(file.destination);
8938
9073
  await chmod(file.destination, 384);
8939
9074
  const [sourceDigest2, destinationDigest, after] = await Promise.all([
8940
9075
  digest(file.source),
8941
9076
  digest(file.destination),
8942
- stat2(file.source)
9077
+ stat3(file.source)
8943
9078
  ]);
8944
9079
  if (sourceDigest2 !== destinationDigest || before.size !== after.size || before.mtimeMs !== after.mtimeMs) {
8945
9080
  throw new Error(`Source changed while copying ${file.name}.`);
@@ -8979,7 +9114,7 @@ function data(plan, applied) {
8979
9114
  ready: plan.blockers.length === 0,
8980
9115
  source: LEGACY_DATA_DIRECTORY,
8981
9116
  destination: relative9(
8982
- dirname12(dirname12(plan.destinationDirectory)),
9117
+ dirname13(dirname13(plan.destinationDirectory)),
8983
9118
  plan.destinationDirectory
8984
9119
  ),
8985
9120
  files: plan.files.map((file) => file.name),
@@ -9163,7 +9298,7 @@ async function runCommand(arguments_) {
9163
9298
  "module list|validate [--locked]|sync [--apply]|enable <id> [--apply]|disable <id> [--apply]|new <id> --spec <path> [--apply]",
9164
9299
  "migration status [--module <id>]|apply --module <id> [--apply]|verify|new <name> --module <id> [--apply]",
9165
9300
  "database reset [--module <id>] [--apply --confirm reset-database]",
9166
- "setup check|quick [--apply --confirm reset-local-auth]|migrate-state [--apply --confirm migrate-legacy-state]",
9301
+ "setup (interactive)|check|quick [--apply --confirm reset-local-auth]|migrate-state [--apply --confirm migrate-legacy-state]",
9167
9302
  ...extensionCommands.map((entry) => entry.command.path.join(" "))
9168
9303
  ],
9169
9304
  options: [
@@ -9178,10 +9313,37 @@ async function runCommand(arguments_) {
9178
9313
  const greenfield = extensionCommands.find(
9179
9314
  (entry) => entry.command.capability.id === "auth.greenfield.reset"
9180
9315
  );
9181
- return greenfield ? await runExtensionCommand(workspace, greenfield, arguments_) : failure(
9182
- "AUTH_MODULE_REQUIRED",
9183
- "Quick setup requires the enabled auth.core module."
9316
+ if (!greenfield)
9317
+ return failure(
9318
+ "AUTH_MODULE_REQUIRED",
9319
+ "Quick setup requires the enabled auth.core module."
9320
+ );
9321
+ const result2 = await runExtensionCommand(
9322
+ workspace,
9323
+ greenfield,
9324
+ arguments_
9184
9325
  );
9326
+ if (!result2.ok || !arguments_.flags.has("apply")) return result2;
9327
+ const sync = extensionCommands.find(
9328
+ (entry) => entry.command.capability.id === "auth.scopes.sync"
9329
+ );
9330
+ if (!sync)
9331
+ return failure(
9332
+ "AUTH_SCOPE_SYNC_REQUIRED",
9333
+ "The demo was created but module permissions could not be synchronized."
9334
+ );
9335
+ const enabled = workspace.config.modules.enabled ?? [];
9336
+ for (const moduleId of enabled) {
9337
+ const granted = await runExtensionCommand(workspace, sync, {
9338
+ positionals: ["auth", "sync-scopes"],
9339
+ flags: /* @__PURE__ */ new Map([
9340
+ ["module", moduleId],
9341
+ ["apply", true]
9342
+ ])
9343
+ });
9344
+ if (!granted.ok) return granted;
9345
+ }
9346
+ return result2;
9185
9347
  }
9186
9348
  if (group === "setup" && action === "migrate-state") {
9187
9349
  const descriptor = capability("workspace.state.migrate");
@@ -9281,13 +9443,13 @@ async function runCommand(arguments_) {
9281
9443
  );
9282
9444
  }
9283
9445
  if (group === "blueprint" && action === "list") {
9284
- const files = await findNamedFiles(workspace.root, "blueprint.json");
9446
+ const files = await findBlueprintFiles(workspace);
9285
9447
  return success({
9286
9448
  blueprints: files.map((file) => relative10(workspace.root, file))
9287
9449
  });
9288
9450
  }
9289
9451
  if (group === "blueprint" && action === "validate") {
9290
- const files = await findNamedFiles(workspace.root, "blueprint.json");
9452
+ const files = await findBlueprintFiles(workspace);
9291
9453
  const reports = await Promise.all(files.map(validateBlueprint));
9292
9454
  const data2 = relativeReports2(workspace.root, reports);
9293
9455
  return reports.every((report) => report.valid) ? success(
@@ -9584,13 +9746,215 @@ async function runProgram(arguments_) {
9584
9746
  }
9585
9747
  }
9586
9748
 
9749
+ // src/setup-wizard.ts
9750
+ import { stat as stat4 } from "node:fs/promises";
9751
+
9752
+ // src/setup-environment.ts
9753
+ import { randomUUID } from "node:crypto";
9754
+ import { lstat as lstat4, open, readFile as readFile17, rename as rename2, rm as rm5, writeFile as writeFile5 } from "node:fs/promises";
9755
+ import { join as join15 } from "node:path";
9756
+ async function readSetupEnvironment(root) {
9757
+ try {
9758
+ return await readFile17(join15(root, ".env"), "utf8");
9759
+ } catch (error) {
9760
+ if (error.code === "ENOENT") return "";
9761
+ throw error;
9762
+ }
9763
+ }
9764
+ async function saveSetupEnvironment(root, before, updates) {
9765
+ const path = join15(root, ".env");
9766
+ try {
9767
+ if (!(await lstat4(path)).isFile())
9768
+ throw new Error("Setup requires .env to be a regular file.");
9769
+ } catch (error) {
9770
+ if (error.code !== "ENOENT") throw error;
9771
+ }
9772
+ for (const [key, value] of Object.entries(updates)) {
9773
+ if (!/^FD_DATABASE_[A-Z_]+$/.test(key) || /[\r\n'\0]/.test(value))
9774
+ throw new Error("Invalid database setting.");
9775
+ }
9776
+ const lines = before.split(/\r?\n/).filter((line) => {
9777
+ const match = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/.exec(
9778
+ line
9779
+ );
9780
+ if (!match || !(match[1] in updates)) return true;
9781
+ const value = match[2].trim();
9782
+ if (value.startsWith('"') && !value.endsWith('"') || value.startsWith("'") && !value.endsWith("'"))
9783
+ throw new Error(
9784
+ "Edit multiline database settings in .env before using setup."
9785
+ );
9786
+ return false;
9787
+ });
9788
+ const source = lines.join("\n").trimEnd() + "\n" + Object.entries(updates).map(([key, value]) => `${key}='${value}'`).join("\n") + "\n";
9789
+ const temporary = join15(root, `.env.setup-${randomUUID()}.tmp`);
9790
+ const lockPath = join15(root, ".env.setup.lock");
9791
+ const lock = await open(lockPath, "wx", 384);
9792
+ try {
9793
+ await writeFile5(temporary, source, { mode: 384, flag: "wx" });
9794
+ if (await readSetupEnvironment(root) !== before)
9795
+ throw new Error("The .env file changed during setup. Run setup again.");
9796
+ await rename2(temporary, path);
9797
+ } finally {
9798
+ try {
9799
+ await rm5(temporary, { force: true });
9800
+ } finally {
9801
+ await lock.close();
9802
+ await rm5(lockPath, { force: true });
9803
+ }
9804
+ }
9805
+ }
9806
+
9807
+ // src/setup-wizard.ts
9808
+ var prompts = {
9809
+ async choose() {
9810
+ const { select } = await import("@inquirer/prompts");
9811
+ return select({
9812
+ message: "How would you like to set up this application?",
9813
+ choices: [
9814
+ {
9815
+ name: "Local demo with embedded PostgreSQL (PGlite)",
9816
+ value: "local"
9817
+ },
9818
+ {
9819
+ name: "Configure an existing PostgreSQL database",
9820
+ value: "postgresql"
9821
+ },
9822
+ { name: "Check configuration only", value: "check" }
9823
+ ]
9824
+ });
9825
+ },
9826
+ async confirm(message) {
9827
+ const { confirm } = await import("@inquirer/prompts");
9828
+ return confirm({ message, default: false });
9829
+ },
9830
+ async connection(message) {
9831
+ const { password } = await import("@inquirer/prompts");
9832
+ return password({
9833
+ message,
9834
+ mask: "*",
9835
+ validate: (value) => validConnection(value) ? true : "Use postgres://user:password@host/database without SSL query parameters."
9836
+ });
9837
+ }
9838
+ };
9839
+ function validConnection(value) {
9840
+ if (value.length > 4096 || /[\r\n'\0]/.test(value)) return false;
9841
+ try {
9842
+ const url = new URL(value);
9843
+ return ["postgres:", "postgresql:"].includes(url.protocol) && Boolean(url.hostname && url.username && url.pathname.length > 1) && !["sslmode", "sslcert", "sslkey", "sslrootcert"].some(
9844
+ (key) => url.searchParams.has(key)
9845
+ );
9846
+ } catch {
9847
+ return false;
9848
+ }
9849
+ }
9850
+ async function runSetupWizard(arguments_, io = prompts, execute = runProgram) {
9851
+ try {
9852
+ const flags = new Map(arguments_.flags);
9853
+ flags.delete("apply");
9854
+ flags.delete("confirm");
9855
+ const check = await execute({ positionals: ["setup", "check"], flags });
9856
+ if (!check.ok) return check;
9857
+ const workspace = await findWorkspace(
9858
+ stringFlag(arguments_, "root") ?? process.cwd()
9859
+ );
9860
+ const before = await readSetupEnvironment(workspace.root);
9861
+ const choice = await io.choose();
9862
+ if (choice === "check") return check;
9863
+ if (choice === "postgresql") {
9864
+ const urls = {
9865
+ FD_DATABASE_URL: await io.connection("Runtime role connection URL"),
9866
+ FD_DATABASE_MIGRATOR_URL: await io.connection(
9867
+ "Migration role connection URL"
9868
+ ),
9869
+ FD_DATABASE_BACKGROUND_URL: await io.connection(
9870
+ "Background role connection URL"
9871
+ )
9872
+ };
9873
+ if (!Object.values(urls).every(validConnection))
9874
+ return failure(
9875
+ "INVALID_DATABASE_CONFIGURATION",
9876
+ "Invalid PostgreSQL connection settings."
9877
+ );
9878
+ const updates = {
9879
+ ...urls,
9880
+ FD_DATABASE_ADAPTER: "postgresql",
9881
+ FD_DATABASE_TLS: "verify-full"
9882
+ };
9883
+ databaseProviderConfigFromEnvironment(
9884
+ { ...process.env, ...updates, NODE_ENV: "production" },
9885
+ workspace.root
9886
+ );
9887
+ if (!await io.confirm(
9888
+ "Save PostgreSQL settings in .env? This does not reset or modify the hosted database."
9889
+ ))
9890
+ return success({ cancelled: true });
9891
+ await saveSetupEnvironment(workspace.root, before, updates);
9892
+ return success(
9893
+ {
9894
+ setup: "postgresql",
9895
+ configured: true,
9896
+ adapter: "postgresql",
9897
+ next: "Run pnpm dev, then open http://localhost:4310 and create an account."
9898
+ },
9899
+ {
9900
+ warnings: [
9901
+ "TLS certificate verification is enabled. The database must provide separate migration, runtime and background roles."
9902
+ ]
9903
+ }
9904
+ );
9905
+ }
9906
+ const config = databaseProviderConfigFromEnvironment(
9907
+ process.env,
9908
+ workspace.root
9909
+ );
9910
+ if (config.adapter !== "pglite" || process.env.FD_DATABASE_PGLITE_DIRECTORY || process.env.CORELOOM_DATABASE_PGLITE_DIRECTORY || process.env.FD_DATABASE_URL || process.env.CORELOOM_DATABASE_URL)
9911
+ return failure(
9912
+ "LOCAL_DEMO_UNAVAILABLE",
9913
+ "Local demo setup requires the default embedded database. The configured database will not be reset."
9914
+ );
9915
+ const preview = await execute({ positionals: ["setup", "quick"], flags });
9916
+ if (!preview.ok) return preview;
9917
+ const directory = config.pglite?.dataDirectory;
9918
+ let existing = false;
9919
+ if (directory)
9920
+ try {
9921
+ await stat4(directory);
9922
+ existing = true;
9923
+ } catch (error) {
9924
+ if (error.code !== "ENOENT") throw error;
9925
+ }
9926
+ const message = existing ? `Reset the local demo database in ${directory}? All existing local data will be deleted. Confirm that the app is stopped.` : "Create a local demo with admin@example.com? Confirm that the app is stopped.";
9927
+ if (!await io.confirm(message)) return success({ cancelled: true });
9928
+ if (await readSetupEnvironment(workspace.root) !== before)
9929
+ return failure(
9930
+ "SETUP_CONFIGURATION_CHANGED",
9931
+ "The .env file changed during setup. Run setup again."
9932
+ );
9933
+ flags.set("apply", true);
9934
+ flags.set("confirm", "reset-local-auth");
9935
+ const applied = await execute({ positionals: ["setup", "quick"], flags });
9936
+ return applied.ok ? success(
9937
+ { ...applied.data, setup: "local" },
9938
+ { warnings: applied.warnings, evidence: applied.evidence }
9939
+ ) : applied;
9940
+ } catch (error) {
9941
+ if (error instanceof Error && ["ExitPromptError", "AbortPromptError"].includes(error.name))
9942
+ return success({ cancelled: true });
9943
+ return failure(
9944
+ "SETUP_FAILED",
9945
+ error instanceof Error ? error.message : "Setup failed."
9946
+ );
9947
+ }
9948
+ }
9949
+
9587
9950
  // src/index.ts
9588
9951
  var invokedAsProgram = process.argv[1]?.endsWith("/index.ts") || process.argv[1]?.endsWith("/index.js");
9589
9952
  if (invokedAsProgram) {
9590
9953
  const arguments_ = parseArguments(process.argv.slice(2));
9591
- const envelope = await runProgram(arguments_);
9954
+ const interactive = arguments_.positionals.length === 1 && arguments_.positionals[0] === "setup" && !arguments_.flags.has("json") && !arguments_.flags.has("help") && process.stdin.isTTY && process.stdout.isTTY;
9955
+ const envelope = await (interactive ? runSetupWizard(arguments_) : runProgram(arguments_));
9592
9956
  process.stdout.write(
9593
- `${renderOutput(envelope, arguments_.flags.has("json"))}
9957
+ `${renderOutput(envelope, arguments_.flags.has("json"), Boolean(process.stdout.isTTY && process.stdout.hasColors?.() && !("NO_COLOR" in process.env) && !process.env.CI))}
9594
9958
  `
9595
9959
  );
9596
9960
  if (!envelope.ok) process.exitCode = 1;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flowdular",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "flowdular": "./dist/index.js",
@@ -12,10 +12,11 @@
12
12
  },
13
13
  "dependencies": {
14
14
  "@electric-sql/pglite": "0.5.8",
15
+ "@inquirer/prompts": "8.7.2",
15
16
  "ajv": "8.20.0",
16
17
  "pg": "8.23.0",
17
18
  "yaml": "2.9.0",
18
- "@flowdular/sdk": "0.2.0"
19
+ "@flowdular/sdk": "0.2.2"
19
20
  },
20
21
  "license": "MIT",
21
22
  "repository": {