spfn 0.3.0-beta.15 → 0.3.0-beta.16
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 +10 -2
- package/dist/index.js +70 -31
- package/dist/surface.d.ts +6 -1
- package/dist/surface.js +68 -29
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -635,6 +635,12 @@ spfn ops list --module ledger # just that module's comma
|
|
|
635
635
|
spfn ops call ledger.compact --yes # effect=destructive needs this
|
|
636
636
|
```
|
|
637
637
|
|
|
638
|
+
An app's own command can carry an effect and a summary too, declared on its route with
|
|
639
|
+
`.effect()` and `.summary()`; `spfn ops list` and `--describe` show them, and an app command
|
|
640
|
+
announcing `read` or `write` needs no `--yes`. A CLI at 0.3.0-beta.15 or older does not
|
|
641
|
+
know app commands carry them: it warns that such a command "names no usable module" and
|
|
642
|
+
asks for `--yes` on it, so upgrade the CLI together with `@spfn/core`.
|
|
643
|
+
|
|
638
644
|
`spfn ops call` refuses a command the app declared `effect: destructive` unless `--yes` is
|
|
639
645
|
given. It refuses the same way when the app announced module metadata this CLI could not
|
|
640
646
|
validate: the effect is then unknown rather than absent, and an unknown effect is not
|
|
@@ -707,8 +713,10 @@ const answer = await client.call('pageAdd', { params: {}, query: {}, body: { tit
|
|
|
707
713
|
// answer: { status, body } — the app's answer, relayed
|
|
708
714
|
```
|
|
709
715
|
|
|
710
|
-
|
|
711
|
-
|
|
716
|
+
Each command carries the `effect` and `summary` the app declared, so your code can tell a
|
|
717
|
+
read from a write before it calls; `call()` itself makes no decision on them. It keeps
|
|
718
|
+
every check `spfn ops` makes: the manifest's shape, each command's path staying under
|
|
719
|
+
`<namespace>/`, module and command metadata, and path parameters that would decode into a dot
|
|
712
720
|
segment. `appUrl` must be https (`http` only against a loopback host) — refused when the
|
|
713
721
|
client is created, with a thrown `Error`. A manifest request that does not answer 200
|
|
714
722
|
throws a `SurfaceManifestError` carrying the `status`. The library prints nothing and does
|
package/dist/index.js
CHANGED
|
@@ -949,7 +949,7 @@ import { dirname as dirname2, join as join10 } from "path";
|
|
|
949
949
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
950
950
|
function getCliVersion() {
|
|
951
951
|
if (true) {
|
|
952
|
-
return "0.3.0-beta.
|
|
952
|
+
return "0.3.0-beta.16";
|
|
953
953
|
}
|
|
954
954
|
return fromPackageJson ?? (fromPackageJson = readPackageVersion());
|
|
955
955
|
}
|
|
@@ -6522,10 +6522,7 @@ function renderCommandUsage(command, invoke) {
|
|
|
6522
6522
|
if (command.summary) {
|
|
6523
6523
|
lines.push(` ${plain(command.summary)}`, "");
|
|
6524
6524
|
}
|
|
6525
|
-
|
|
6526
|
-
lines.push(` Effect: ${command.effect ?? "unknown"}`);
|
|
6527
|
-
lines.push(` Scopes: ${command.scopes?.map(plain).join(", ") || "(not declared)"}`, "");
|
|
6528
|
-
}
|
|
6525
|
+
lines.push(...metadataLines(command));
|
|
6529
6526
|
for (const section of SECTIONS) {
|
|
6530
6527
|
const schema = command.input[section.key];
|
|
6531
6528
|
if (!isSchema(schema)) {
|
|
@@ -6546,6 +6543,16 @@ function renderCommandUsage(command, invoke) {
|
|
|
6546
6543
|
}
|
|
6547
6544
|
return lines.join("\n").replace(/\n+$/, "");
|
|
6548
6545
|
}
|
|
6546
|
+
function metadataLines(command) {
|
|
6547
|
+
if (!command.effect && !command.scopes) {
|
|
6548
|
+
return [];
|
|
6549
|
+
}
|
|
6550
|
+
const lines = [` Effect: ${command.effect ?? "unknown"}`];
|
|
6551
|
+
if (command.module !== void 0 || command.scopes !== void 0) {
|
|
6552
|
+
lines.push(` Scopes: ${command.scopes?.map(plain).join(", ") || "(not declared)"}`);
|
|
6553
|
+
}
|
|
6554
|
+
return [...lines, ""];
|
|
6555
|
+
}
|
|
6549
6556
|
function exampleFlags(command) {
|
|
6550
6557
|
const parts = [];
|
|
6551
6558
|
for (const section of SECTIONS) {
|
|
@@ -6611,34 +6618,66 @@ function usableCommands(commands, moduleIds, reader) {
|
|
|
6611
6618
|
);
|
|
6612
6619
|
continue;
|
|
6613
6620
|
}
|
|
6614
|
-
|
|
6615
|
-
name: command.name,
|
|
6616
|
-
method: command.method,
|
|
6617
|
-
path: command.path,
|
|
6618
|
-
input: isPlainObject(command.input) ? command.input : {}
|
|
6619
|
-
};
|
|
6620
|
-
const metadataReason = unusableModuleMetadataBecause(command, moduleIds, reader);
|
|
6621
|
-
if (metadataReason === null && command.module !== void 0) {
|
|
6622
|
-
normalized.module = command.module;
|
|
6623
|
-
normalized.summary = command.summary;
|
|
6624
|
-
normalized.effect = command.effect;
|
|
6625
|
-
normalized.scopes = [...command.scopes];
|
|
6626
|
-
} else if (metadataReason !== null) {
|
|
6627
|
-
normalized.metadataRejected = true;
|
|
6628
|
-
reader.warn(
|
|
6629
|
-
`Ignoring module metadata for command "${plain(command.name)}": ${metadataReason}. Its effect is unknown, so treat it as destructive.`,
|
|
6630
|
-
{ kind: "metadata", command: command.name, reason: metadataReason }
|
|
6631
|
-
);
|
|
6632
|
-
}
|
|
6633
|
-
usable.push(normalized);
|
|
6621
|
+
usable.push(withMetadata(command, moduleIds, reader));
|
|
6634
6622
|
}
|
|
6635
6623
|
return usable;
|
|
6636
6624
|
}
|
|
6637
|
-
function
|
|
6638
|
-
const
|
|
6639
|
-
|
|
6640
|
-
|
|
6625
|
+
function withMetadata(command, moduleIds, reader) {
|
|
6626
|
+
const normalized = {
|
|
6627
|
+
name: command.name,
|
|
6628
|
+
method: command.method,
|
|
6629
|
+
path: command.path,
|
|
6630
|
+
input: isPlainObject(command.input) ? command.input : {}
|
|
6631
|
+
};
|
|
6632
|
+
if (command.module !== void 0 || command.scopes !== void 0) {
|
|
6633
|
+
return withModuleMetadata(normalized, command, moduleIds, reader);
|
|
6634
|
+
}
|
|
6635
|
+
return withAppMetadata(normalized, command, reader);
|
|
6636
|
+
}
|
|
6637
|
+
function withModuleMetadata(normalized, command, moduleIds, reader) {
|
|
6638
|
+
const reason = unusableModuleMetadataBecause(command, moduleIds, reader);
|
|
6639
|
+
if (reason !== null) {
|
|
6640
|
+
return rejectedMetadata(normalized, "module", reason, reader);
|
|
6641
|
+
}
|
|
6642
|
+
return {
|
|
6643
|
+
...normalized,
|
|
6644
|
+
module: command.module,
|
|
6645
|
+
summary: command.summary,
|
|
6646
|
+
effect: command.effect,
|
|
6647
|
+
scopes: [...command.scopes]
|
|
6648
|
+
};
|
|
6649
|
+
}
|
|
6650
|
+
function withAppMetadata(normalized, command, reader) {
|
|
6651
|
+
const reason = unusableAppMetadataBecause(command);
|
|
6652
|
+
if (reason !== null) {
|
|
6653
|
+
return rejectedMetadata(normalized, "command", reason, reader);
|
|
6641
6654
|
}
|
|
6655
|
+
return {
|
|
6656
|
+
...normalized,
|
|
6657
|
+
...command.summary !== void 0 ? { summary: command.summary } : {},
|
|
6658
|
+
...command.effect !== void 0 ? { effect: command.effect } : {}
|
|
6659
|
+
};
|
|
6660
|
+
}
|
|
6661
|
+
function rejectedMetadata(normalized, metadata, reason, reader) {
|
|
6662
|
+
reader.warn(
|
|
6663
|
+
`Ignoring ${metadata} metadata for command "${plain(normalized.name)}": ${reason}. Its effect is unknown, so treat it as destructive.`,
|
|
6664
|
+
{ kind: "metadata", command: normalized.name, metadata, reason }
|
|
6665
|
+
);
|
|
6666
|
+
return { ...normalized, metadataRejected: true };
|
|
6667
|
+
}
|
|
6668
|
+
function unusableAppMetadataBecause(command) {
|
|
6669
|
+
if (command.summary !== void 0 && !isUsableSummary(command.summary)) {
|
|
6670
|
+
return "its summary is empty or too long";
|
|
6671
|
+
}
|
|
6672
|
+
if (command.effect !== void 0 && !EFFECTS.has(command.effect)) {
|
|
6673
|
+
return "its effect is unknown";
|
|
6674
|
+
}
|
|
6675
|
+
return null;
|
|
6676
|
+
}
|
|
6677
|
+
function isUsableSummary(summary) {
|
|
6678
|
+
return typeof summary === "string" && summary.length > 0 && summary.length <= MAX_SUMMARY_LENGTH;
|
|
6679
|
+
}
|
|
6680
|
+
function unusableModuleMetadataBecause(command, moduleIds, reader) {
|
|
6642
6681
|
if (typeof command.module !== "string" || !moduleIds.has(command.module)) {
|
|
6643
6682
|
return "it names no usable module";
|
|
6644
6683
|
}
|
|
@@ -6648,7 +6687,7 @@ function unusableModuleMetadataBecause(command, moduleIds, reader) {
|
|
|
6648
6687
|
if (!command.path.startsWith(`${reader.prefix}${command.module}/`)) {
|
|
6649
6688
|
return "its path is outside the module namespace";
|
|
6650
6689
|
}
|
|
6651
|
-
if (
|
|
6690
|
+
if (!isUsableSummary(command.summary)) {
|
|
6652
6691
|
return "its summary is missing or too long";
|
|
6653
6692
|
}
|
|
6654
6693
|
if (typeof command.effect !== "string" || !EFFECTS.has(command.effect)) {
|
|
@@ -6835,7 +6874,7 @@ function printOpsWarning(_message, warning) {
|
|
|
6835
6874
|
break;
|
|
6836
6875
|
case "metadata":
|
|
6837
6876
|
console.error(
|
|
6838
|
-
`\u26A0\uFE0F Ignoring module metadata for ops command "${plain(warning.command)}": ${warning.reason}. Calling it will require --yes.`
|
|
6877
|
+
`\u26A0\uFE0F Ignoring ${warning.metadata ?? "module"} metadata for ops command "${plain(warning.command)}": ${warning.reason}. Calling it will require --yes.`
|
|
6839
6878
|
);
|
|
6840
6879
|
break;
|
|
6841
6880
|
case "modules":
|
package/dist/surface.d.ts
CHANGED
|
@@ -57,13 +57,18 @@ interface SurfaceResponse {
|
|
|
57
57
|
* terminal.
|
|
58
58
|
*/
|
|
59
59
|
|
|
60
|
-
/**
|
|
60
|
+
/**
|
|
61
|
+
* What the client dropped from a manifest, and why. A `metadata` warning says
|
|
62
|
+
* whose rules refused it: a module command's, or an app command's own
|
|
63
|
+
* `effect` and `summary`.
|
|
64
|
+
*/
|
|
61
65
|
type SurfaceWarning = {
|
|
62
66
|
kind: 'command';
|
|
63
67
|
reason: string;
|
|
64
68
|
} | {
|
|
65
69
|
kind: 'metadata';
|
|
66
70
|
command: string;
|
|
71
|
+
metadata?: 'module' | 'command';
|
|
67
72
|
reason: string;
|
|
68
73
|
} | {
|
|
69
74
|
kind: 'modules';
|
package/dist/surface.js
CHANGED
|
@@ -181,10 +181,7 @@ function renderCommandUsage(command, invoke) {
|
|
|
181
181
|
if (command.summary) {
|
|
182
182
|
lines.push(` ${plain(command.summary)}`, "");
|
|
183
183
|
}
|
|
184
|
-
|
|
185
|
-
lines.push(` Effect: ${command.effect ?? "unknown"}`);
|
|
186
|
-
lines.push(` Scopes: ${command.scopes?.map(plain).join(", ") || "(not declared)"}`, "");
|
|
187
|
-
}
|
|
184
|
+
lines.push(...metadataLines(command));
|
|
188
185
|
for (const section of SECTIONS) {
|
|
189
186
|
const schema = command.input[section.key];
|
|
190
187
|
if (!isSchema(schema)) {
|
|
@@ -205,6 +202,16 @@ function renderCommandUsage(command, invoke) {
|
|
|
205
202
|
}
|
|
206
203
|
return lines.join("\n").replace(/\n+$/, "");
|
|
207
204
|
}
|
|
205
|
+
function metadataLines(command) {
|
|
206
|
+
if (!command.effect && !command.scopes) {
|
|
207
|
+
return [];
|
|
208
|
+
}
|
|
209
|
+
const lines = [` Effect: ${command.effect ?? "unknown"}`];
|
|
210
|
+
if (command.module !== void 0 || command.scopes !== void 0) {
|
|
211
|
+
lines.push(` Scopes: ${command.scopes?.map(plain).join(", ") || "(not declared)"}`);
|
|
212
|
+
}
|
|
213
|
+
return [...lines, ""];
|
|
214
|
+
}
|
|
208
215
|
function exampleFlags(command) {
|
|
209
216
|
const parts = [];
|
|
210
217
|
for (const section of SECTIONS) {
|
|
@@ -270,34 +277,66 @@ function usableCommands(commands, moduleIds, reader) {
|
|
|
270
277
|
);
|
|
271
278
|
continue;
|
|
272
279
|
}
|
|
273
|
-
|
|
274
|
-
name: command.name,
|
|
275
|
-
method: command.method,
|
|
276
|
-
path: command.path,
|
|
277
|
-
input: isPlainObject(command.input) ? command.input : {}
|
|
278
|
-
};
|
|
279
|
-
const metadataReason = unusableModuleMetadataBecause(command, moduleIds, reader);
|
|
280
|
-
if (metadataReason === null && command.module !== void 0) {
|
|
281
|
-
normalized.module = command.module;
|
|
282
|
-
normalized.summary = command.summary;
|
|
283
|
-
normalized.effect = command.effect;
|
|
284
|
-
normalized.scopes = [...command.scopes];
|
|
285
|
-
} else if (metadataReason !== null) {
|
|
286
|
-
normalized.metadataRejected = true;
|
|
287
|
-
reader.warn(
|
|
288
|
-
`Ignoring module metadata for command "${plain(command.name)}": ${metadataReason}. Its effect is unknown, so treat it as destructive.`,
|
|
289
|
-
{ kind: "metadata", command: command.name, reason: metadataReason }
|
|
290
|
-
);
|
|
291
|
-
}
|
|
292
|
-
usable.push(normalized);
|
|
280
|
+
usable.push(withMetadata(command, moduleIds, reader));
|
|
293
281
|
}
|
|
294
282
|
return usable;
|
|
295
283
|
}
|
|
296
|
-
function
|
|
297
|
-
const
|
|
298
|
-
|
|
299
|
-
|
|
284
|
+
function withMetadata(command, moduleIds, reader) {
|
|
285
|
+
const normalized = {
|
|
286
|
+
name: command.name,
|
|
287
|
+
method: command.method,
|
|
288
|
+
path: command.path,
|
|
289
|
+
input: isPlainObject(command.input) ? command.input : {}
|
|
290
|
+
};
|
|
291
|
+
if (command.module !== void 0 || command.scopes !== void 0) {
|
|
292
|
+
return withModuleMetadata(normalized, command, moduleIds, reader);
|
|
293
|
+
}
|
|
294
|
+
return withAppMetadata(normalized, command, reader);
|
|
295
|
+
}
|
|
296
|
+
function withModuleMetadata(normalized, command, moduleIds, reader) {
|
|
297
|
+
const reason = unusableModuleMetadataBecause(command, moduleIds, reader);
|
|
298
|
+
if (reason !== null) {
|
|
299
|
+
return rejectedMetadata(normalized, "module", reason, reader);
|
|
300
|
+
}
|
|
301
|
+
return {
|
|
302
|
+
...normalized,
|
|
303
|
+
module: command.module,
|
|
304
|
+
summary: command.summary,
|
|
305
|
+
effect: command.effect,
|
|
306
|
+
scopes: [...command.scopes]
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
function withAppMetadata(normalized, command, reader) {
|
|
310
|
+
const reason = unusableAppMetadataBecause(command);
|
|
311
|
+
if (reason !== null) {
|
|
312
|
+
return rejectedMetadata(normalized, "command", reason, reader);
|
|
313
|
+
}
|
|
314
|
+
return {
|
|
315
|
+
...normalized,
|
|
316
|
+
...command.summary !== void 0 ? { summary: command.summary } : {},
|
|
317
|
+
...command.effect !== void 0 ? { effect: command.effect } : {}
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
function rejectedMetadata(normalized, metadata, reason, reader) {
|
|
321
|
+
reader.warn(
|
|
322
|
+
`Ignoring ${metadata} metadata for command "${plain(normalized.name)}": ${reason}. Its effect is unknown, so treat it as destructive.`,
|
|
323
|
+
{ kind: "metadata", command: normalized.name, metadata, reason }
|
|
324
|
+
);
|
|
325
|
+
return { ...normalized, metadataRejected: true };
|
|
326
|
+
}
|
|
327
|
+
function unusableAppMetadataBecause(command) {
|
|
328
|
+
if (command.summary !== void 0 && !isUsableSummary(command.summary)) {
|
|
329
|
+
return "its summary is empty or too long";
|
|
330
|
+
}
|
|
331
|
+
if (command.effect !== void 0 && !EFFECTS.has(command.effect)) {
|
|
332
|
+
return "its effect is unknown";
|
|
300
333
|
}
|
|
334
|
+
return null;
|
|
335
|
+
}
|
|
336
|
+
function isUsableSummary(summary) {
|
|
337
|
+
return typeof summary === "string" && summary.length > 0 && summary.length <= MAX_SUMMARY_LENGTH;
|
|
338
|
+
}
|
|
339
|
+
function unusableModuleMetadataBecause(command, moduleIds, reader) {
|
|
301
340
|
if (typeof command.module !== "string" || !moduleIds.has(command.module)) {
|
|
302
341
|
return "it names no usable module";
|
|
303
342
|
}
|
|
@@ -307,7 +346,7 @@ function unusableModuleMetadataBecause(command, moduleIds, reader) {
|
|
|
307
346
|
if (!command.path.startsWith(`${reader.prefix}${command.module}/`)) {
|
|
308
347
|
return "its path is outside the module namespace";
|
|
309
348
|
}
|
|
310
|
-
if (
|
|
349
|
+
if (!isUsableSummary(command.summary)) {
|
|
311
350
|
return "its summary is missing or too long";
|
|
312
351
|
}
|
|
313
352
|
if (typeof command.effect !== "string" || !EFFECTS.has(command.effect)) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spfn",
|
|
3
|
-
"version": "0.3.0-beta.
|
|
3
|
+
"version": "0.3.0-beta.16",
|
|
4
4
|
"description": "Scaffold a full-stack TypeScript backend onto a Next.js app built with an AI coding agent: auth, database, typed routes and codegen, one fixed vertical slice per feature",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|