@nanobpm/urban 0.34.0 → 0.36.0
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/dist/cli.d.ts +0 -1
- package/dist/cli.js +32 -47
- package/dist/openapi/spec.d.ts +63 -0
- package/dist/openapi/spec.js +168 -0
- package/dist/runtime/core/modules/api.d.ts +19 -4
- package/dist/runtime/core/modules/api.js +99 -4
- package/dist/runtime/core/runtime.d.ts +10 -0
- package/dist/runtime/core/runtime.js +2 -2
- package/dist/runtime/index.d.ts +3 -1
- package/dist/runtime/index.js +2 -1
- package/dist/toolkit/api-dir.d.ts +11 -0
- package/dist/toolkit/api-dir.js +36 -0
- package/dist/toolkit/derivers/api.d.ts +25 -2
- package/dist/toolkit/derivers/api.js +60 -5
- package/dist/toolkit/gen.js +4 -1
- package/dist/toolkit/generate.d.ts +27 -0
- package/dist/toolkit/generate.js +47 -0
- package/dist/toolkit/index.d.ts +8 -1
- package/dist/toolkit/index.js +5 -1
- package/dist/toolkit/scaffold/operations.d.ts +21 -0
- package/dist/toolkit/scaffold/operations.js +55 -0
- package/dist/toolkit/scaffold/workers.js +1 -1
- package/dist/toolkit/scaffold.d.ts +16 -0
- package/dist/toolkit/scaffold.js +40 -0
- package/package.json +74 -74
- package/src/cli.test.ts +0 -1
- package/src/cli.ts +30 -45
- package/src/openapi/spec.test.ts +171 -0
- package/src/openapi/spec.ts +219 -1
- package/src/runtime/core/modules/api.test.ts +183 -5
- package/src/runtime/core/modules/api.ts +125 -8
- package/src/runtime/core/runtime.test.ts +53 -1
- package/src/runtime/core/runtime.ts +12 -2
- package/src/runtime/index.ts +3 -1
- package/src/toolkit/api-dir.test.ts +32 -0
- package/src/toolkit/api-dir.ts +41 -0
- package/src/toolkit/derivers/api.test.ts +41 -3
- package/src/toolkit/derivers/api.ts +75 -5
- package/src/toolkit/gen.ts +5 -2
- package/src/toolkit/generate.test.ts +135 -0
- package/src/toolkit/generate.ts +71 -0
- package/src/toolkit/index.ts +10 -0
- package/src/toolkit/scaffold/operations.test.ts +136 -0
- package/src/toolkit/scaffold/operations.ts +75 -0
- package/src/toolkit/scaffold/workers.ts +1 -1
- package/src/toolkit/scaffold.ts +56 -0
|
@@ -2,6 +2,7 @@ import type { EngineClient, HostContext } from "./host.ts";
|
|
|
2
2
|
import { type AppManifest } from "./manifest.ts";
|
|
3
3
|
import type { TemplateSource } from "./modules/templates.ts";
|
|
4
4
|
import { DataLayer } from "./modules/datasource.ts";
|
|
5
|
+
import type { SchedulerDeps } from "./modules/scheduler.ts";
|
|
5
6
|
import { type SecurityPolicy } from "./modules/security.ts";
|
|
6
7
|
/** Resolve the HTTP port: explicit option, else $PORT, else 8090. Throws a clear
|
|
7
8
|
* error when $PORT is set but not a valid integer in 0..65535. */
|
|
@@ -34,6 +35,15 @@ export interface CreateUrbanAppOptions {
|
|
|
34
35
|
* `name → content` map). Merged over the manifest's `models.templates` (this wins on
|
|
35
36
|
* collision). See `deployModels`. */
|
|
36
37
|
templates?: TemplateSource;
|
|
38
|
+
/**
|
|
39
|
+
* Injectable timer + clock seam driving every background loop (the cron trigger loops
|
|
40
|
+
* and the instance-tracking reconciler). Defaults to the live scheduler (real timers,
|
|
41
|
+
* wall clock). The e2e test kit injects a manual scheduler so those loops advance
|
|
42
|
+
* deterministically over a virtual clock — the single seam that makes a whole-app
|
|
43
|
+
* `settle()` possible. Threaded verbatim into `mountTriggers` and `mountInstanceTracking`
|
|
44
|
+
* so one injected scheduler drives them both (no per-module wiring, no drift).
|
|
45
|
+
*/
|
|
46
|
+
scheduler?: SchedulerDeps;
|
|
37
47
|
}
|
|
38
48
|
export interface UrbanApp {
|
|
39
49
|
readonly manifest: AppManifest;
|
|
@@ -146,13 +146,13 @@ export async function createUrbanApp(opts) {
|
|
|
146
146
|
describe.surfaces = s.describe();
|
|
147
147
|
}
|
|
148
148
|
if (flags.triggers) {
|
|
149
|
-
const t = mountTriggers(ctx, api);
|
|
149
|
+
const t = mountTriggers(ctx, api, opts.scheduler);
|
|
150
150
|
routes.push(...t.routes);
|
|
151
151
|
mounted.push(t);
|
|
152
152
|
describe.triggers = t.describe?.();
|
|
153
153
|
}
|
|
154
154
|
if (flags.instanceTracking && (manifest.instanceTracking?.length ?? 0) > 0) {
|
|
155
|
-
const it = mountInstanceTracking(ctx, api);
|
|
155
|
+
const it = mountInstanceTracking(ctx, api, opts.scheduler);
|
|
156
156
|
mounted.push(it);
|
|
157
157
|
describe.instanceTracking = it.describe?.();
|
|
158
158
|
}
|
package/dist/runtime/index.d.ts
CHANGED
|
@@ -21,7 +21,7 @@ export { cancelInstanceReconciling } from "./core/modules/cancel.ts";
|
|
|
21
21
|
export type { CancelInstanceResult, CancelInstanceState } from "./core/modules/cancel.ts";
|
|
22
22
|
export { mountActions, resolveActionHandler } from "./core/modules/actions.ts";
|
|
23
23
|
export type { ActionHandler, ActionRequest, ActionResult, ActionsHandle } from "./core/modules/actions.ts";
|
|
24
|
-
export { defineOperation, mountApi, resolveOperationHandler } from "./core/modules/api.ts";
|
|
24
|
+
export { defineOperation, mountApi, resolveOperationHandler, NotImplemented } from "./core/modules/api.ts";
|
|
25
25
|
export type { ApiBinding, ApiHandle, DefaultContract, OperationContract, OperationHandler, OperationInput, OperationResult, } from "./core/modules/api.ts";
|
|
26
26
|
export type { AppJobHandler } from "./core/modules/workers.ts";
|
|
27
27
|
export { resolveHandler, sdkDecisionEvaluator } from "./core/modules/workers.ts";
|
|
@@ -31,6 +31,8 @@ export { evalCorrelation } from "./core/modules/triggers.ts";
|
|
|
31
31
|
export { deployModels } from "./core/modules/deploy.ts";
|
|
32
32
|
export { applyTemplates, resolveTemplates } from "./core/modules/templates.ts";
|
|
33
33
|
export type { TemplateApplication, TemplateSource } from "./core/modules/templates.ts";
|
|
34
|
+
export { defaultScheduler, MAX_TIMER_DELAY_MS } from "./core/modules/scheduler.ts";
|
|
35
|
+
export type { SchedulerDeps } from "./core/modules/scheduler.ts";
|
|
34
36
|
export { createNodeHost } from "./adapters/node.ts";
|
|
35
37
|
export { createDenoHost } from "./adapters/deno.ts";
|
|
36
38
|
export { isDeno, selectHost } from "./adapters/detect.ts";
|
package/dist/runtime/index.js
CHANGED
|
@@ -17,13 +17,14 @@ export { makeGateway, Table } from "./core/modules/gateway.js";
|
|
|
17
17
|
export { createPagesRoutes, mountPages } from "./core/modules/pages.js";
|
|
18
18
|
export { cancelInstanceReconciling } from "./core/modules/cancel.js";
|
|
19
19
|
export { mountActions, resolveActionHandler } from "./core/modules/actions.js";
|
|
20
|
-
export { defineOperation, mountApi, resolveOperationHandler } from "./core/modules/api.js";
|
|
20
|
+
export { defineOperation, mountApi, resolveOperationHandler, NotImplemented } from "./core/modules/api.js";
|
|
21
21
|
export { resolveHandler, sdkDecisionEvaluator } from "./core/modules/workers.js";
|
|
22
22
|
export { buildMessages, callLlm, resolveProvider, runLlmJob, } from "./core/modules/llm.js";
|
|
23
23
|
export { evalCorrelation } from "./core/modules/triggers.js";
|
|
24
24
|
// Model deploy + `{{name}}` template substitution
|
|
25
25
|
export { deployModels } from "./core/modules/deploy.js";
|
|
26
26
|
export { applyTemplates, resolveTemplates } from "./core/modules/templates.js";
|
|
27
|
+
export { defaultScheduler, MAX_TIMER_DELAY_MS } from "./core/modules/scheduler.js";
|
|
27
28
|
// Adapters + engine + run entrypoint
|
|
28
29
|
export { createNodeHost } from "./adapters/node.js";
|
|
29
30
|
export { createDenoHost } from "./adapters/deno.js";
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** The default directory operation delegates live in (mirrors the runtime's `api.dir` default). */
|
|
2
|
+
export declare const DEFAULT_OPERATIONS_DIR = "operations";
|
|
3
|
+
/**
|
|
4
|
+
* Normalize an `api.dir` to a clean, app-relative directory segment (no leading/trailing slashes,
|
|
5
|
+
* backslashes folded to `/`). An empty value falls back to {@link DEFAULT_OPERATIONS_DIR}.
|
|
6
|
+
*
|
|
7
|
+
* Throws if `dir` is absolute (POSIX `/…` or Windows `C:\…`/`\…`) or contains a `..` traversal
|
|
8
|
+
* segment — both would let the generated controller's import paths or the scaffolder's write target
|
|
9
|
+
* diverge from (or escape) the app root.
|
|
10
|
+
*/
|
|
11
|
+
export declare function normalizeApiDir(dir: string): string;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// Shared normalization + validation for the OpenAPI surface's `api.dir` (the directory operation
|
|
2
|
+
// delegates live in). Used by BOTH the deriver (which bakes `<dir>/<operationId>` into the generated
|
|
3
|
+
// controller's static import paths) and the scaffolder (which writes stubs to `<dir>/<operationId>.ts`)
|
|
4
|
+
// so the two can never disagree on where a delegate lives — a single rule, one place.
|
|
5
|
+
//
|
|
6
|
+
// The rule is intentionally strict: `api.dir` must be a safe app-relative directory. An absolute path
|
|
7
|
+
// or a `..` segment is REJECTED with a clear error rather than silently rewritten, because a silent
|
|
8
|
+
// rewrite would let `urban gen` scaffold into one directory while the runtime resolves delegates
|
|
9
|
+
// from another (drift), and a `..` segment could escape the app root when later joined.
|
|
10
|
+
/** The default directory operation delegates live in (mirrors the runtime's `api.dir` default). */
|
|
11
|
+
export const DEFAULT_OPERATIONS_DIR = "operations";
|
|
12
|
+
/**
|
|
13
|
+
* Normalize an `api.dir` to a clean, app-relative directory segment (no leading/trailing slashes,
|
|
14
|
+
* backslashes folded to `/`). An empty value falls back to {@link DEFAULT_OPERATIONS_DIR}.
|
|
15
|
+
*
|
|
16
|
+
* Throws if `dir` is absolute (POSIX `/…` or Windows `C:\…`/`\…`) or contains a `..` traversal
|
|
17
|
+
* segment — both would let the generated controller's import paths or the scaffolder's write target
|
|
18
|
+
* diverge from (or escape) the app root.
|
|
19
|
+
*/
|
|
20
|
+
export function normalizeApiDir(dir) {
|
|
21
|
+
const raw = dir.trim();
|
|
22
|
+
if (raw.length === 0)
|
|
23
|
+
return DEFAULT_OPERATIONS_DIR;
|
|
24
|
+
if (isAbsolutePath(raw)) {
|
|
25
|
+
throw new Error(`api.dir must be an app-relative directory, not an absolute path: ${dir}`);
|
|
26
|
+
}
|
|
27
|
+
const cleaned = raw.replace(/\\/g, "/").replace(/^\.?\/+/, "").replace(/\/+$/, "");
|
|
28
|
+
if (cleaned.split("/").some((seg) => seg === "..")) {
|
|
29
|
+
throw new Error(`api.dir must not contain ".." path segments: ${dir}`);
|
|
30
|
+
}
|
|
31
|
+
return cleaned.length > 0 ? cleaned : DEFAULT_OPERATIONS_DIR;
|
|
32
|
+
}
|
|
33
|
+
/** True for a POSIX absolute path (`/…`) or a Windows absolute/drive path (`C:\…`, `C:/…`, `\…`). */
|
|
34
|
+
function isAbsolutePath(path) {
|
|
35
|
+
return /^([/\\]|[A-Za-z]:[/\\])/.test(path);
|
|
36
|
+
}
|
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import type { DerivedArtifact, Deriver } from "../artifact.ts";
|
|
2
|
+
import { DEFAULT_OPERATIONS_DIR } from "../api-dir.ts";
|
|
2
3
|
import { type OpenApiDoc, type OpenApiSchema } from "../../openapi/spec.ts";
|
|
3
4
|
/** Filenames of the emitted artifacts (siblings of worker-io.d.ts in nano-generated/). */
|
|
4
5
|
export declare const API_BINDINGS_DTS = "api-io.d.ts";
|
|
5
6
|
export declare const API_BINDINGS_TS = "operations.ts";
|
|
7
|
+
/** The generated delegate registry: statically imports every operation delegate and asserts the
|
|
8
|
+
* complete, correctly-typed set via `satisfies` — so a missing/mismatched/orphan delegate fails
|
|
9
|
+
* `tsc` (ADR 0059). The runtime dispatches through this map instead of per-op path imports. */
|
|
10
|
+
export declare const API_CONTROLLER_TS = "controller.ts";
|
|
11
|
+
export { DEFAULT_OPERATIONS_DIR };
|
|
6
12
|
/** Render a JSON-Schema (OpenAPI subset) as a TS type expression. Local component `$ref`s render as
|
|
7
13
|
* their component name (emitted as named interfaces); unknown shapes fall back to `unknown`. */
|
|
8
14
|
export declare function schemaToTs(schema: OpenApiSchema | undefined, depth?: number): string;
|
|
@@ -12,8 +18,25 @@ export declare function emitApiBindings(doc: OpenApiDoc): string;
|
|
|
12
18
|
* generated typed `defineWorker`). Written verbatim (it never changes with the spec) — it only
|
|
13
19
|
* re-types the runtime's `defineOperation`. */
|
|
14
20
|
export declare function emitApiBindingsRuntime(): string;
|
|
15
|
-
/**
|
|
16
|
-
|
|
21
|
+
/**
|
|
22
|
+
* Emit `controller.ts`: the generated delegate registry (ADR 0059). It statically imports every
|
|
23
|
+
* operation delegate (`<dir>/<operationId>.ts`) and asserts the whole set against the spec-derived
|
|
24
|
+
* contract with a single `satisfies { [K in ApiOperationId]: OperationHandler<ReqFor<K>, ResFor<K>> }`.
|
|
25
|
+
* That one assertion gives four compile-time drift guarantees, all surfaced by `tsc`:
|
|
26
|
+
* • a missing delegate file → the static `import` fails ("Cannot find module");
|
|
27
|
+
* • an op in the spec, missing here → the mapped type requires key `K` ("Property is missing");
|
|
28
|
+
* • a delegate with the wrong signature → not assignable to `OperationHandler<Req, Res>`;
|
|
29
|
+
* • an orphan delegate (no spec op) → an excess property vs the mapped type.
|
|
30
|
+
* The runtime imports this ONE barrel and dispatches via `operations[operationId]`, so existing
|
|
31
|
+
* endpoints cannot drift from the contract. Delegates are keyed by their real operationId; the
|
|
32
|
+
* import binding is index-based (`op0`, `op1`, …) because an operationId is a safe path segment but
|
|
33
|
+
* not necessarily a valid TS identifier. Requires each delegate to be a default export.
|
|
34
|
+
*/
|
|
35
|
+
export declare function emitApiController(doc: OpenApiDoc, operationsDir?: string): string;
|
|
36
|
+
/** Derive the api artifacts from a parsed OpenAPI document. `operationsDir` is the manifest's
|
|
37
|
+
* `api.dir` (default `operations/`) — the controller's static delegate imports resolve into it. */
|
|
38
|
+
export declare function deriveApi(doc: OpenApiDoc, operationsDir?: string): DerivedArtifact[];
|
|
17
39
|
export declare const apiDeriver: Deriver<{
|
|
18
40
|
doc: OpenApiDoc;
|
|
41
|
+
operationsDir?: string;
|
|
19
42
|
}>;
|
|
@@ -6,10 +6,16 @@
|
|
|
6
6
|
// (`runtime/core/modules/api.ts`) is the matching half: it validates + routes from the SAME spec via
|
|
7
7
|
// the shared `openapi/spec.ts`, so types and runtime never drift.
|
|
8
8
|
import { GENERATED_DIR } from "../artifact.js";
|
|
9
|
-
import {
|
|
9
|
+
import { DEFAULT_OPERATIONS_DIR, normalizeApiDir } from "../api-dir.js";
|
|
10
|
+
import { collectOperations, assertUniqueOperationIds, isObjectSchema, refName, } from "../../openapi/spec.js";
|
|
10
11
|
/** Filenames of the emitted artifacts (siblings of worker-io.d.ts in nano-generated/). */
|
|
11
12
|
export const API_BINDINGS_DTS = "api-io.d.ts";
|
|
12
13
|
export const API_BINDINGS_TS = "operations.ts";
|
|
14
|
+
/** The generated delegate registry: statically imports every operation delegate and asserts the
|
|
15
|
+
* complete, correctly-typed set via `satisfies` — so a missing/mismatched/orphan delegate fails
|
|
16
|
+
* `tsc` (ADR 0059). The runtime dispatches through this map instead of per-op path imports. */
|
|
17
|
+
export const API_CONTROLLER_TS = "controller.ts";
|
|
18
|
+
export { DEFAULT_OPERATIONS_DIR };
|
|
13
19
|
/** A TS identifier form of an operationId (used for the per-op type names). */
|
|
14
20
|
function typeStem(operationId) {
|
|
15
21
|
const cleaned = operationId.replace(/[^A-Za-z0-9]+/g, " ").trim();
|
|
@@ -210,15 +216,64 @@ export function emitApiBindingsRuntime() {
|
|
|
210
216
|
` return defineOperationRaw(id, handler);\n` +
|
|
211
217
|
`}\n`);
|
|
212
218
|
}
|
|
213
|
-
/**
|
|
214
|
-
|
|
219
|
+
/** Normalize an `api.dir` to a clean relative segment (see `normalizeApiDir`); rejects absolute
|
|
220
|
+
* paths and `..` segments so the generated import paths stay app-relative and can't drift. */
|
|
221
|
+
function normalizeOperationsDir(dir) {
|
|
222
|
+
return normalizeApiDir(dir);
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Emit `controller.ts`: the generated delegate registry (ADR 0059). It statically imports every
|
|
226
|
+
* operation delegate (`<dir>/<operationId>.ts`) and asserts the whole set against the spec-derived
|
|
227
|
+
* contract with a single `satisfies { [K in ApiOperationId]: OperationHandler<ReqFor<K>, ResFor<K>> }`.
|
|
228
|
+
* That one assertion gives four compile-time drift guarantees, all surfaced by `tsc`:
|
|
229
|
+
* • a missing delegate file → the static `import` fails ("Cannot find module");
|
|
230
|
+
* • an op in the spec, missing here → the mapped type requires key `K` ("Property is missing");
|
|
231
|
+
* • a delegate with the wrong signature → not assignable to `OperationHandler<Req, Res>`;
|
|
232
|
+
* • an orphan delegate (no spec op) → an excess property vs the mapped type.
|
|
233
|
+
* The runtime imports this ONE barrel and dispatches via `operations[operationId]`, so existing
|
|
234
|
+
* endpoints cannot drift from the contract. Delegates are keyed by their real operationId; the
|
|
235
|
+
* import binding is index-based (`op0`, `op1`, …) because an operationId is a safe path segment but
|
|
236
|
+
* not necessarily a valid TS identifier. Requires each delegate to be a default export.
|
|
237
|
+
*/
|
|
238
|
+
export function emitApiController(doc, operationsDir = DEFAULT_OPERATIONS_DIR) {
|
|
239
|
+
assertUniqueOperationIds(doc);
|
|
240
|
+
const ops = collectOperations(doc);
|
|
241
|
+
const dir = normalizeOperationsDir(operationsDir);
|
|
242
|
+
const header = "// AUTO-GENERATED by @nanobpm/urban from the app's OpenAPI spec (ADR 0059): the delegate\n" +
|
|
243
|
+
"// registry. Statically imports every operation delegate and asserts the complete, correctly-\n" +
|
|
244
|
+
"// typed set via `satisfies` — a missing, mismatched, or orphan delegate fails `tsc`. The\n" +
|
|
245
|
+
"// runtime dispatches through this map, so endpoints cannot drift from the spec. Do not edit —\n" +
|
|
246
|
+
"// regenerated from the spec.\n" +
|
|
247
|
+
"// eslint-disable\n\n";
|
|
248
|
+
const typeImports = `import type { OperationHandler } from "@nanobpm/urban";\n` +
|
|
249
|
+
`import type { ApiOperationId, ApiOperations } from "./${API_BINDINGS_DTS}";\n`;
|
|
250
|
+
const delegateImports = ops
|
|
251
|
+
.map((op, i) => `import op${i} from ${JSON.stringify(`../${dir}/${op.operationId}.ts`)};`)
|
|
252
|
+
.join("\n");
|
|
253
|
+
const mapEntries = ops.map((op, i) => ` ${JSON.stringify(op.operationId)}: op${i},`).join("\n");
|
|
254
|
+
return (header +
|
|
255
|
+
typeImports +
|
|
256
|
+
(delegateImports ? `${delegateImports}\n` : "") +
|
|
257
|
+
`\n` +
|
|
258
|
+
`type ReqFor<K extends ApiOperationId> = ApiOperations[K]["request"];\n` +
|
|
259
|
+
`type ResFor<K extends ApiOperationId> = ApiOperations[K]["response"];\n\n` +
|
|
260
|
+
`/** Every operationId → its default-exported delegate. The \`satisfies\` clause makes \`tsc\`\n` +
|
|
261
|
+
` * reject a missing, wrongly-typed, or orphan delegate (ADR 0059). */\n` +
|
|
262
|
+
`export const operations = {\n${mapEntries}${mapEntries ? "\n" : ""}} satisfies {\n` +
|
|
263
|
+
` [K in ApiOperationId]: OperationHandler<ReqFor<K>, ResFor<K>>;\n` +
|
|
264
|
+
`};\n`);
|
|
265
|
+
}
|
|
266
|
+
/** Derive the api artifacts from a parsed OpenAPI document. `operationsDir` is the manifest's
|
|
267
|
+
* `api.dir` (default `operations/`) — the controller's static delegate imports resolve into it. */
|
|
268
|
+
export function deriveApi(doc, operationsDir = DEFAULT_OPERATIONS_DIR) {
|
|
215
269
|
return [
|
|
216
270
|
{ path: `${GENERATED_DIR}/${API_BINDINGS_DTS}`, content: emitApiBindings(doc) },
|
|
217
271
|
{ path: `${GENERATED_DIR}/${API_BINDINGS_TS}`, content: emitApiBindingsRuntime() },
|
|
272
|
+
{ path: `${GENERATED_DIR}/${API_CONTROLLER_TS}`, content: emitApiController(doc, operationsDir) },
|
|
218
273
|
];
|
|
219
274
|
}
|
|
220
275
|
export const apiDeriver = {
|
|
221
276
|
id: "openapi->api-io",
|
|
222
|
-
describe: "Derive api-io.d.ts (typed request/response per operationId) + the typed defineOperation.",
|
|
223
|
-
derive: ({ doc }) => deriveApi(doc),
|
|
277
|
+
describe: "Derive api-io.d.ts (typed request/response per operationId) + the typed defineOperation + the delegate registry.",
|
|
278
|
+
derive: ({ doc, operationsDir }) => deriveApi(doc, operationsDir),
|
|
224
279
|
};
|
package/dist/toolkit/gen.js
CHANGED
|
@@ -98,7 +98,10 @@ async function collectAll(opts) {
|
|
|
98
98
|
const specRef = typeof manifest.api?.spec === "string" ? manifest.api.spec.trim() : "";
|
|
99
99
|
if (!opts.modelsOnly && specRef.length > 0) {
|
|
100
100
|
const specText = await io.readText(join(root, specRef));
|
|
101
|
-
|
|
101
|
+
const opsDir = typeof manifest.api?.dir === "string" && manifest.api.dir.trim().length > 0
|
|
102
|
+
? manifest.api.dir.trim()
|
|
103
|
+
: undefined;
|
|
104
|
+
artifacts.push(...deriveApi(parseSpec(specText), opsDir));
|
|
102
105
|
}
|
|
103
106
|
return { artifacts: sortArtifacts(artifacts), derived };
|
|
104
107
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { GenOptions, GenResult } from "./gen.ts";
|
|
2
|
+
import type { OperationStubOutcome, StubOutcome } from "./scaffold.ts";
|
|
3
|
+
import type { StubManifestEntry } from "./scaffold/workers.ts";
|
|
4
|
+
export interface GenerateResult extends GenResult {
|
|
5
|
+
/** Worker-stub scaffolding outcomes (created / kept / would-create). */
|
|
6
|
+
workerStubs: StubOutcome[];
|
|
7
|
+
/** Operation-delegate scaffolding outcomes (created / kept / would-create). */
|
|
8
|
+
operationStubs: OperationStubOutcome[];
|
|
9
|
+
/** Worker manifest entries wired into `workers[]` (write mode only). */
|
|
10
|
+
wiredWorkers: StubManifestEntry[];
|
|
11
|
+
/** True when `workers[]` was patched (write mode, unwired workers present). */
|
|
12
|
+
manifestPatched: boolean;
|
|
13
|
+
/**
|
|
14
|
+
* App-relative paths of stubs that would be created — the scaffolding equivalent of `drift`.
|
|
15
|
+
* Populated in `check` mode (nothing is written); a caller should fail CI when non-empty.
|
|
16
|
+
*/
|
|
17
|
+
missingStubs: string[];
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Derive artifacts and scaffold the write-once handler stubs they reference, in one pass.
|
|
21
|
+
* In `check` mode nothing is written and any would-be-created stub is reported in `missingStubs`.
|
|
22
|
+
* Scaffolding is skipped when derivation is `incomplete` (never scaffold from a half-derived model);
|
|
23
|
+
* the caller reports the derivation failure first.
|
|
24
|
+
*/
|
|
25
|
+
export declare function generate(opts: GenOptions & {
|
|
26
|
+
check?: boolean;
|
|
27
|
+
}): Promise<GenerateResult>;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// `urban gen`: the whole generation concern in one pass — derive artifacts (migrations, worker-I/O,
|
|
2
|
+
// code-first models, OpenAPI contracts + controller) AND scaffold the write-once handler stubs those
|
|
3
|
+
// artifacts reference (worker stubs from the model, operation-delegate stubs from the OpenAPI spec).
|
|
4
|
+
//
|
|
5
|
+
// This composes the pure derivation (`runGen`, gen.ts) with the write-once scaffolders (scaffold.ts)
|
|
6
|
+
// WITHOUT a cycle: scaffold.ts already depends on gen.ts, so this leaf module depends on both and
|
|
7
|
+
// neither depends back. Scaffolding is write-once (an existing, human-owned stub is never clobbered),
|
|
8
|
+
// so folding it into `gen` is idempotent. `--check` writes nothing: a missing stub surfaces in
|
|
9
|
+
// `missingStubs` exactly like a drifted artifact surfaces in `drift`, so CI fails when a stub the
|
|
10
|
+
// generated controller imports isn't committed.
|
|
11
|
+
import { runGen } from "./gen.js";
|
|
12
|
+
import { scaffoldOperations, scaffoldWorkers } from "./scaffold.js";
|
|
13
|
+
/**
|
|
14
|
+
* Derive artifacts and scaffold the write-once handler stubs they reference, in one pass.
|
|
15
|
+
* In `check` mode nothing is written and any would-be-created stub is reported in `missingStubs`.
|
|
16
|
+
* Scaffolding is skipped when derivation is `incomplete` (never scaffold from a half-derived model);
|
|
17
|
+
* the caller reports the derivation failure first.
|
|
18
|
+
*/
|
|
19
|
+
export async function generate(opts) {
|
|
20
|
+
const res = await runGen(opts);
|
|
21
|
+
const empty = {
|
|
22
|
+
...res,
|
|
23
|
+
workerStubs: [],
|
|
24
|
+
operationStubs: [],
|
|
25
|
+
wiredWorkers: [],
|
|
26
|
+
manifestPatched: false,
|
|
27
|
+
missingStubs: [],
|
|
28
|
+
};
|
|
29
|
+
if (res.incomplete)
|
|
30
|
+
return empty;
|
|
31
|
+
const write = !opts.check;
|
|
32
|
+
const scaffoldOpts = { root: opts.root, io: opts.io, manifestFile: opts.manifestFile, write };
|
|
33
|
+
const workers = await scaffoldWorkers(scaffoldOpts);
|
|
34
|
+
const operations = await scaffoldOperations(scaffoldOpts);
|
|
35
|
+
const missingStubs = [
|
|
36
|
+
...workers.outcomes.filter((o) => o.status === "would-create").map((o) => o.handlerPath),
|
|
37
|
+
...operations.outcomes.filter((o) => o.status === "would-create").map((o) => o.handlerPath),
|
|
38
|
+
];
|
|
39
|
+
return {
|
|
40
|
+
...res,
|
|
41
|
+
workerStubs: workers.outcomes,
|
|
42
|
+
operationStubs: operations.outcomes,
|
|
43
|
+
wiredWorkers: workers.wired,
|
|
44
|
+
manifestPatched: workers.manifestPatched,
|
|
45
|
+
missingStubs,
|
|
46
|
+
};
|
|
47
|
+
}
|
package/dist/toolkit/index.d.ts
CHANGED
|
@@ -13,7 +13,7 @@ export { deriveMeta, emitMeta, foldMeta, metaDeriver, scanModelMeta, META_TS, }
|
|
|
13
13
|
export type { MetaDecl } from "./derivers/meta.ts";
|
|
14
14
|
export { deriveMessageBindings, emitMessageBindings, emitMessageBindingsRuntime, messagesDeriver, scanModelMessages, MESSAGE_BINDINGS_DTS, MESSAGE_BINDINGS_TS, } from "./derivers/messages.ts";
|
|
15
15
|
export type { MessageIo, MessageBindingDecl } from "./derivers/messages.ts";
|
|
16
|
-
export { apiDeriver, deriveApi, emitApiBindings, emitApiBindingsRuntime, schemaToTs, API_BINDINGS_DTS, API_BINDINGS_TS, } from "./derivers/api.ts";
|
|
16
|
+
export { apiDeriver, deriveApi, emitApiBindings, emitApiBindingsRuntime, emitApiController, schemaToTs, API_BINDINGS_DTS, API_BINDINGS_TS, API_CONTROLLER_TS, } from "./derivers/api.ts";
|
|
17
17
|
export { collectOperations, HTTP_METHODS, isSafeOperationId, operationsWithoutId, operationsWithUnsafeId, parseSpec, refName, resolveSchema, toRouteMatcher, undeclaredPathParams, validateValue, } from "../openapi/spec.ts";
|
|
18
18
|
export type { HttpMethodLower, OpenApiDoc, OpenApiParameter, OpenApiSchema, OperationInfo, } from "../openapi/spec.ts";
|
|
19
19
|
export declare const DERIVERS: readonly [import("./artifact.ts").Deriver<import("./derivers/migrations.ts").ToolkitManifest>, import("./artifact.ts").Deriver<import("./derivers/migrations.ts").ToolkitManifest>, import("./artifact.ts").Deriver<{
|
|
@@ -26,9 +26,12 @@ export declare const DERIVERS: readonly [import("./artifact.ts").Deriver<import(
|
|
|
26
26
|
declaredTypeIds: Iterable<string>;
|
|
27
27
|
}>, import("./artifact.ts").Deriver<{
|
|
28
28
|
doc: import("../openapi/spec.ts").OpenApiDoc;
|
|
29
|
+
operationsDir?: string;
|
|
29
30
|
}>];
|
|
30
31
|
export { collectArtifacts, runGen, joinPath, readModels, expandPattern, previewModels } from "./gen.ts";
|
|
31
32
|
export type { GenIO, GenOptions, GenResult } from "./gen.ts";
|
|
33
|
+
export { generate } from "./generate.ts";
|
|
34
|
+
export type { GenerateResult } from "./generate.ts";
|
|
32
35
|
export { createNodeGenIO } from "./fsio.ts";
|
|
33
36
|
export { deriveModels, isWorkflow, bpmnFilename, processesOutDir, DEFAULT_WORKFLOW_PATTERNS, PROCESSES_DIR, MODEL_PROVENANCE, } from "./models.ts";
|
|
34
37
|
export type { DerivedModels, DerivedModelInfo, ModelError, ModelsManifest } from "./models.ts";
|
|
@@ -36,5 +39,9 @@ export { planWorkerScaffold, renderWorkerStub, slugifyTaskType, } from "./scaffo
|
|
|
36
39
|
export type { ScaffoldWorker, SkippedWorker, SkipReason, StubManifestEntry, WorkerScaffoldPlan, WorkerStubPlan, } from "./scaffold/workers.ts";
|
|
37
40
|
export { scaffoldWorkers } from "./scaffold.ts";
|
|
38
41
|
export type { ScaffoldOptions, ScaffoldRun, StubOutcome, StubStatus } from "./scaffold.ts";
|
|
42
|
+
export { planOperationScaffold, renderOperationStub } from "./scaffold/operations.ts";
|
|
43
|
+
export type { OperationStubPlan } from "./scaffold/operations.ts";
|
|
44
|
+
export { scaffoldOperations } from "./scaffold.ts";
|
|
45
|
+
export type { OperationScaffoldRun, OperationStubOutcome } from "./scaffold.ts";
|
|
39
46
|
export { addConnector } from "./addConnector.ts";
|
|
40
47
|
export type { AddConnectorOptions, AddConnectorResult } from "./addConnector.ts";
|
package/dist/toolkit/index.js
CHANGED
|
@@ -11,7 +11,7 @@ export { deriveWorkerBindings, emitWorkerBindings, emitWorkerBindingsRuntime, ov
|
|
|
11
11
|
export { byModelPath, typeRefFor } from "./derivers/worker-io.js";
|
|
12
12
|
export { deriveMeta, emitMeta, foldMeta, metaDeriver, scanModelMeta, META_TS, } from "./derivers/meta.js";
|
|
13
13
|
export { deriveMessageBindings, emitMessageBindings, emitMessageBindingsRuntime, messagesDeriver, scanModelMessages, MESSAGE_BINDINGS_DTS, MESSAGE_BINDINGS_TS, } from "./derivers/messages.js";
|
|
14
|
-
export { apiDeriver, deriveApi, emitApiBindings, emitApiBindingsRuntime, schemaToTs, API_BINDINGS_DTS, API_BINDINGS_TS, } from "./derivers/api.js";
|
|
14
|
+
export { apiDeriver, deriveApi, emitApiBindings, emitApiBindingsRuntime, emitApiController, schemaToTs, API_BINDINGS_DTS, API_BINDINGS_TS, API_CONTROLLER_TS, } from "./derivers/api.js";
|
|
15
15
|
// The OpenAPI machinery the deriver + runtime share (ADR 0058).
|
|
16
16
|
export { collectOperations, HTTP_METHODS, isSafeOperationId, operationsWithoutId, operationsWithUnsafeId, parseSpec, refName, resolveSchema, toRouteMatcher, undeclaredPathParams, validateValue, } from "../openapi/spec.js";
|
|
17
17
|
// The registry of all derivers (for discovery / IDE migration).
|
|
@@ -31,10 +31,14 @@ export const DERIVERS = [
|
|
|
31
31
|
];
|
|
32
32
|
// Gen orchestrator + IO
|
|
33
33
|
export { collectArtifacts, runGen, joinPath, readModels, expandPattern, previewModels } from "./gen.js";
|
|
34
|
+
export { generate } from "./generate.js";
|
|
34
35
|
export { createNodeGenIO } from "./fsio.js";
|
|
35
36
|
// Code-first model derivation (workflows/*.ts → BPMN)
|
|
36
37
|
export { deriveModels, isWorkflow, bpmnFilename, processesOutDir, DEFAULT_WORKFLOW_PATTERNS, PROCESSES_DIR, MODEL_PROVENANCE, } from "./models.js";
|
|
37
38
|
// Worker-stub scaffolder (ADR 0056): write-once handler stubs from the model.
|
|
38
39
|
export { planWorkerScaffold, renderWorkerStub, slugifyTaskType, } from "./scaffold/workers.js";
|
|
39
40
|
export { scaffoldWorkers } from "./scaffold.js";
|
|
41
|
+
// Operation-delegate scaffolder (ADR 0059): write-once typed delegate stubs from the OpenAPI spec.
|
|
42
|
+
export { planOperationScaffold, renderOperationStub } from "./scaffold/operations.js";
|
|
43
|
+
export { scaffoldOperations } from "./scaffold.js";
|
|
40
44
|
export { addConnector } from "./addConnector.js";
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { DEFAULT_OPERATIONS_DIR } from "../api-dir.ts";
|
|
2
|
+
import { type OpenApiDoc } from "../../openapi/spec.ts";
|
|
3
|
+
export { DEFAULT_OPERATIONS_DIR };
|
|
4
|
+
/** One planned operation-delegate stub. */
|
|
5
|
+
export interface OperationStubPlan {
|
|
6
|
+
operationId: string;
|
|
7
|
+
/** App-relative delegate path: `<dir>/<operationId>.ts`. */
|
|
8
|
+
handlerPath: string;
|
|
9
|
+
/** The stub file contents (a typed `defineOperation` that throws `NotImplemented` → HTTP 501). */
|
|
10
|
+
stub: string;
|
|
11
|
+
}
|
|
12
|
+
/** Render a stub matching a real hand-authored operation delegate (default-exported, typed via the
|
|
13
|
+
* generated `defineOperation`). It throws `NotImplemented` so the endpoint returns 501 until the
|
|
14
|
+
* body is implemented — never a silent 500, never a drift from the contract. */
|
|
15
|
+
export declare function renderOperationStub(operationId: string, dir?: string): string;
|
|
16
|
+
/**
|
|
17
|
+
* Plan write-once delegate stubs from the spec: one per declared `operationId` (collectOperations
|
|
18
|
+
* already skips ops without a safe id). The planner emits a plan for every operation; the impure
|
|
19
|
+
* edge (`scaffoldOperations`) keeps an existing file verbatim and creates only the absent ones.
|
|
20
|
+
*/
|
|
21
|
+
export declare function planOperationScaffold(doc: OpenApiDoc, dir?: string): OperationStubPlan[];
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// Pure planner for the write-once operation-delegate scaffolder (ADR 0059). Given a parsed OpenAPI
|
|
2
|
+
// document, it plans one typed delegate stub per declared `operationId` — the ONE part of the HTTP
|
|
3
|
+
// surface that cannot be derived from the spec (the handler *body* is human logic). Everything here
|
|
4
|
+
// is pure and testable like a deriver; the impure write-if-absent edge lives in `../scaffold.ts`.
|
|
5
|
+
//
|
|
6
|
+
// scaffold ≠ derive: stubs are human-owned files under `<dir>/<operationId>.ts`, so they are planned
|
|
7
|
+
// once and never overwritten — unlike the derived `nano-generated/` tree (which includes the
|
|
8
|
+
// controller registry that statically imports these delegates and type-checks the whole set).
|
|
9
|
+
import { DEFAULT_OPERATIONS_DIR, normalizeApiDir } from "../api-dir.js";
|
|
10
|
+
import { assertUniqueOperationIds, collectOperations } from "../../openapi/spec.js";
|
|
11
|
+
export { DEFAULT_OPERATIONS_DIR };
|
|
12
|
+
/** Normalize an `api.dir` to a clean relative segment (see `normalizeApiDir`); rejects absolute
|
|
13
|
+
* paths and `..` segments so stubs are scaffolded where the runtime resolves delegates from. */
|
|
14
|
+
function normalizeDir(dir) {
|
|
15
|
+
return normalizeApiDir(dir);
|
|
16
|
+
}
|
|
17
|
+
/** Number of `../` hops from `<dir>/<file>.ts` back up to the app root (dir may be nested). */
|
|
18
|
+
function upToRoot(dir) {
|
|
19
|
+
const depth = dir.split("/").filter((s) => s.length > 0).length;
|
|
20
|
+
return "../".repeat(depth);
|
|
21
|
+
}
|
|
22
|
+
/** Render a stub matching a real hand-authored operation delegate (default-exported, typed via the
|
|
23
|
+
* generated `defineOperation`). It throws `NotImplemented` so the endpoint returns 501 until the
|
|
24
|
+
* body is implemented — never a silent 500, never a drift from the contract. */
|
|
25
|
+
export function renderOperationStub(operationId, dir = DEFAULT_OPERATIONS_DIR) {
|
|
26
|
+
const up = upToRoot(normalizeDir(dir));
|
|
27
|
+
const key = JSON.stringify(operationId);
|
|
28
|
+
return (`// Operation delegate stub for the \`${operationId}\` endpoint, scaffolded from the OpenAPI spec\n` +
|
|
29
|
+
`// (ADR 0059). This file is yours to edit — \`urban gen\` will never overwrite it. The\n` +
|
|
30
|
+
`// generated controller (nano-generated/controller.ts) imports this default export and\n` +
|
|
31
|
+
`// type-checks it against the spec, so the signature cannot drift. Implement the body below\n` +
|
|
32
|
+
`// (the validated \`{ req, params, query, body }\` input + the injected \`app\`) and delete the throw.\n` +
|
|
33
|
+
`import { NotImplemented } from "@nanobpm/urban";\n` +
|
|
34
|
+
`import { defineOperation } from ${JSON.stringify(`${up}nano-generated/operations.ts`)};\n` +
|
|
35
|
+
`\n` +
|
|
36
|
+
`export default defineOperation(${key}, async (input, app) => {\n` +
|
|
37
|
+
` // \`input\` is the validated request (params/query/body/req); reach app state + services\n` +
|
|
38
|
+
` // through the injected \`app\` API. Return \`{ status?, headers?, body? }\` (or nothing → 204).\n` +
|
|
39
|
+
` throw new NotImplemented(${key});\n` +
|
|
40
|
+
`});\n`);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Plan write-once delegate stubs from the spec: one per declared `operationId` (collectOperations
|
|
44
|
+
* already skips ops without a safe id). The planner emits a plan for every operation; the impure
|
|
45
|
+
* edge (`scaffoldOperations`) keeps an existing file verbatim and creates only the absent ones.
|
|
46
|
+
*/
|
|
47
|
+
export function planOperationScaffold(doc, dir = DEFAULT_OPERATIONS_DIR) {
|
|
48
|
+
assertUniqueOperationIds(doc);
|
|
49
|
+
const cleanDir = normalizeDir(dir);
|
|
50
|
+
return collectOperations(doc).map((op) => ({
|
|
51
|
+
operationId: op.operationId,
|
|
52
|
+
handlerPath: `${cleanDir}/${op.operationId}.ts`,
|
|
53
|
+
stub: renderOperationStub(op.operationId, cleanDir),
|
|
54
|
+
}));
|
|
55
|
+
}
|
|
@@ -41,7 +41,7 @@ export function renderWorkerStub(taskType, typedIn, typedOut) {
|
|
|
41
41
|
? ` // \`job.variables\` is typed as WorkerInputs[${key}].`
|
|
42
42
|
: ` // \`job.variables\` is an open Record<string, unknown> (no declared input type).`;
|
|
43
43
|
return (`// Handler stub for the \`${taskType}\` service task, scaffolded from the model (ADR 0056).\n` +
|
|
44
|
-
`// This file is yours to edit — \`urban
|
|
44
|
+
`// This file is yours to edit — \`urban gen\` will never overwrite it. Implement the body\n` +
|
|
45
45
|
`// below (use \`app.data.table(...)\` for state) and delete the throw.\n` +
|
|
46
46
|
`import type { AppJobHandler } from "@nanobpm/urban/worker";\n` +
|
|
47
47
|
generatedImport +
|
|
@@ -29,3 +29,19 @@ export interface ScaffoldRun {
|
|
|
29
29
|
}
|
|
30
30
|
/** Scaffold write-once worker stubs from the model, wiring them into `manifest.workers[]`. */
|
|
31
31
|
export declare function scaffoldWorkers(opts: ScaffoldOptions): Promise<ScaffoldRun>;
|
|
32
|
+
/** One planned operation-delegate outcome (write-once). */
|
|
33
|
+
export interface OperationStubOutcome {
|
|
34
|
+
operationId: string;
|
|
35
|
+
handlerPath: string;
|
|
36
|
+
status: StubStatus;
|
|
37
|
+
}
|
|
38
|
+
export interface OperationScaffoldRun {
|
|
39
|
+
outcomes: OperationStubOutcome[];
|
|
40
|
+
write: boolean;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Scaffold write-once operation-delegate stubs from the app's OpenAPI spec (ADR 0059). One typed
|
|
44
|
+
* `defineOperation` stub per declared `operationId`, created only if absent (human-owned files are
|
|
45
|
+
* kept verbatim). Dry-run by default. A no-op when the app declares no `api.spec`.
|
|
46
|
+
*/
|
|
47
|
+
export declare function scaffoldOperations(opts: ScaffoldOptions): Promise<OperationScaffoldRun>;
|
package/dist/toolkit/scaffold.js
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
// human-owned, so an existing file is KEPT verbatim, never clobbered (the opposite of `runGen`).
|
|
5
5
|
import { joinPath, readModels } from "./gen.js";
|
|
6
6
|
import { planWorkerScaffold, } from "./scaffold/workers.js";
|
|
7
|
+
import { planOperationScaffold } from "./scaffold/operations.js";
|
|
8
|
+
import { parseSpec } from "../openapi/spec.js";
|
|
7
9
|
/** Scaffold write-once worker stubs from the model, wiring them into `manifest.workers[]`. */
|
|
8
10
|
export async function scaffoldWorkers(opts) {
|
|
9
11
|
const { root, io } = opts;
|
|
@@ -52,3 +54,41 @@ export async function scaffoldWorkers(opts) {
|
|
|
52
54
|
}
|
|
53
55
|
return { outcomes, skipped, wired, manifestPatched, write };
|
|
54
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* Scaffold write-once operation-delegate stubs from the app's OpenAPI spec (ADR 0059). One typed
|
|
59
|
+
* `defineOperation` stub per declared `operationId`, created only if absent (human-owned files are
|
|
60
|
+
* kept verbatim). Dry-run by default. A no-op when the app declares no `api.spec`.
|
|
61
|
+
*/
|
|
62
|
+
export async function scaffoldOperations(opts) {
|
|
63
|
+
const { root, io } = opts;
|
|
64
|
+
const write = opts.write ?? false;
|
|
65
|
+
const manifestFile = opts.manifestFile ?? "nano.app.json";
|
|
66
|
+
const manifestPath = joinPath(root, manifestFile);
|
|
67
|
+
const manifest = JSON.parse(await io.readText(manifestPath));
|
|
68
|
+
const specRef = typeof manifest.api?.spec === "string" ? manifest.api.spec.trim() : "";
|
|
69
|
+
if (specRef.length === 0)
|
|
70
|
+
return { outcomes: [], write };
|
|
71
|
+
const specText = await io.readText(joinPath(root, specRef));
|
|
72
|
+
const dir = typeof manifest.api?.dir === "string" && manifest.api.dir.trim().length > 0
|
|
73
|
+
? manifest.api.dir.trim()
|
|
74
|
+
: undefined;
|
|
75
|
+
const plans = planOperationScaffold(parseSpec(specText), dir);
|
|
76
|
+
const outcomes = [];
|
|
77
|
+
for (const plan of plans) {
|
|
78
|
+
const abs = joinPath(root, plan.handlerPath);
|
|
79
|
+
const exists = await io.exists(abs);
|
|
80
|
+
let status;
|
|
81
|
+
if (exists) {
|
|
82
|
+
status = "kept"; // human-owned — never clobber
|
|
83
|
+
}
|
|
84
|
+
else if (write) {
|
|
85
|
+
await io.writeText(abs, plan.stub);
|
|
86
|
+
status = "created";
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
status = "would-create";
|
|
90
|
+
}
|
|
91
|
+
outcomes.push({ operationId: plan.operationId, handlerPath: plan.handlerPath, status });
|
|
92
|
+
}
|
|
93
|
+
return { outcomes, write };
|
|
94
|
+
}
|