@mawaru/sdk 0.35.0 → 0.37.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/_schemas/extract-component.test.ts +75 -0
- package/_schemas/extract-component.ts +24 -1
- package/_schemas/graph.test.ts +54 -7
- package/_schemas/graph.ts +11 -6
- package/_schemas/port-spec.test.ts +28 -0
- package/_schemas/port-spec.ts +8 -0
- package/dist/_cli/previewApi.d.ts +10 -0
- package/dist/_cli/previewApi.js +28 -0
- package/dist/_cli/previewCommand.d.ts +18 -0
- package/dist/_cli/previewCommand.js +109 -0
- package/dist/_cli/previewFiles.d.ts +9 -0
- package/dist/_cli/previewFiles.js +64 -0
- package/dist/_cli/previewSlack.d.ts +24 -0
- package/dist/_cli/previewSlack.js +67 -0
- package/dist/_schemas/ai-manifest.d.ts +1 -1
- package/dist/_schemas/extract-component.d.ts +1 -0
- package/dist/_schemas/extract-component.js +17 -2
- package/dist/_schemas/graph.js +9 -6
- package/dist/_schemas/port-spec.d.ts +1 -0
- package/dist/_schemas/port-spec.js +6 -0
- package/dist/cli.js +14 -2
- package/dist/slack-view.d.ts +58 -0
- package/dist/slack-view.js +21 -0
- package/docs/development.md +27 -0
- package/package.json +7 -2
- package/skills/create-loop/SKILL.md +6 -0
- package/slack-view.ts +143 -0
- package/templates/CLAUDE.md +3 -0
|
@@ -417,3 +417,78 @@ describe("buildExtractPlan(子グラフと親置換の生成)", () => {
|
|
|
417
417
|
})
|
|
418
418
|
})
|
|
419
419
|
})
|
|
420
|
+
|
|
421
|
+
// forで配列を要素ごとに処理する(繰り返しの枠).md:繰り返しは結果を配列で返すので出口はちょうど1つ
|
|
422
|
+
describe("buildExtractPlan(singleExit:繰り返しの出口は1つ)", () => {
|
|
423
|
+
const childEndNode = (): GraphNode => ({
|
|
424
|
+
id: uuid(),
|
|
425
|
+
kind: "end",
|
|
426
|
+
name: "終了",
|
|
427
|
+
ports: [port("in", "in", {}), port("out", "main", {})],
|
|
428
|
+
hooks: [],
|
|
429
|
+
position_x: 560,
|
|
430
|
+
position_y: 200,
|
|
431
|
+
})
|
|
432
|
+
const plan = (
|
|
433
|
+
nodes: GraphNode[],
|
|
434
|
+
connections: { from_port_id: string; to_port_id: string }[],
|
|
435
|
+
selectedIds: string[],
|
|
436
|
+
childEnd = childEndNode(),
|
|
437
|
+
) =>
|
|
438
|
+
buildExtractPlan({
|
|
439
|
+
nodes,
|
|
440
|
+
connections,
|
|
441
|
+
selectedIds,
|
|
442
|
+
name: "繰り返し",
|
|
443
|
+
componentLoopId: uuid(),
|
|
444
|
+
childStartOutPortId: uuid(),
|
|
445
|
+
childEnd,
|
|
446
|
+
newId: uuid,
|
|
447
|
+
singleExit: true,
|
|
448
|
+
})
|
|
449
|
+
|
|
450
|
+
it("外へ出る線が無ければ、線の出ていない唯一の出力を出口にする(結果を返す)", () => {
|
|
451
|
+
const { d, nodes, connections } = chain()
|
|
452
|
+
const childEnd = childEndNode()
|
|
453
|
+
|
|
454
|
+
const result = plan(nodes, connections, [d.id], childEnd)
|
|
455
|
+
|
|
456
|
+
if (!result.ok) throw new Error(result.reason)
|
|
457
|
+
const [nd] = result.childNodes
|
|
458
|
+
if (!nd) throw new Error("コピーがない")
|
|
459
|
+
expect(result.childConnections).toContainEqual({
|
|
460
|
+
from_port_id: outOf(nd).id,
|
|
461
|
+
to_port_id: inOf(childEnd).id,
|
|
462
|
+
})
|
|
463
|
+
expect(result.groupNode.ports.filter((p) => p.io === "out")).toHaveLength(1)
|
|
464
|
+
})
|
|
465
|
+
|
|
466
|
+
it("外へ出る線があればそれが出口(ふだんの抽出と同じ)", () => {
|
|
467
|
+
const { b, c, nodes, connections } = chain()
|
|
468
|
+
|
|
469
|
+
const result = plan(nodes, connections, [b.id, c.id])
|
|
470
|
+
|
|
471
|
+
if (!result.ok) throw new Error(result.reason)
|
|
472
|
+
expect(result.childEnd.ports.filter((p) => p.io === "in")).toHaveLength(1)
|
|
473
|
+
})
|
|
474
|
+
|
|
475
|
+
it("出口が2つ以上・出口にできる出力が決まらない選択は拒否する", () => {
|
|
476
|
+
const a = makeNode("program", "A")
|
|
477
|
+
const b = makeNode("program", "B")
|
|
478
|
+
const c = makeNode("program", "C")
|
|
479
|
+
const d = makeNode("program", "D")
|
|
480
|
+
// B と C を選ぶ:B → D、C → D の2つの出口
|
|
481
|
+
const twoExits = plan(
|
|
482
|
+
[a, b, c, d],
|
|
483
|
+
[connect(a, b), connect(a, c), connect(b, d), connect(c, d)],
|
|
484
|
+
[b.id, c.id],
|
|
485
|
+
)
|
|
486
|
+
expect(twoExits.ok).toBe(false)
|
|
487
|
+
if (twoExits.ok) return
|
|
488
|
+
expect(twoExits.reason).toContain("出口を1つ")
|
|
489
|
+
|
|
490
|
+
// 外へ出る線が無く、線の出ていない出力が2つ(B と C)
|
|
491
|
+
const ambiguous = plan([b, c], [], [b.id, c.id])
|
|
492
|
+
expect(ambiguous.ok).toBe(false)
|
|
493
|
+
})
|
|
494
|
+
})
|
|
@@ -118,6 +118,10 @@ export const buildExtractPlan = (params: {
|
|
|
118
118
|
childStartOutPortId: string
|
|
119
119
|
childEnd: GraphNode
|
|
120
120
|
newId: () => string
|
|
121
|
+
// 繰り返し(for):結果を配列で返すので出口はちょうど1つ。外へ出る線が無ければ、
|
|
122
|
+
// 選択の中で線の出ていない唯一の出力を出口にする
|
|
123
|
+
// (forで配列を要素ごとに処理する(繰り返しの枠).md)
|
|
124
|
+
singleExit?: boolean
|
|
121
125
|
}): { ok: false; reason: string } | ({ ok: true } & ExtractPlan) => {
|
|
122
126
|
const {
|
|
123
127
|
nodes,
|
|
@@ -128,11 +132,30 @@ export const buildExtractPlan = (params: {
|
|
|
128
132
|
childStartOutPortId,
|
|
129
133
|
childEnd,
|
|
130
134
|
newId,
|
|
135
|
+
singleExit = false,
|
|
131
136
|
} = params
|
|
132
137
|
const analysis = analyzeExtractSelection(nodes, connections, selectedIds)
|
|
133
138
|
if (!analysis.ok) return analysis
|
|
134
139
|
|
|
135
140
|
const selected = new Set(selectedIds)
|
|
141
|
+
let exitPortIds = analysis.exitPortIds
|
|
142
|
+
if (singleExit) {
|
|
143
|
+
if (exitPortIds.length === 0) {
|
|
144
|
+
const wiredOut = new Set(analysis.internal.map((c) => c.from_port_id))
|
|
145
|
+
const free = nodes
|
|
146
|
+
.filter((n) => selected.has(n.id))
|
|
147
|
+
.flatMap((n) =>
|
|
148
|
+
n.ports
|
|
149
|
+
.filter((p) => p.io === "out" && !wiredOut.has(p.id))
|
|
150
|
+
.map((p) => p.id),
|
|
151
|
+
)
|
|
152
|
+
exitPortIds = free.length === 1 ? free : []
|
|
153
|
+
}
|
|
154
|
+
if (exitPortIds.length !== 1) {
|
|
155
|
+
return { ok: false, reason: "繰り返しの出口を1つにしてください" }
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
136
159
|
// ノード・ポートとも新 id を採番してコピー(kind 別設定・hooks は共有参照で問題ない
|
|
137
160
|
// =この後 JSON 化して保存 API に渡すだけで、エディタ状態には子側を持たない)。
|
|
138
161
|
// node_key は親の版の行との突き合わせ用なので子へは持ち込まない(子 loop の draft に
|
|
@@ -185,7 +208,7 @@ export const buildExtractPlan = (params: {
|
|
|
185
208
|
}
|
|
186
209
|
}
|
|
187
210
|
}
|
|
188
|
-
const exits =
|
|
211
|
+
const exits = exitPortIds.map((exitPortId, i) => {
|
|
189
212
|
const exitName = portById.get(exitPortId)?.name ?? "出力"
|
|
190
213
|
const seeded = i === 0 ? seededEndIns[0] : undefined
|
|
191
214
|
const endIn: GraphPort = seeded
|
package/_schemas/graph.test.ts
CHANGED
|
@@ -997,22 +997,69 @@ describe("saveGraphBodySchema:開始/終了ノード(start/end kind)", ()
|
|
|
997
997
|
).toThrow(/in ポートはちょうど1つ/)
|
|
998
998
|
})
|
|
999
999
|
|
|
1000
|
-
it("
|
|
1001
|
-
const s = startNode()
|
|
1000
|
+
it("end からの接続を拒否する", () => {
|
|
1002
1001
|
const e = endNode()
|
|
1003
1002
|
const other = makeNode()
|
|
1003
|
+
expect(() =>
|
|
1004
|
+
saveGraphBodySchema.parse({
|
|
1005
|
+
nodes: [e, other],
|
|
1006
|
+
connections: [connect(e, other)],
|
|
1007
|
+
}),
|
|
1008
|
+
).toThrow(/終了ノード/)
|
|
1009
|
+
})
|
|
1010
|
+
|
|
1011
|
+
// start の入口へ戻すと次の周回になる
|
|
1012
|
+
// (docs/tasks/wip/startノードに入力をつないで周回する(runの周回).md)
|
|
1013
|
+
it("start の入口へ1本つなげる(周回の最後から戻す)", () => {
|
|
1014
|
+
const s = startNode()
|
|
1015
|
+
const other = { ...makeNode(simplePorts()), kind: "wait" }
|
|
1004
1016
|
expect(() =>
|
|
1005
1017
|
saveGraphBodySchema.parse({
|
|
1006
1018
|
nodes: [s, other],
|
|
1007
|
-
connections: [connect(other, s)],
|
|
1019
|
+
connections: [connect(s, other), connect(other, s)],
|
|
1008
1020
|
}),
|
|
1009
|
-
).toThrow(
|
|
1021
|
+
).not.toThrow()
|
|
1022
|
+
})
|
|
1023
|
+
|
|
1024
|
+
it("start の入口につなげる線は1本だけ", () => {
|
|
1025
|
+
const s = startNode()
|
|
1026
|
+
const a = makeNode()
|
|
1027
|
+
const b = makeNode()
|
|
1010
1028
|
expect(() =>
|
|
1011
1029
|
saveGraphBodySchema.parse({
|
|
1012
|
-
nodes: [
|
|
1013
|
-
connections: [connect(
|
|
1030
|
+
nodes: [s, a, b],
|
|
1031
|
+
connections: [connect(a, s), connect(b, s)],
|
|
1014
1032
|
}),
|
|
1015
|
-
).toThrow(
|
|
1033
|
+
).toThrow(/開始ノードの入口につなげる線は1本だけです/)
|
|
1034
|
+
})
|
|
1035
|
+
|
|
1036
|
+
it("start の入口の型(直後のノードが受ける型)に合わない線を拒否する", () => {
|
|
1037
|
+
const s = startNode()
|
|
1038
|
+
const objectIn = {
|
|
1039
|
+
type: "object",
|
|
1040
|
+
properties: { title: { type: "string" } },
|
|
1041
|
+
required: ["title"],
|
|
1042
|
+
}
|
|
1043
|
+
const next = {
|
|
1044
|
+
...makeNode([
|
|
1045
|
+
{ ...port("in", "in"), schema: objectIn },
|
|
1046
|
+
port("out", "main"),
|
|
1047
|
+
]),
|
|
1048
|
+
kind: "program",
|
|
1049
|
+
}
|
|
1050
|
+
const last = {
|
|
1051
|
+
...makeNode([
|
|
1052
|
+
port("in", "in"),
|
|
1053
|
+
{ ...port("out", "main"), schema: { type: "string" } },
|
|
1054
|
+
]),
|
|
1055
|
+
kind: "program",
|
|
1056
|
+
}
|
|
1057
|
+
expect(() =>
|
|
1058
|
+
saveGraphBodySchema.parse({
|
|
1059
|
+
nodes: [s, next, last],
|
|
1060
|
+
connections: [connect(s, next), connect(next, last), connect(last, s)],
|
|
1061
|
+
}),
|
|
1062
|
+
).toThrow(/入力スキーマに適合しません/)
|
|
1016
1063
|
})
|
|
1017
1064
|
})
|
|
1018
1065
|
|
package/_schemas/graph.ts
CHANGED
|
@@ -292,9 +292,10 @@ export const graphNodeSchema = z.object({
|
|
|
292
292
|
position_y: z.number(),
|
|
293
293
|
// 表示アイコン(全 kind 共通の任意設定。human はフロントが担当から導出するので通常設定しない)
|
|
294
294
|
icon: nodeIconSchema.optional(),
|
|
295
|
-
// Slack ビュー(全 kind
|
|
296
|
-
// human の view
|
|
297
|
-
//
|
|
295
|
+
// Slack ビュー(全 kind 共通の任意設定。実行中(human は承認待ち)・完了メッセージの見せ方)。
|
|
296
|
+
// human の view と同じ「不透明なキー」で、選べる key は GET slack-views が返す。
|
|
297
|
+
// <slug>/<name> は実行 repo の views/slack/<name>/(@mawaru/sdk/slack-view の契約で書く)。
|
|
298
|
+
// 保存時に mawaru が見るのは common/* の実在と <slug>/* の可視性だけ。
|
|
298
299
|
// 省略=承認待ちは承認ボタンだけ・完了は ✅ の1行(データは出さない)。
|
|
299
300
|
// ポートの型には一切影響しない
|
|
300
301
|
slack_view: z.string().min(1).optional(),
|
|
@@ -521,12 +522,16 @@ export const saveGraphBodySchema = z
|
|
|
521
522
|
const toNode = nodeByPortId.get(conn.to_port_id)
|
|
522
523
|
if (!from || !to || !fromNode || !toNode) return // 構造違反は検出済み
|
|
523
524
|
|
|
524
|
-
// start
|
|
525
|
-
|
|
525
|
+
// end からは引き出せない(終端)。start の入口へは戻せる(次の周回になる)が、
|
|
526
|
+
// 線は1本だけ(2本あると両方が着いたときに周回が2本並んで走る)
|
|
527
|
+
if (
|
|
528
|
+
toNode.kind === "start" &&
|
|
529
|
+
(inboundByPort.get(conn.to_port_id)?.length ?? 0) > 1
|
|
530
|
+
) {
|
|
526
531
|
ctx.addIssue({
|
|
527
532
|
code: "custom",
|
|
528
533
|
path: ["connections", i, "to_port_id"],
|
|
529
|
-
message: "
|
|
534
|
+
message: "開始ノードの入口につなげる線は1本だけです",
|
|
530
535
|
})
|
|
531
536
|
}
|
|
532
537
|
if (fromNode.kind === "end") {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest"
|
|
2
2
|
import type { GraphPort, JsonSchema } from "./node.js"
|
|
3
3
|
import {
|
|
4
|
+
arrayOf,
|
|
4
5
|
dataFormatOf,
|
|
5
6
|
dataSchemaOf,
|
|
6
7
|
fileSchema,
|
|
@@ -1116,3 +1117,30 @@ describe("schemaTypeKey(型としての同一性)", () => {
|
|
|
1116
1117
|
).not.toBe(schemaTypeKey({ type: "array", items: [{ type: "number" }] }))
|
|
1117
1118
|
})
|
|
1118
1119
|
})
|
|
1120
|
+
|
|
1121
|
+
describe("arrayOf(for の配置ポート:1件分の型を配列に包む)", () => {
|
|
1122
|
+
it("宣言された型は items に入れた配列になる", () => {
|
|
1123
|
+
const error = { type: "object", properties: { id: { type: "string" } } }
|
|
1124
|
+
expect(arrayOf(error)).toEqual({ type: "array", items: error })
|
|
1125
|
+
})
|
|
1126
|
+
|
|
1127
|
+
it("未宣言の型は items を持たない配列になる(要素の型は判断できないまま)", () => {
|
|
1128
|
+
expect(arrayOf(UNDECLARED_SCHEMA)).toEqual({ type: "array" })
|
|
1129
|
+
})
|
|
1130
|
+
|
|
1131
|
+
it("配列にした型同士は要素の型で適合を判定する", () => {
|
|
1132
|
+
const narrow = {
|
|
1133
|
+
type: "object",
|
|
1134
|
+
properties: { id: { type: "string" }, title: { type: "string" } },
|
|
1135
|
+
required: ["id", "title"],
|
|
1136
|
+
}
|
|
1137
|
+
const wide = {
|
|
1138
|
+
type: "object",
|
|
1139
|
+
properties: { id: { type: "string" } },
|
|
1140
|
+
required: ["id"],
|
|
1141
|
+
}
|
|
1142
|
+
expect(schemaFit(arrayOf(narrow), arrayOf(wide))).toBe("ok")
|
|
1143
|
+
expect(schemaFit(arrayOf(wide), arrayOf(narrow))).toBe("mismatch")
|
|
1144
|
+
expect(schemaFit(wide, arrayOf(wide))).toBe("mismatch")
|
|
1145
|
+
})
|
|
1146
|
+
})
|
package/_schemas/port-spec.ts
CHANGED
|
@@ -15,6 +15,14 @@ export const UNDECLARED_SCHEMA: JsonSchema = {}
|
|
|
15
15
|
export const isUndeclaredSchema = (schema: JsonSchema): boolean =>
|
|
16
16
|
Object.keys(schema).length === 0 || schema.type === undefined
|
|
17
17
|
|
|
18
|
+
// 1件分の型を配列に包む(for の配置ポート:in = 中身の start.in の配列、out = 中身の
|
|
19
|
+
// end の in の配列。docs/tasks/done/forで配列を要素ごとに処理する(繰り返しの枠).md)。
|
|
20
|
+
// 未宣言は items を持たせない(要素の型は判断できないまま)
|
|
21
|
+
export const arrayOf = (schema: JsonSchema): JsonSchema =>
|
|
22
|
+
isUndeclaredSchema(schema)
|
|
23
|
+
? { type: "array" }
|
|
24
|
+
: { type: "array", items: schema }
|
|
25
|
+
|
|
18
26
|
// ---- file 型(docs/tasks/done/AIノードのファイル入力(file型と添付).md) ----
|
|
19
27
|
// schema 上の file は x-mawaru-type マーカー付きの object(nominal に識別する)。値はバイト
|
|
20
28
|
// ではなく storage path の参照で、署名 URL はどこにも焼き込まない。
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type Credentials } from "./credentials.js";
|
|
2
|
+
export type MawaruApi = (method: "GET" | "POST", path: string, body?: unknown) => Promise<{
|
|
3
|
+
status: number;
|
|
4
|
+
body: unknown;
|
|
5
|
+
}>;
|
|
6
|
+
export declare const mawaruApiFor: (credentials: Credentials, tenantId: string) => MawaruApi;
|
|
7
|
+
export declare const expectOk: (what: string, res: {
|
|
8
|
+
status: number;
|
|
9
|
+
body: unknown;
|
|
10
|
+
}) => unknown;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { accessTokenFrom } from "./credentials.js";
|
|
2
|
+
export const mawaruApiFor = (credentials, tenantId) => async (method, path, body) => {
|
|
3
|
+
const token = await accessTokenFrom(credentials);
|
|
4
|
+
const res = await fetch(`${credentials.api_url}/tenants/${tenantId}${path}`, {
|
|
5
|
+
method,
|
|
6
|
+
headers: {
|
|
7
|
+
Authorization: `Bearer ${token}`,
|
|
8
|
+
...(body === undefined ? {} : { "content-type": "application/json" }),
|
|
9
|
+
},
|
|
10
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
11
|
+
});
|
|
12
|
+
const text = await res.text();
|
|
13
|
+
try {
|
|
14
|
+
return { status: res.status, body: text ? JSON.parse(text) : null };
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return { status: res.status, body: { message: text } };
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
// 2xx 以外は API の message を添えて失敗にする(403 は admin でない・409 は Slack 未接続 等)
|
|
21
|
+
export const expectOk = (what, res) => {
|
|
22
|
+
if (res.status >= 200 && res.status < 300)
|
|
23
|
+
return res.body;
|
|
24
|
+
const message = res.body && typeof res.body === "object" && "message" in res.body
|
|
25
|
+
? String(res.body.message)
|
|
26
|
+
: `HTTP ${res.status}`;
|
|
27
|
+
throw new Error(`${what}に失敗しました(${res.status}): ${message}`);
|
|
28
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type MawaruApi } from "./previewApi.js";
|
|
2
|
+
import { type SlackSurface } from "./previewSlack.js";
|
|
3
|
+
export type PreviewOptions = {
|
|
4
|
+
target: string;
|
|
5
|
+
case?: string;
|
|
6
|
+
surface?: SlackSurface;
|
|
7
|
+
channel?: string;
|
|
8
|
+
};
|
|
9
|
+
export declare const parsePreviewArgs: (args: string[]) => PreviewOptions;
|
|
10
|
+
type Log = (line: string) => void;
|
|
11
|
+
export declare const runPreviewWith: (params: {
|
|
12
|
+
root: string;
|
|
13
|
+
options: PreviewOptions;
|
|
14
|
+
api: MawaruApi;
|
|
15
|
+
log: Log;
|
|
16
|
+
}) => Promise<number>;
|
|
17
|
+
export declare const runPreview: (args: string[]) => Promise<number>;
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { parseArgs } from "node:util";
|
|
4
|
+
import { requireCredentials } from "./credentials.js";
|
|
5
|
+
import { expectOk, mawaruApiFor } from "./previewApi.js";
|
|
6
|
+
import { readViewFiles, resolveView } from "./previewFiles.js";
|
|
7
|
+
import { previewSlack, SLACK_SURFACES, } from "./previewSlack.js";
|
|
8
|
+
// `mawaru preview <種類>/<名前> [--case <ケース名>] [--surface running|completed] [--channel <#名前|ID>]`
|
|
9
|
+
// (実行 repo のルートで実行)
|
|
10
|
+
//
|
|
11
|
+
// views/<種類>/<名前>/ のビューを、push せずに手元のファイルのまま確かめる。描画は mawaru API がやるので、
|
|
12
|
+
// `mawaru login` のセッション(web と同じ membership)と、どのテナントのつもりで描くかの
|
|
13
|
+
// MAWARU_TENANT_ID(repo の .env。create-loop スキルと同じ)が要る。
|
|
14
|
+
// ファイルの集め方・cases.ts の評価・ケースの絞り込みは種類をまたいで共通で、種類ごとに「確かめ方」
|
|
15
|
+
// (Slack は送る。承認ビューは repo へ移すときに足す)だけを差し替える
|
|
16
|
+
// (docs/tasks/wip/Slackビューの顧客repo配置.md の「手元プレビュー」)
|
|
17
|
+
const USAGE = "使い方: npx mawaru preview <種類>/<名前> [--case <ケース名>] [--surface running|completed] [--channel <#名前|チャンネル ID>]\n" +
|
|
18
|
+
" 例: npx mawaru preview slack/pr-review(views/slack/pr-review/ の cases.ts の全ケースを Slack に送る)";
|
|
19
|
+
export const parsePreviewArgs = (args) => {
|
|
20
|
+
const { values, positionals } = parseArgs({
|
|
21
|
+
args,
|
|
22
|
+
allowPositionals: true,
|
|
23
|
+
options: {
|
|
24
|
+
case: { type: "string" },
|
|
25
|
+
surface: { type: "string" },
|
|
26
|
+
channel: { type: "string" },
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
const [target, ...extra] = positionals;
|
|
30
|
+
if (!target || extra.length > 0)
|
|
31
|
+
throw new Error(USAGE);
|
|
32
|
+
if (values.surface !== undefined &&
|
|
33
|
+
!SLACK_SURFACES.includes(values.surface)) {
|
|
34
|
+
throw new Error("--surface には running か completed を指定してください");
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
target,
|
|
38
|
+
...(values.case !== undefined ? { case: values.case } : {}),
|
|
39
|
+
...(values.surface !== undefined
|
|
40
|
+
? { surface: values.surface }
|
|
41
|
+
: {}),
|
|
42
|
+
...(values.channel !== undefined ? { channel: values.channel } : {}),
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
const PREVIEWERS = {
|
|
46
|
+
slack: {
|
|
47
|
+
casesPath: "/slack-views/cases",
|
|
48
|
+
run: ({ name, files, cases, options, api, log }) => {
|
|
49
|
+
const channel = options.channel ?? process.env.MAWARU_PREVIEW_CHANNEL;
|
|
50
|
+
if (!channel) {
|
|
51
|
+
throw new Error("送り先のチャンネルを --channel か repo の .env の MAWARU_PREVIEW_CHANNEL で指定してください(#名前 かチャンネル ID)");
|
|
52
|
+
}
|
|
53
|
+
return previewSlack({
|
|
54
|
+
name,
|
|
55
|
+
files,
|
|
56
|
+
cases: cases,
|
|
57
|
+
surfaces: options.surface ? [options.surface] : SLACK_SURFACES,
|
|
58
|
+
channel,
|
|
59
|
+
api,
|
|
60
|
+
log,
|
|
61
|
+
});
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
// 終了コードを返す(描画に失敗したものがあれば 1)。テストはここを呼ぶ
|
|
66
|
+
export const runPreviewWith = async (params) => {
|
|
67
|
+
const { root, options, api, log } = params;
|
|
68
|
+
const view = resolveView(root, options.target);
|
|
69
|
+
const files = readViewFiles(root, view);
|
|
70
|
+
const previewer = PREVIEWERS[view.kind];
|
|
71
|
+
const { cases } = expectOk("ケースの読み込み", await api("POST", previewer.casesPath, { files }));
|
|
72
|
+
const picked = options.case === undefined
|
|
73
|
+
? cases
|
|
74
|
+
: cases.filter((c) => c.name === options.case);
|
|
75
|
+
if (picked.length === 0) {
|
|
76
|
+
throw new Error(options.case === undefined
|
|
77
|
+
? "cases.ts にケースがありません"
|
|
78
|
+
: `ケース「${options.case}」がありません(ある: ${cases.map((c) => c.name).join(", ")})`);
|
|
79
|
+
}
|
|
80
|
+
const failed = await previewer.run({
|
|
81
|
+
name: view.name,
|
|
82
|
+
files,
|
|
83
|
+
cases: picked,
|
|
84
|
+
options,
|
|
85
|
+
api,
|
|
86
|
+
log,
|
|
87
|
+
});
|
|
88
|
+
if (failed > 0)
|
|
89
|
+
log(`失敗 ${failed} 件`);
|
|
90
|
+
return failed > 0 ? 1 : 0;
|
|
91
|
+
};
|
|
92
|
+
export const runPreview = async (args) => {
|
|
93
|
+
const options = parsePreviewArgs(args);
|
|
94
|
+
const root = process.cwd();
|
|
95
|
+
const env = join(root, ".env");
|
|
96
|
+
if (existsSync(env))
|
|
97
|
+
process.loadEnvFile(env);
|
|
98
|
+
const tenantId = process.env.MAWARU_TENANT_ID;
|
|
99
|
+
if (!tenantId) {
|
|
100
|
+
throw new Error("MAWARU_TENANT_ID が未設定です。repo の .env に MAWARU_TENANT_ID=<テナントの ID> を書いてください。");
|
|
101
|
+
}
|
|
102
|
+
const credentials = requireCredentials();
|
|
103
|
+
return runPreviewWith({
|
|
104
|
+
root,
|
|
105
|
+
options,
|
|
106
|
+
api: mawaruApiFor(credentials, tenantId),
|
|
107
|
+
log: (line) => console.log(line),
|
|
108
|
+
});
|
|
109
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare const VIEW_KINDS: readonly ["slack"];
|
|
2
|
+
export type ViewKind = (typeof VIEW_KINDS)[number];
|
|
3
|
+
export type ViewTarget = {
|
|
4
|
+
kind: ViewKind;
|
|
5
|
+
name: string;
|
|
6
|
+
};
|
|
7
|
+
export declare const listViews: (root: string) => ViewTarget[];
|
|
8
|
+
export declare const resolveView: (root: string, spec: string) => ViewTarget;
|
|
9
|
+
export declare const readViewFiles: (root: string, view: ViewTarget) => Record<string, string>;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { readdirSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
// 手元プレビュー(`mawaru preview`)が読む、実行 repo の views/<種類>/<名前>/ のファイル。
|
|
4
|
+
// 描画は mawaru API がやるので、ここは「対象を決めてファイルを集める」だけで、種類をまたいで共通
|
|
5
|
+
// (docs/tasks/wip/Slackビューの顧客repo配置.md の「手元プレビュー」)
|
|
6
|
+
// 手元プレビューできるビューの種類(views/ 直下のディレクトリ名)。承認ビューを repo へ移すときに
|
|
7
|
+
// "approval" を足す(previewCommand.ts の振り分けにも足す)
|
|
8
|
+
export const VIEW_KINDS = ["slack"];
|
|
9
|
+
const dirsIn = (dir) => {
|
|
10
|
+
try {
|
|
11
|
+
return readdirSync(dir, { withFileTypes: true })
|
|
12
|
+
.filter((entry) => entry.isDirectory() && !entry.name.startsWith("."))
|
|
13
|
+
.map((entry) => entry.name)
|
|
14
|
+
.sort();
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return [];
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
// views/<種類>/ 直下のディレクトリ(=ビュー)。無ければ空
|
|
21
|
+
export const listViews = (root) => VIEW_KINDS.flatMap((kind) => dirsIn(join(root, "views", kind)).map((name) => ({ kind, name })));
|
|
22
|
+
const labelOf = (view) => `${view.kind}/${view.name}`;
|
|
23
|
+
// 指定(views/slack/pr・slack/pr・pr)をビューに決める。一覧にあるものだけを受ける
|
|
24
|
+
// (../ でディレクトリの外を読ませない)。決まらなければ、あるビューを添えて失敗する
|
|
25
|
+
export const resolveView = (root, spec) => {
|
|
26
|
+
const normalized = spec
|
|
27
|
+
.replace(/^\.\//, "")
|
|
28
|
+
.replace(/^views\//, "")
|
|
29
|
+
.replace(/\/+$/, "");
|
|
30
|
+
const views = listViews(root);
|
|
31
|
+
const found = normalized.includes("/")
|
|
32
|
+
? views.filter((view) => labelOf(view) === normalized)
|
|
33
|
+
: views.filter((view) => view.name === normalized);
|
|
34
|
+
if (found.length === 1 && found[0])
|
|
35
|
+
return found[0];
|
|
36
|
+
const known = views.length === 0
|
|
37
|
+
? "views/ にビューがありません"
|
|
38
|
+
: `ある対象: ${views.map(labelOf).join(", ")}`;
|
|
39
|
+
throw new Error(found.length > 1
|
|
40
|
+
? `${spec} は複数の種類にあります。<種類>/<名前> で指定してください(${found.map(labelOf).join(", ")})`
|
|
41
|
+
: `${spec} が見つかりません(${known})`);
|
|
42
|
+
};
|
|
43
|
+
// mawaru API へ送る files({ ビューのディレクトリからの相対パス(/ 区切り): 中身 })。
|
|
44
|
+
// .ts だけをサブディレクトリまで集める(上限の検証は API 側)
|
|
45
|
+
export const readViewFiles = (root, view) => {
|
|
46
|
+
const dir = join(root, "views", view.kind, view.name);
|
|
47
|
+
const files = {};
|
|
48
|
+
const walk = (relative) => {
|
|
49
|
+
for (const entry of readdirSync(join(dir, relative), {
|
|
50
|
+
withFileTypes: true,
|
|
51
|
+
})) {
|
|
52
|
+
if (entry.name.startsWith("."))
|
|
53
|
+
continue;
|
|
54
|
+
const path = relative ? `${relative}/${entry.name}` : entry.name;
|
|
55
|
+
if (entry.isDirectory())
|
|
56
|
+
walk(path);
|
|
57
|
+
else if (entry.isFile() && entry.name.endsWith(".ts")) {
|
|
58
|
+
files[path] = readFileSync(join(dir, path), "utf8");
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
walk("");
|
|
63
|
+
return files;
|
|
64
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { SlackViewCase } from "../slack-view.js";
|
|
2
|
+
import { type MawaruApi } from "./previewApi.js";
|
|
3
|
+
export type SlackSurface = "running" | "completed";
|
|
4
|
+
export declare const SLACK_SURFACES: SlackSurface[];
|
|
5
|
+
export declare const previewBodyOf: (files: Record<string, string>, testCase: SlackViewCase, surface: SlackSurface) => {
|
|
6
|
+
refs?: {} | null | undefined;
|
|
7
|
+
output_schema?: Record<string, unknown> | undefined;
|
|
8
|
+
output?: {} | null | undefined;
|
|
9
|
+
input_schema?: Record<string, unknown> | undefined;
|
|
10
|
+
files: Record<string, string>;
|
|
11
|
+
surface: SlackSurface;
|
|
12
|
+
kind: "start" | "join" | "ai" | "human" | "program" | "guardrail" | "wait" | "end" | "component";
|
|
13
|
+
node_name: string;
|
|
14
|
+
input: unknown;
|
|
15
|
+
};
|
|
16
|
+
export declare const previewSlack: (params: {
|
|
17
|
+
name: string;
|
|
18
|
+
files: Record<string, string>;
|
|
19
|
+
cases: SlackViewCase[];
|
|
20
|
+
surfaces: SlackSurface[];
|
|
21
|
+
channel: string;
|
|
22
|
+
api: MawaruApi;
|
|
23
|
+
log: (line: string) => void;
|
|
24
|
+
}) => Promise<number>;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { expectOk } from "./previewApi.js";
|
|
2
|
+
export const SLACK_SURFACES = ["running", "completed"];
|
|
3
|
+
const SURFACE_LABEL = {
|
|
4
|
+
running: "実行中",
|
|
5
|
+
completed: "完了",
|
|
6
|
+
};
|
|
7
|
+
// ケース(cases.ts の SlackViewCase)を mawaru のプレビュー API の body にする
|
|
8
|
+
export const previewBodyOf = (files, testCase, surface) => ({
|
|
9
|
+
files,
|
|
10
|
+
surface,
|
|
11
|
+
kind: testCase.kind ?? "human",
|
|
12
|
+
node_name: testCase.nodeName ?? testCase.name,
|
|
13
|
+
input: testCase.input,
|
|
14
|
+
...(testCase.inputSchema !== undefined
|
|
15
|
+
? { input_schema: testCase.inputSchema }
|
|
16
|
+
: {}),
|
|
17
|
+
...(testCase.output !== undefined ? { output: testCase.output } : {}),
|
|
18
|
+
...(testCase.outputSchema !== undefined
|
|
19
|
+
? { output_schema: testCase.outputSchema }
|
|
20
|
+
: {}),
|
|
21
|
+
...(testCase.refs !== undefined ? { refs: testCase.refs } : {}),
|
|
22
|
+
});
|
|
23
|
+
// 送り先:チャンネル ID はそのまま、#名前(または名前)は mawaru のボットが見えるチャンネルから引き直す
|
|
24
|
+
const CHANNEL_ID = /^[CGD][A-Z0-9]{6,}$/;
|
|
25
|
+
const resolveChannel = async (api, channel) => {
|
|
26
|
+
if (!channel.startsWith("#") && CHANNEL_ID.test(channel))
|
|
27
|
+
return channel;
|
|
28
|
+
const name = channel.replace(/^#/, "");
|
|
29
|
+
const body = expectOk("チャンネルの一覧の取得", await api("GET", "/integrations/slack/channels"));
|
|
30
|
+
const found = (body.channels ?? []).find((c) => c.name === name);
|
|
31
|
+
if (!found) {
|
|
32
|
+
throw new Error(`#${name} が見つかりません(mawaru のボットが参加しているチャンネルを指定してください)`);
|
|
33
|
+
}
|
|
34
|
+
return found.id;
|
|
35
|
+
};
|
|
36
|
+
// 送ったうちの、描画に失敗した(フォールバックした理由がある)数を返す
|
|
37
|
+
export const previewSlack = async (params) => {
|
|
38
|
+
const { name, files, cases, surfaces, api, log } = params;
|
|
39
|
+
const channelId = await resolveChannel(api, params.channel);
|
|
40
|
+
log(`🧪 ${name}:${cases.length} ケース × ${surfaces.length} 面を ${params.channel} に送ります`);
|
|
41
|
+
let thread;
|
|
42
|
+
let failed = 0;
|
|
43
|
+
for (const testCase of cases) {
|
|
44
|
+
for (const surface of surfaces) {
|
|
45
|
+
const result = expectOk("Slack への送信", await api("POST", "/slack-views/test-post", {
|
|
46
|
+
...previewBodyOf(files, testCase, surface),
|
|
47
|
+
channel_id: channelId,
|
|
48
|
+
...(thread
|
|
49
|
+
? { thread_ts: thread }
|
|
50
|
+
: { header: `🧪 ${name} のプレビュー` }),
|
|
51
|
+
}));
|
|
52
|
+
thread ??= result.thread_ts;
|
|
53
|
+
const label = `${testCase.name}/${SURFACE_LABEL[surface]}`;
|
|
54
|
+
if (result.error) {
|
|
55
|
+
failed += 1;
|
|
56
|
+
log(`⚠ ${label}:フォールバック(${result.error})`);
|
|
57
|
+
}
|
|
58
|
+
else if (result.fallback) {
|
|
59
|
+
log(`– ${label}:未実装(mawaru の既定の表示)`);
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
log(`✔ ${label}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return failed;
|
|
67
|
+
};
|
|
@@ -78,8 +78,8 @@ export declare const aiManifestSchema: z.ZodObject<{
|
|
|
78
78
|
"gpt-5.6-luna": "gpt-5.6-luna";
|
|
79
79
|
}>>;
|
|
80
80
|
auth_mode: z.ZodOptional<z.ZodEnum<{
|
|
81
|
-
subscription: "subscription";
|
|
82
81
|
api: "api";
|
|
82
|
+
subscription: "subscription";
|
|
83
83
|
}>>;
|
|
84
84
|
skills: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
85
85
|
env: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
@@ -55,11 +55,26 @@ export const analyzeExtractSelection = (nodes, connections, selectedIds) => {
|
|
|
55
55
|
// end の in が1つの loop は out main の1口として振る舞う)
|
|
56
56
|
const SINGLE_OUT_KEY = "main";
|
|
57
57
|
export const buildExtractPlan = (params) => {
|
|
58
|
-
const { nodes, connections, selectedIds, name, componentLoopId, childStartOutPortId, childEnd, newId, } = params;
|
|
58
|
+
const { nodes, connections, selectedIds, name, componentLoopId, childStartOutPortId, childEnd, newId, singleExit = false, } = params;
|
|
59
59
|
const analysis = analyzeExtractSelection(nodes, connections, selectedIds);
|
|
60
60
|
if (!analysis.ok)
|
|
61
61
|
return analysis;
|
|
62
62
|
const selected = new Set(selectedIds);
|
|
63
|
+
let exitPortIds = analysis.exitPortIds;
|
|
64
|
+
if (singleExit) {
|
|
65
|
+
if (exitPortIds.length === 0) {
|
|
66
|
+
const wiredOut = new Set(analysis.internal.map((c) => c.from_port_id));
|
|
67
|
+
const free = nodes
|
|
68
|
+
.filter((n) => selected.has(n.id))
|
|
69
|
+
.flatMap((n) => n.ports
|
|
70
|
+
.filter((p) => p.io === "out" && !wiredOut.has(p.id))
|
|
71
|
+
.map((p) => p.id));
|
|
72
|
+
exitPortIds = free.length === 1 ? free : [];
|
|
73
|
+
}
|
|
74
|
+
if (exitPortIds.length !== 1) {
|
|
75
|
+
return { ok: false, reason: "繰り返しの出口を1つにしてください" };
|
|
76
|
+
}
|
|
77
|
+
}
|
|
63
78
|
// ノード・ポートとも新 id を採番してコピー(kind 別設定・hooks は共有参照で問題ない
|
|
64
79
|
// =この後 JSON 化して保存 API に渡すだけで、エディタ状態には子側を持たない)。
|
|
65
80
|
// node_key は親の版の行との突き合わせ用なので子へは持ち込まない(子 loop の draft に
|
|
@@ -108,7 +123,7 @@ export const buildExtractPlan = (params) => {
|
|
|
108
123
|
}
|
|
109
124
|
}
|
|
110
125
|
};
|
|
111
|
-
const exits =
|
|
126
|
+
const exits = exitPortIds.map((exitPortId, i) => {
|
|
112
127
|
const exitName = portById.get(exitPortId)?.name ?? "出力";
|
|
113
128
|
const seeded = i === 0 ? seededEndIns[0] : undefined;
|
|
114
129
|
const endIn = seeded
|
package/dist/_schemas/graph.js
CHANGED
|
@@ -235,9 +235,10 @@ export const graphNodeSchema = z.object({
|
|
|
235
235
|
position_y: z.number(),
|
|
236
236
|
// 表示アイコン(全 kind 共通の任意設定。human はフロントが担当から導出するので通常設定しない)
|
|
237
237
|
icon: nodeIconSchema.optional(),
|
|
238
|
-
// Slack ビュー(全 kind
|
|
239
|
-
// human の view
|
|
240
|
-
//
|
|
238
|
+
// Slack ビュー(全 kind 共通の任意設定。実行中(human は承認待ち)・完了メッセージの見せ方)。
|
|
239
|
+
// human の view と同じ「不透明なキー」で、選べる key は GET slack-views が返す。
|
|
240
|
+
// <slug>/<name> は実行 repo の views/slack/<name>/(@mawaru/sdk/slack-view の契約で書く)。
|
|
241
|
+
// 保存時に mawaru が見るのは common/* の実在と <slug>/* の可視性だけ。
|
|
241
242
|
// 省略=承認待ちは承認ボタンだけ・完了は ✅ の1行(データは出さない)。
|
|
242
243
|
// ポートの型には一切影響しない
|
|
243
244
|
slack_view: z.string().min(1).optional(),
|
|
@@ -447,12 +448,14 @@ export const saveGraphBodySchema = z
|
|
|
447
448
|
const toNode = nodeByPortId.get(conn.to_port_id);
|
|
448
449
|
if (!from || !to || !fromNode || !toNode)
|
|
449
450
|
return; // 構造違反は検出済み
|
|
450
|
-
// start
|
|
451
|
-
|
|
451
|
+
// end からは引き出せない(終端)。start の入口へは戻せる(次の周回になる)が、
|
|
452
|
+
// 線は1本だけ(2本あると両方が着いたときに周回が2本並んで走る)
|
|
453
|
+
if (toNode.kind === "start" &&
|
|
454
|
+
(inboundByPort.get(conn.to_port_id)?.length ?? 0) > 1) {
|
|
452
455
|
ctx.addIssue({
|
|
453
456
|
code: "custom",
|
|
454
457
|
path: ["connections", i, "to_port_id"],
|
|
455
|
-
message: "
|
|
458
|
+
message: "開始ノードの入口につなげる線は1本だけです",
|
|
456
459
|
});
|
|
457
460
|
}
|
|
458
461
|
if (fromNode.kind === "end") {
|
|
@@ -2,6 +2,7 @@ import { z } from "zod";
|
|
|
2
2
|
import type { GraphPort, JsonSchema } from "./node.js";
|
|
3
3
|
export declare const UNDECLARED_SCHEMA: JsonSchema;
|
|
4
4
|
export declare const isUndeclaredSchema: (schema: JsonSchema) => boolean;
|
|
5
|
+
export declare const arrayOf: (schema: JsonSchema) => JsonSchema;
|
|
5
6
|
export declare const FILE_SCHEMA_MARKER = "x-mawaru-type";
|
|
6
7
|
export declare const FILE_SCHEMA_TYPE = "file";
|
|
7
8
|
export declare const fileSchema: () => JsonSchema;
|
|
@@ -9,6 +9,12 @@ import { z } from "zod";
|
|
|
9
9
|
// (docs/tasks/done/ポートスキーマのany撲滅.md)
|
|
10
10
|
export const UNDECLARED_SCHEMA = {};
|
|
11
11
|
export const isUndeclaredSchema = (schema) => Object.keys(schema).length === 0 || schema.type === undefined;
|
|
12
|
+
// 1件分の型を配列に包む(for の配置ポート:in = 中身の start.in の配列、out = 中身の
|
|
13
|
+
// end の in の配列。docs/tasks/done/forで配列を要素ごとに処理する(繰り返しの枠).md)。
|
|
14
|
+
// 未宣言は items を持たせない(要素の型は判断できないまま)
|
|
15
|
+
export const arrayOf = (schema) => isUndeclaredSchema(schema)
|
|
16
|
+
? { type: "array" }
|
|
17
|
+
: { type: "array", items: schema };
|
|
12
18
|
// ---- file 型(docs/tasks/done/AIノードのファイル入力(file型と添付).md) ----
|
|
13
19
|
// schema 上の file は x-mawaru-type マーカー付きの object(nominal に識別する)。値はバイト
|
|
14
20
|
// ではなく storage path の参照で、署名 URL はどこにも焼き込まない。
|
package/dist/cli.js
CHANGED
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
import { relative } from "node:path";
|
|
3
3
|
import { clearCredentials, loadCredentials } from "./_cli/credentials.js";
|
|
4
4
|
import { runLogin } from "./_cli/login.js";
|
|
5
|
+
import { runPreview } from "./_cli/previewCommand.js";
|
|
5
6
|
import { runSessionResume } from "./_cli/sessionCommand.js";
|
|
6
7
|
import { runInit } from "./init.js";
|
|
7
8
|
import { runTypegen } from "./typegen.js";
|
|
8
9
|
import { validateRepo } from "./validate.js";
|
|
9
|
-
// 使い方: mawaru <init|typegen|validate|login|logout|session> [引数]
|
|
10
|
+
// 使い方: mawaru <init|typegen|validate|login|logout|session|preview> [引数]
|
|
10
11
|
const USAGE = `使い方: mawaru <command> [引数]
|
|
11
12
|
|
|
12
13
|
コマンド:
|
|
@@ -16,7 +17,9 @@ const USAGE = `使い方: mawaru <command> [引数]
|
|
|
16
17
|
login ブラウザで承認して mawaru にログインする(~/.mawaru/credentials.json)
|
|
17
18
|
logout 保存した資格情報を消す
|
|
18
19
|
session resume <step URL>
|
|
19
|
-
AI ノードのセッションを手元に復元して CLI(claude / codex)の resume
|
|
20
|
+
AI ノードのセッションを手元に復元して CLI(claude / codex)の resume で再開する
|
|
21
|
+
preview <種類>/<名前> [--case <ケース名>] [--surface running|completed] [--channel <#名前|ID>]
|
|
22
|
+
views/ のビューを push せずに手元のファイルで確かめる(slack:cases.ts のケースを Slack に送る)`;
|
|
20
23
|
const [command, ...rest] = process.argv.slice(2);
|
|
21
24
|
// init / typegen / validate は repo ルートを任意で受ける(省略時はカレント)
|
|
22
25
|
const root = rest[0] ?? process.cwd();
|
|
@@ -92,6 +95,15 @@ switch (command) {
|
|
|
92
95
|
}
|
|
93
96
|
break;
|
|
94
97
|
}
|
|
98
|
+
case "preview": {
|
|
99
|
+
try {
|
|
100
|
+
process.exit(await runPreview(rest));
|
|
101
|
+
}
|
|
102
|
+
catch (e) {
|
|
103
|
+
fail(e instanceof Error ? e.message : String(e));
|
|
104
|
+
}
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
95
107
|
default: {
|
|
96
108
|
console.error(USAGE);
|
|
97
109
|
process.exit(command === undefined || command === "--help" ? 0 : 1);
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { JsonSchema, NodeKind } from "./_schemas/node.js";
|
|
2
|
+
export type SlackBlocks = unknown[];
|
|
3
|
+
export type FileRef = {
|
|
4
|
+
path: string;
|
|
5
|
+
name: string;
|
|
6
|
+
mime_type?: string;
|
|
7
|
+
size?: number;
|
|
8
|
+
};
|
|
9
|
+
export type { JsonSchema, NodeKind };
|
|
10
|
+
export type SlackViewMaterial<TIn = unknown, TOut = unknown> = {
|
|
11
|
+
input: TIn;
|
|
12
|
+
output?: TOut;
|
|
13
|
+
};
|
|
14
|
+
export type SlackViewContext<TIn = unknown> = {
|
|
15
|
+
kind: NodeKind;
|
|
16
|
+
input: TIn;
|
|
17
|
+
inputSchema: JsonSchema;
|
|
18
|
+
refs?: unknown;
|
|
19
|
+
additionalOutputs?: {
|
|
20
|
+
key: string;
|
|
21
|
+
name: string;
|
|
22
|
+
schema: JsonSchema;
|
|
23
|
+
}[];
|
|
24
|
+
nodeName: string;
|
|
25
|
+
stepUrl: string;
|
|
26
|
+
fileUrls: Record<string, string>;
|
|
27
|
+
};
|
|
28
|
+
export type SlackCompletedContext<TIn = unknown, TOut = unknown> = {
|
|
29
|
+
input: TIn;
|
|
30
|
+
output: TOut;
|
|
31
|
+
inputSchema: JsonSchema;
|
|
32
|
+
outputSchema: JsonSchema;
|
|
33
|
+
refs?: unknown;
|
|
34
|
+
nodeName: string;
|
|
35
|
+
runUrl: string;
|
|
36
|
+
fileUrls: Record<string, string>;
|
|
37
|
+
};
|
|
38
|
+
export type SlackView<TIn = unknown, TOut = unknown> = {
|
|
39
|
+
files?: (m: SlackViewMaterial<TIn, TOut>) => FileRef[];
|
|
40
|
+
renderRunning?: (ctx: SlackViewContext<TIn>) => SlackBlocks;
|
|
41
|
+
renderCompleted?: (ctx: SlackCompletedContext<TIn, TOut>) => SlackBlocks;
|
|
42
|
+
};
|
|
43
|
+
export type SlackViewCase = {
|
|
44
|
+
name: string;
|
|
45
|
+
kind?: NodeKind;
|
|
46
|
+
nodeName?: string;
|
|
47
|
+
input: unknown;
|
|
48
|
+
inputSchema?: JsonSchema;
|
|
49
|
+
output?: unknown;
|
|
50
|
+
outputSchema?: JsonSchema;
|
|
51
|
+
refs?: unknown;
|
|
52
|
+
};
|
|
53
|
+
export declare const decisionActionId: (portKey: string) => string;
|
|
54
|
+
export declare const decisionFormActionId: (portKey: string) => string;
|
|
55
|
+
export declare const parseDecisionActionId: (actionId: string | undefined) => {
|
|
56
|
+
port_key: string;
|
|
57
|
+
form: boolean;
|
|
58
|
+
} | null;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// ---- 決定ボタンのプロトコル ----
|
|
2
|
+
// ビューはボタンの action_id だけで「どの出口ポートへ流すか」を宣言する
|
|
3
|
+
// (step の特定は message ts の逆引き。value は使わない):
|
|
4
|
+
// decision:<port_key> … そのまま確定。ホストが { [port_key]: step.input } で確定する
|
|
5
|
+
// decision-form:<port_key> … そのノードの out ポート <port_key> のスキーマから生成した
|
|
6
|
+
// モーダルで値を集め、{ [port_key]: 入力値 } で確定する
|
|
7
|
+
// human の出口が approve / reject 以外に増えても、ビューがボタンを置くだけで流せる。
|
|
8
|
+
// 既存の "approve"(標準ビュー・投稿済みメッセージ)は従来の承認経路のまま動く
|
|
9
|
+
const DECISION_PREFIX = "decision:";
|
|
10
|
+
const DECISION_FORM_PREFIX = "decision-form:";
|
|
11
|
+
export const decisionActionId = (portKey) => `${DECISION_PREFIX}${portKey}`;
|
|
12
|
+
export const decisionFormActionId = (portKey) => `${DECISION_FORM_PREFIX}${portKey}`;
|
|
13
|
+
export const parseDecisionActionId = (actionId) => {
|
|
14
|
+
if (actionId?.startsWith(DECISION_FORM_PREFIX)) {
|
|
15
|
+
return { port_key: actionId.slice(DECISION_FORM_PREFIX.length), form: true };
|
|
16
|
+
}
|
|
17
|
+
if (actionId?.startsWith(DECISION_PREFIX)) {
|
|
18
|
+
return { port_key: actionId.slice(DECISION_PREFIX.length), form: false };
|
|
19
|
+
}
|
|
20
|
+
return null;
|
|
21
|
+
};
|
package/docs/development.md
CHANGED
|
@@ -20,6 +20,8 @@ nodes/
|
|
|
20
20
|
ai/<dir>/ # AI ノード定義:config.json
|
|
21
21
|
hook/<dir>/ # NodeIOHook:config.json + main.ts(ノードに付く観測フック)
|
|
22
22
|
skills/<dir>/ # AI ノードが参照する skill(SKILL.md)
|
|
23
|
+
views/
|
|
24
|
+
slack/<name>/ # Slack ビュー:index.ts(+手元プレビュー用の cases.ts)
|
|
23
25
|
```
|
|
24
26
|
|
|
25
27
|
## Program ノード(`nodes/program/<dir>/`)
|
|
@@ -70,6 +72,31 @@ skills/<dir>/ # AI ノードが参照する skill(SKILL.md)
|
|
|
70
72
|
- ノードの入出力・外部シグナルを観測する独立実行。正は `_schemas/hook-manifest.ts`
|
|
71
73
|
(`on`: input / output / signal の宣言・`env`)
|
|
72
74
|
|
|
75
|
+
## Slack ビュー(`views/slack/<name>/`)
|
|
76
|
+
|
|
77
|
+
- ノードの Slack メッセージの本文を描く。**実行中**(全 kind。human は承認待ち)と**完了**の2面。
|
|
78
|
+
ノードの「Slack ビュー」に `<テナントの slug>/<name>` を選ぶと使われる(エディタの選択肢に
|
|
79
|
+
ディレクトリ名で出る)。タイトル行・追加出口のボタン・投稿は mawaru が描く
|
|
80
|
+
- `index.ts` — `export default { files?, renderRunning?, renderCompleted? } satisfies SlackView<入力の型>`。
|
|
81
|
+
契約(型・ctx の中身・決定ボタンの `decisionActionId` / `decisionFormActionId`)の正は
|
|
82
|
+
`@mawaru/sdk/slack-view`。実装した関数の面だけが変わる
|
|
83
|
+
- **純関数**にする:mawaru が隔離環境(QuickJS)で動かすので、`fetch` / `process` / `require` / タイマー /
|
|
84
|
+
`console` は無い。1回の呼び出しは 200ms・32MB まで。ctx も戻り値も JSON で受け渡す
|
|
85
|
+
- import できるのは同じディレクトリ内の相対パスと `@mawaru/sdk/slack-view` だけ(npm パッケージは不可)。
|
|
86
|
+
ビルドしない(mawaru が TypeScript の型を外して読む。`enum` など型を外すだけで JS にならない書き方は不可)
|
|
87
|
+
- 入力は崩れて届くことがある(`input` が `null` 等)。例外を投げない書き方にする。失敗すると、承認待ちは
|
|
88
|
+
承認ボタンだけ・完了は1行のフォールバックになる
|
|
89
|
+
- 版:ループの**公開版は publish 時のデフォルトブランチの commit に固定**される。push しただけでは本番の
|
|
90
|
+
通知は変わらず、ループを publish し直すと次の run から変わる。テスト実行は push だけで変わる
|
|
91
|
+
- `cases.ts` — 手元プレビューのケース(`SlackViewCase[]` を `export default`)。本番の描画では読まない
|
|
92
|
+
- 手元プレビュー:`npx mawaru login` のあと、repo ルートで `npx mawaru preview slack/<name>`。push せずに
|
|
93
|
+
手元のファイルのまま、`cases.ts` の全ケースを実行中・完了の両方の面で実際の Slack に送る(mawaru API が本番と
|
|
94
|
+
同じ組み立てで描く。1回の実行ごとに見出しを1つ立て、全部をそのスレッドに入れる)
|
|
95
|
+
- `--case <ケース名>`・`--surface running|completed` で絞る。送り先は `--channel`(`#名前` かチャンネル ID)か
|
|
96
|
+
`.env` の `MAWARU_PREVIEW_CHANNEL`。`.env` の `MAWARU_TENANT_ID` も要る。送れるのはテナントの admin だけ
|
|
97
|
+
- 端末に1通ごとの結果が出る(`✔` 成功・`⚠` 描画に失敗して理由つき・`–` 未実装の面)。失敗が1つでもあれば exit 1。
|
|
98
|
+
ビューを直したら同じコマンドで送り直して確かめる
|
|
99
|
+
|
|
73
100
|
## loop(回路)の作成・更新
|
|
74
101
|
|
|
75
102
|
- `.claude/skills/create-loop` スキルに従う(graph API の契約は `_schemas/graph.ts` が正)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mawaru/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.37.0",
|
|
4
4
|
"description": "mawaru 実行 repo の開発 SDK。契約スキーマ(config.json 規約・ループ graph API)の正 + typegen / validate CLI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -10,6 +10,10 @@
|
|
|
10
10
|
".": {
|
|
11
11
|
"types": "./dist/index.d.ts",
|
|
12
12
|
"default": "./dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"./slack-view": {
|
|
15
|
+
"types": "./dist/slack-view.d.ts",
|
|
16
|
+
"default": "./dist/slack-view.js"
|
|
13
17
|
}
|
|
14
18
|
},
|
|
15
19
|
"files": [
|
|
@@ -18,7 +22,8 @@
|
|
|
18
22
|
"templates",
|
|
19
23
|
"docs",
|
|
20
24
|
"_schemas",
|
|
21
|
-
"index.ts"
|
|
25
|
+
"index.ts",
|
|
26
|
+
"slack-view.ts"
|
|
22
27
|
],
|
|
23
28
|
"engines": {
|
|
24
29
|
"node": ">=20"
|
|
@@ -77,6 +77,12 @@ repo との矛盾・使用中ノードの削除は 409。**graph PUT は冪等
|
|
|
77
77
|
1つの出口から複数の線を引くと枝が並行に走る(fan-out)。枝の結果を待ち合わせるときは
|
|
78
78
|
合流(`join`)を置く:線のつながった入口が全部揃うまで待ち、`{ [入口の key]: 値 }` にまとめて
|
|
79
79
|
`main` から流す。並行した枝を合流させずに同じノード(待機など)へ戻すと、枝の数だけ走り続ける。
|
|
80
|
+
配列の要素ごとに同じ手順や別ループを回すときは繰り返し(for)にする:`POST loops` に
|
|
81
|
+
`kind: "for"` と `owner_loop_id`(このループ)を渡して中身の loop を作り、その graph
|
|
82
|
+
(start → 1件分の処理 → end)を保存してから、このループに component ノード
|
|
83
|
+
(`component.component_loop_id`=作った for)を置く。配置ノードの in / out は中身の型の配列に
|
|
84
|
+
自動で焼き込まれ、全件そろうと入力と同じ順の配列で `main` から流れる(1件ずつは同時に走る)。
|
|
85
|
+
中身の出口(end につないだ in)は1つにする。
|
|
80
86
|
2. **repo 側の準備**:足りない program / ai は `nodes/program/<dir>/` / `nodes/ai/<dir>/` に作る
|
|
81
87
|
(config.json → `npx mawaru typegen` → main.ts 実装 → `npx mawaru validate`)。
|
|
82
88
|
**AI ノードの決定論的な処理は prompt ではなく `setup.sh` に書く**(下記)。
|
package/slack-view.ts
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import type { JsonSchema, NodeKind } from "./_schemas/node.js"
|
|
2
|
+
|
|
3
|
+
// Slack ビューの契約(`@mawaru/sdk/slack-view`)。ノードの Slack ビュー
|
|
4
|
+
// (nodes.slack_view_key)が実装する関数と、ホスト(mawaru)が渡す ctx の形の正。
|
|
5
|
+
// mawaru 内蔵のビュー(backend の _helpers/slackViews/)と、顧客の実行 repo の
|
|
6
|
+
// views/slack/<name>/ のビューが同じ契約で書く
|
|
7
|
+
// (docs/tasks/wip/Slackビューの顧客repo配置.md)。
|
|
8
|
+
//
|
|
9
|
+
// ビューは局面ごとの描画関数を**実装した分だけ**カスタマイズが効く任意のビュー:
|
|
10
|
+
// - renderRunning … 全 kind の実行中メッセージ(タイトル context 行より下の全ブロック)。
|
|
11
|
+
// human では承認待ちのメッセージになる。未実装なら human は承認ボタンだけの
|
|
12
|
+
// フォールバック、human 以外は従来のタイトル1行のまま
|
|
13
|
+
// - renderCompleted … 全 kind の完了メッセージ(✅ タイトル行より下の全ブロック)。
|
|
14
|
+
// 未実装なら従来の1行畳みのまま
|
|
15
|
+
// スレッドの解決・担当メンション・投稿・ts の保存・承認の認可はホストに残る。
|
|
16
|
+
//
|
|
17
|
+
// ビューは**純関数**:Slack API も DB も触らない(通知1本のために I/O するビューが
|
|
18
|
+
// 生まれると、ベストエフォート通知の失敗経路が読めなくなる)。必要な値は
|
|
19
|
+
// ホストが解決して ctx に載せる。ctx と戻り値は JSON で表現できる値だけにする
|
|
20
|
+
// (顧客 repo のビューは隔離環境で動き、値は JSON で受け渡す)。
|
|
21
|
+
//
|
|
22
|
+
// このファイルは実行時の import を持たない(型の import だけ):顧客 repo のビューは
|
|
23
|
+
// 隔離環境の中でこのファイルをそのまま import する
|
|
24
|
+
|
|
25
|
+
// Slack の Block Kit の blocks。形はビューの自由で、ホストは上限(50個)だけを守る
|
|
26
|
+
export type SlackBlocks = unknown[]
|
|
27
|
+
|
|
28
|
+
// ノード間を流れる file の値(storage path の参照)。署名 URL はホストが ctx.fileUrls で渡す
|
|
29
|
+
export type FileRef = {
|
|
30
|
+
path: string
|
|
31
|
+
name: string
|
|
32
|
+
mime_type?: string
|
|
33
|
+
size?: number
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export type { JsonSchema, NodeKind }
|
|
37
|
+
|
|
38
|
+
// ビューが描く材料:input=step の入力、output=step の出力(選ばれた出口ポートの値。
|
|
39
|
+
// 実行中の時点では undefined)。files の宣言はこの material から行う
|
|
40
|
+
export type SlackViewMaterial<TIn = unknown, TOut = unknown> = {
|
|
41
|
+
input: TIn
|
|
42
|
+
output?: TOut
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// 実行中メッセージの ctx(全 kind 共通)。TIn は in ポートの型
|
|
46
|
+
// (human なら承認ビューの宣言が転写されたポートの型)
|
|
47
|
+
export type SlackViewContext<TIn = unknown> = {
|
|
48
|
+
// ノードの kind。決定ボタン(承認・差し戻し)は human のときだけ意味を持つので、
|
|
49
|
+
// 複数の kind で使うビューはこれを見てボタンを描くか決める
|
|
50
|
+
kind: NodeKind
|
|
51
|
+
// ノードの in に流れてきた入力そのもの(ホストは加工しない)。
|
|
52
|
+
// human は [承認する] で確定するのもこの値なので、表示と承認内容は必ず一致する
|
|
53
|
+
input: TIn
|
|
54
|
+
// input の型(in ポートの schema そのもの)。未宣言のポートは {}。
|
|
55
|
+
// 標準ビューはこれで列と項目名を決める(宣言が無ければ値の実形から導出する)
|
|
56
|
+
inputSchema: JsonSchema
|
|
57
|
+
// run の袋(ループ変数。steps.refs=この step 開始時点のスナップショット)。
|
|
58
|
+
// 予約キー input が run の起動入力。省略=無し(古い run・プレビュー)
|
|
59
|
+
refs?: unknown
|
|
60
|
+
// ホストが接続済みの追加出口だけを渡す(approve / reject はビュー側が描く)。
|
|
61
|
+
// human 以外は出口がボタンにならないので常に無い
|
|
62
|
+
additionalOutputs?: { key: string; name: string; schema: JsonSchema }[]
|
|
63
|
+
nodeName: string
|
|
64
|
+
// step 画面(human なら承認画面)への URL。[修正する] / [確認する] のリンク先
|
|
65
|
+
stepUrl: string
|
|
66
|
+
// ビューが files で宣言したファイルの閲覧用署名 URL(path → URL)。
|
|
67
|
+
// ホストがベストエフォートで解決する:失敗した・tenant 外のパスは載らないので、
|
|
68
|
+
// ビューは「無いかもしれない」前提で描く(image ブロック等に使う)
|
|
69
|
+
fileUrls: Record<string, string>
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// 完了メッセージの ctx。output は完了した step の出力そのもの(ホストは加工しない)
|
|
73
|
+
export type SlackCompletedContext<TIn = unknown, TOut = unknown> = {
|
|
74
|
+
input: TIn
|
|
75
|
+
output: TOut
|
|
76
|
+
// in ポートの schema と、選ばれた出口ポート(step.output_port_key)の schema。
|
|
77
|
+
// 未宣言のポートは {}
|
|
78
|
+
inputSchema: JsonSchema
|
|
79
|
+
outputSchema: JsonSchema
|
|
80
|
+
refs?: unknown
|
|
81
|
+
nodeName: string
|
|
82
|
+
// run 詳細画面への URL(完了メッセージからの誘導先)
|
|
83
|
+
runUrl: string
|
|
84
|
+
fileUrls: Record<string, string>
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export type SlackView<TIn = unknown, TOut = unknown> = {
|
|
88
|
+
// ビューが画像等で使いたい material 内のファイル参照(純関数のまま宣言だけする)。
|
|
89
|
+
// 署名 URL の発行という I/O はホストの仕事で、結果は ctx.fileUrls で受け取る
|
|
90
|
+
files?: (m: SlackViewMaterial<TIn, TOut>) => FileRef[]
|
|
91
|
+
// 実行中(全 kind。human は承認待ち)。タイトル context 行より下の全ブロック
|
|
92
|
+
renderRunning?: (ctx: SlackViewContext<TIn>) => SlackBlocks
|
|
93
|
+
// 完了(全 kind)。✅ タイトル context 行より下の全ブロック
|
|
94
|
+
renderCompleted?: (ctx: SlackCompletedContext<TIn, TOut>) => SlackBlocks
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// 手元プレビュー(npx mawaru preview)のケース。顧客 repo の
|
|
98
|
+
// views/slack/<name>/cases.ts が export default する。本番の描画では読まない
|
|
99
|
+
export type SlackViewCase = {
|
|
100
|
+
name: string
|
|
101
|
+
// ノードの kind。省略時は human(承認待ち)
|
|
102
|
+
kind?: NodeKind
|
|
103
|
+
// タイトル行のノード名。省略時は name
|
|
104
|
+
nodeName?: string
|
|
105
|
+
// ctx.input
|
|
106
|
+
input: unknown
|
|
107
|
+
// ctx.inputSchema。省略=未宣言({})
|
|
108
|
+
inputSchema?: JsonSchema
|
|
109
|
+
// 完了の ctx.output。省略時は input(approve が素通しのビューが大半)
|
|
110
|
+
output?: unknown
|
|
111
|
+
outputSchema?: JsonSchema
|
|
112
|
+
refs?: unknown
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ---- 決定ボタンのプロトコル ----
|
|
116
|
+
// ビューはボタンの action_id だけで「どの出口ポートへ流すか」を宣言する
|
|
117
|
+
// (step の特定は message ts の逆引き。value は使わない):
|
|
118
|
+
// decision:<port_key> … そのまま確定。ホストが { [port_key]: step.input } で確定する
|
|
119
|
+
// decision-form:<port_key> … そのノードの out ポート <port_key> のスキーマから生成した
|
|
120
|
+
// モーダルで値を集め、{ [port_key]: 入力値 } で確定する
|
|
121
|
+
// human の出口が approve / reject 以外に増えても、ビューがボタンを置くだけで流せる。
|
|
122
|
+
// 既存の "approve"(標準ビュー・投稿済みメッセージ)は従来の承認経路のまま動く
|
|
123
|
+
|
|
124
|
+
const DECISION_PREFIX = "decision:"
|
|
125
|
+
const DECISION_FORM_PREFIX = "decision-form:"
|
|
126
|
+
|
|
127
|
+
export const decisionActionId = (portKey: string): string =>
|
|
128
|
+
`${DECISION_PREFIX}${portKey}`
|
|
129
|
+
|
|
130
|
+
export const decisionFormActionId = (portKey: string): string =>
|
|
131
|
+
`${DECISION_FORM_PREFIX}${portKey}`
|
|
132
|
+
|
|
133
|
+
export const parseDecisionActionId = (
|
|
134
|
+
actionId: string | undefined,
|
|
135
|
+
): { port_key: string; form: boolean } | null => {
|
|
136
|
+
if (actionId?.startsWith(DECISION_FORM_PREFIX)) {
|
|
137
|
+
return { port_key: actionId.slice(DECISION_FORM_PREFIX.length), form: true }
|
|
138
|
+
}
|
|
139
|
+
if (actionId?.startsWith(DECISION_PREFIX)) {
|
|
140
|
+
return { port_key: actionId.slice(DECISION_PREFIX.length), form: false }
|
|
141
|
+
}
|
|
142
|
+
return null
|
|
143
|
+
}
|
package/templates/CLAUDE.md
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
- `nodes/program/<dir>/` — Program ノード(`config.json` + `main.ts`)。`main.ts` は
|
|
10
10
|
`run(input, ctx)` を export し、戻り値は `{ <出口ポートのkey>: <データ> }` の単一キーのオブジェクト
|
|
11
11
|
- `nodes/ai/<dir>/` — AI ノード定義(`config.json`。prompt / model / skills / 入出力スキーマ / env)
|
|
12
|
+
- `views/slack/<name>/` — Slack ビュー(実行中・承認待ち・完了メッセージの本文)。`index.ts` + 手元プレビュー用の `cases.ts`
|
|
12
13
|
- `.github/workflows/mawaru-runner.yml` — mawaru 所有の実行シム。**編集しない**
|
|
13
14
|
(`npx @mawaru/sdk init` が再生成する)
|
|
14
15
|
|
|
@@ -21,3 +22,5 @@ loop / node 開発にとりかかる前に、必ず `node_modules/@mawaru/sdk/do
|
|
|
21
22
|
- 型生成:`npx mawaru typegen`(config.json → `types.d.ts`)
|
|
22
23
|
- 規約検証:`npx mawaru validate`(CI 向け。error で exit 1)
|
|
23
24
|
- ループの作成・更新:`.claude/skills/create-loop` スキルに従う
|
|
25
|
+
- Slack ビューの確認:`npx mawaru preview slack/<name>`(push せずに手元のファイルで `cases.ts` の全ケースを Slack に送る。
|
|
26
|
+
失敗は理由が端末に出て exit 1。先に `npx mawaru login`、送り先は `.env` の `MAWARU_PREVIEW_CHANNEL`)
|