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 +13 -0
- package/dist/distribution.js +1 -1
- package/dist/index.js +600 -236
- package/package.json +3 -2
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.
|
package/dist/distribution.js
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
2003
|
-
|
|
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
|
-
//
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
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
|
-
|
|
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
|
|
2125
|
-
const
|
|
2126
|
-
const
|
|
2127
|
-
|
|
2128
|
-
|
|
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
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
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
|
|
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
|
|
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 =
|
|
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:
|
|
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.
|
|
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
|
|
3595
|
-
if (!Array.isArray(
|
|
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
|
|
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
|
|
4074
|
+
const registered2 = manifests.map(
|
|
4011
4075
|
(manifest2) => ({ manifest: manifest2 })
|
|
4012
4076
|
);
|
|
4013
|
-
const registry = createModuleRegistry(
|
|
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
|
|
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
|
-
|
|
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,
|
|
6398
|
-
const requested = new Set(
|
|
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,
|
|
6743
|
-
["policy.capabilities", "
|
|
6744
|
-
["policy.model-routing", "
|
|
6745
|
-
["blueprints.root", "
|
|
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
|
|
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
|
|
6796
|
-
|
|
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
|
|
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 =
|
|
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
|
|
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(
|
|
7089
|
-
let databaseMigrations;
|
|
7219
|
+
const entry = join11(dirname11(manifestPath), "src/services/migration.ts");
|
|
7090
7220
|
try {
|
|
7091
|
-
|
|
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:
|
|
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
|
|
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(
|
|
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
|
|
8935
|
+
stat as stat3
|
|
8801
8936
|
} from "node:fs/promises";
|
|
8802
|
-
import { dirname as
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
9182
|
-
|
|
9183
|
-
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
19
|
+
"@flowdular/sdk": "0.2.2"
|
|
19
20
|
},
|
|
20
21
|
"license": "MIT",
|
|
21
22
|
"repository": {
|