@dev.sail.money/sailor 1.3.0-227 → 1.3.0-228

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.
Files changed (47) hide show
  1. package/AGENTS.md +1 -1
  2. package/README.md +59 -498
  3. package/package.json +1 -5
  4. package/packages/cli/dist/index.cjs +0 -14
  5. package/packages/sdk/dist/fees.d.ts +2 -1
  6. package/packages/sdk/dist/fees.d.ts.map +1 -1
  7. package/packages/sdk/dist/fees.js +2 -1
  8. package/packages/sdk/dist/fees.js.map +1 -1
  9. package/packages/sdk/dist/intelligence.d.ts +1 -1
  10. package/packages/sdk/dist/intelligence.js +1 -1
  11. package/packages/sdk/package.json +2 -2
  12. package/templates/default/.agents/skills/sail-mandates/references/examples-index.md +1 -1
  13. package/templates/default/.agents/skills/sail-template-swap/SKILL.md +2 -2
  14. package/templates/default/.agents/skills/sail-template-swap-no-oracle/SKILL.md +2 -2
  15. package/templates/default/.agents/skills/sail-templates/SKILL.md +2 -2
  16. package/templates/default/docs/PERMISSION_MODEL.md +2 -2
  17. package/docs/PERMISSION_MODEL.md +0 -93
  18. package/examples/README.md +0 -24
  19. package/examples/lifi-permissions/LifiBoundedApprovePermissionCloneable.sol +0 -84
  20. package/examples/lifi-permissions/LifiDiamondSwapPermissionCloneable.sol +0 -97
  21. package/examples/lifi-permissions/README.md +0 -53
  22. package/scripts/check-docs.mjs +0 -309
  23. package/scripts/check-init.mjs +0 -123
  24. package/scripts/check-update.mjs +0 -177
  25. package/scripts/clean.mjs +0 -17
  26. /package/{examples → templates/default/examples}/custom-mandate/.sail/contracts/interfaces/IPermission.sol +0 -0
  27. /package/{examples → templates/default/examples}/custom-mandate/README.md +0 -0
  28. /package/{examples → templates/default/examples}/custom-mandate/foundry.toml +0 -0
  29. /package/{examples → templates/default/examples}/custom-mandate/mandates/BoundedCallPermission.sol +0 -0
  30. /package/{examples → templates/default/examples}/custom-mandate/mandates/README.md +0 -0
  31. /package/{examples → templates/default/examples}/custom-mandate/mandates/SailCalldata.sol +0 -0
  32. /package/{examples → templates/default/examples}/permissions/BoundedApproveAndCallBatch.sol +0 -0
  33. /package/{examples → templates/default/examples}/permissions/BoundedBorrow_AaveV3_Arbitrum.sol +0 -0
  34. /package/{examples → templates/default/examples}/permissions/BoundedLimitless_Base.sol +0 -0
  35. /package/{examples → templates/default/examples}/permissions/BoundedPerp_GMXv2_Arbitrum.sol +0 -0
  36. /package/{examples → templates/default/examples}/permissions/BoundedStake_Venice_Base.sol +0 -0
  37. /package/{examples → templates/default/examples}/permissions/BoundedSupply_AaveV3_Arbitrum.sol +0 -0
  38. /package/{examples → templates/default/examples}/permissions/BoundedSwapNative_UniswapV3_Base.sol +0 -0
  39. /package/{examples → templates/default/examples}/permissions/BoundedSwap_UniswapV3_Base.sol +0 -0
  40. /package/{examples → templates/default/examples}/permissions/BoundedSwap_UniswapV4_Unichain.sol +0 -0
  41. /package/{examples → templates/default/examples}/permissions/BoundedTransfer_ERC20_Ethereum.sol +0 -0
  42. /package/{examples → templates/default/examples}/permissions/BoundedVault_ERC4626_Base.sol +0 -0
  43. /package/{examples → templates/default/examples}/permissions/README.md +0 -0
  44. /package/{examples → templates/default/examples}/permissions/SailCalldata.sol +0 -0
  45. /package/{examples → templates/default/examples}/permissions/foundry.toml +0 -0
  46. /package/{examples → templates/default/examples}/permissions/interfaces/IBatchPermission.sol +0 -0
  47. /package/{examples → templates/default/examples}/permissions/interfaces/IPermission.sol +0 -0
@@ -1,309 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Doc-drift gate.
4
- *
5
- * The agent-facing docs (CLAUDE.md, AGENTS.md, AGENT_PLAYBOOK.md, the scaffolded
6
- * template, package READMEs) tell an LLM agent which `sailor` commands and which
7
- * `client.<ns>.<method>(...)` SDK calls to use. If a doc names a command or method
8
- * that no longer exists, the agent confidently does the wrong thing. This script
9
- * is the regression net: it derives the *real* surface from source and fails if a
10
- * doc references something that isn't there.
11
- *
12
- * - CLI commands ← parsed from packages/cli/src/index.ts (commander tree)
13
- * - SDK methods ← parsed from packages/sdk/src/client.ts (namespace classes)
14
- *
15
- * Only `sailor …` is validated, never the SailFramework `sail …` binary that some
16
- * docs reference. Only references inside `inline code` or ```fenced``` blocks are
17
- * checked, so prose ("the sailor CLI") never trips it.
18
- *
19
- * Run: `node scripts/check-docs.mjs` (or `pnpm docs:check`). Exit 1 on any miss.
20
- */
21
-
22
- import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
23
- import { dirname, join, relative } from "node:path";
24
- import { fileURLToPath } from "node:url";
25
-
26
- const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
27
- const rel = (p) => relative(ROOT, p);
28
-
29
- // ── 1. Derive the real CLI command surface ──────────────────────────────────
30
-
31
- /**
32
- * Parse the commander tree from index.ts into:
33
- * leaves — Set of full top-level invocations ("init", "doctor", "dispatch preview")
34
- * groups — Map<groupName, Set<subcommand>> (e.g. "mandate" → {prepare, sign, …})
35
- */
36
- function parseCliSurface() {
37
- const src = readFileSync(join(ROOT, "packages/cli/src/index.ts"), "utf-8");
38
- const leaves = new Set();
39
- const groups = new Map();
40
- const varToGroup = new Map(); // commander variable name → group command name
41
-
42
- // `const ui = program.command("ui")` — a group declared on a variable.
43
- for (const m of src.matchAll(/const\s+(\w+)\s*=\s*program\s*\.command\(\s*"([^"]+)"/g)) {
44
- const groupName = m[2].split(/\s+/)[0];
45
- varToGroup.set(m[1], groupName);
46
- if (!groups.has(groupName)) groups.set(groupName, new Set());
47
- }
48
-
49
- // `program.command("init [dir]")` — a top-level leaf (not assigned to a var).
50
- for (const m of src.matchAll(/program\s*\.command\(\s*"([^"]+)"/g)) {
51
- const name = m[1].split(/\s+/)[0];
52
- // Multi-word leaf names (e.g. the "dispatch preview" stub) keep their full form too.
53
- const full = m[1]
54
- .replace(/\s*\[.*$/, "")
55
- .replace(/\s*<.*$/, "")
56
- .trim();
57
- if (!groups.has(name)) {
58
- leaves.add(name);
59
- if (full.includes(" ")) leaves.add(full);
60
- }
61
- }
62
-
63
- // `stub("setup", …)` / `stub("dispatch preview", …)` — registered top-level commands.
64
- for (const m of src.matchAll(/\bstub\(\s*"([^"]+)"/g)) {
65
- leaves.add(m[1]);
66
- leaves.add(m[1].split(/\s+/)[0]);
67
- }
68
-
69
- // `ui.command("start")` / `mandate.command("prepare")` — subcommands of a group.
70
- for (const m of src.matchAll(/(\w+)\s*\.command\(\s*"([^"]+)"/g)) {
71
- const groupName = varToGroup.get(m[1]);
72
- if (!groupName) continue; // not a known group variable (e.g. program.command handled above)
73
- const sub = m[2].split(/\s+/)[0];
74
- groups.get(groupName).add(sub);
75
- }
76
-
77
- return { leaves, groups };
78
- }
79
-
80
- // ── 2. Derive the real SDK surface ───────────────────────────────────────────
81
-
82
- /**
83
- * Parse client.ts into:
84
- * namespaces — Map<propertyName, Set<methodName>> (account → {create, get, …})
85
- * clientMethods — Set of direct SailorClient methods (capabilities, withSigner, …)
86
- */
87
- function parseSdkSurface() {
88
- const src = readFileSync(join(ROOT, "packages/sdk/src/client.ts"), "utf-8");
89
- const lines = src.split("\n");
90
-
91
- // Map namespace *class* → set of method names, by slicing each class body.
92
- const classMethods = new Map();
93
- let current = null;
94
- let depth = 0;
95
- const reserved = new Set([
96
- "if",
97
- "for",
98
- "while",
99
- "switch",
100
- "catch",
101
- "return",
102
- "super",
103
- "constructor",
104
- "function",
105
- "await",
106
- "typeof",
107
- "new",
108
- ]);
109
- for (const line of lines) {
110
- const classMatch = line.match(/^\s*(?:export\s+)?(?:abstract\s+)?class\s+(\w+)/);
111
- if (classMatch) {
112
- current = classMatch[1];
113
- classMethods.set(current, new Set());
114
- depth = 0;
115
- }
116
- if (current) {
117
- // Method declarations live at class-body depth 1.
118
- const methodMatch = line.match(
119
- /^\s{2}(?:public\s+|private\s+|protected\s+)?(?:async\s+)?(?:get\s+)?(\w+)\s*[(<]/,
120
- );
121
- if (methodMatch && !reserved.has(methodMatch[1])) {
122
- classMethods.get(current).add(methodMatch[1]);
123
- }
124
- depth += (line.match(/{/g)?.length ?? 0) - (line.match(/}/g)?.length ?? 0);
125
- if (depth <= 0 && line.includes("}")) current = null;
126
- }
127
- }
128
-
129
- // Map runtime property name → class: `this.account = new AccountNamespace(`
130
- const namespaces = new Map();
131
- for (const m of src.matchAll(/this\.(\w+)\s*=\s*new\s+(\w+)\s*\(/g)) {
132
- const [, prop, cls] = m;
133
- if (classMethods.has(cls)) namespaces.set(prop, classMethods.get(cls));
134
- }
135
- // `this.dispatch = dispatch;` — assigned from a local built earlier in the ctor.
136
- for (const m of src.matchAll(/this\.(\w+)\s*=\s*(\w+);/g)) {
137
- const [, prop, localVar] = m;
138
- if (namespaces.has(prop)) continue;
139
- // Resolve the local: `const dispatch = new DispatchNamespace(`
140
- const localDecl = new RegExp(`(?:const|let)\\s+${localVar}\\s*=\\s*new\\s+(\\w+)\\s*\\(`);
141
- const lm = src.match(localDecl);
142
- if (lm && classMethods.has(lm[1])) namespaces.set(prop, classMethods.get(lm[1]));
143
- }
144
-
145
- // Direct methods on the SailorClient class itself.
146
- const clientMethods = classMethods.get("SailorClient") ?? new Set();
147
-
148
- return { namespaces, clientMethods };
149
- }
150
-
151
- // ── 3. Collect doc files + extract code references ───────────────────────────
152
-
153
- const DOC_GLOBS = [
154
- "CLAUDE.md",
155
- "AGENTS.md",
156
- "AGENT_PLAYBOOK.md",
157
- "README.md",
158
- "docs",
159
- "templates",
160
- "packages/cli/README.md",
161
- "packages/sdk/README.md",
162
- ];
163
-
164
- function collectDocs() {
165
- const files = [];
166
- const walk = (p) => {
167
- const st = statSync(p, { throwIfNoEntry: false });
168
- if (!st) return;
169
- if (st.isDirectory()) {
170
- for (const e of readdirSync(p)) {
171
- if (e === "node_modules" || e === "dist" || e.startsWith(".git")) continue;
172
- walk(join(p, e));
173
- }
174
- } else if (p.endsWith(".md")) {
175
- files.push(p);
176
- }
177
- };
178
- for (const g of DOC_GLOBS) walk(join(ROOT, g));
179
- return [...new Set(files)];
180
- }
181
-
182
- /** Pull the text of every `inline` and ```fenced``` code region from markdown. */
183
- function codeRegions(md) {
184
- const regions = [];
185
- // Fenced blocks first, then strip them so inline matching doesn't double-count.
186
- const rest = md.replace(/```[\s\S]*?```/g, (block) => {
187
- regions.push(block.replace(/```/g, ""));
188
- return "\n";
189
- });
190
- for (const m of rest.matchAll(/`([^`\n]+)`/g)) regions.push(m[1]);
191
- return regions;
192
- }
193
-
194
- // ── 4. Skills consistency ─────────────────────────────────────────────────────
195
- //
196
- // The scaffolded template ships agent skills under .agents/skills/. AGENTS.md is
197
- // the routing layer: it must point at every skill that exists, and every skill it
198
- // points at must exist with valid frontmatter — otherwise an agent either never
199
- // discovers a workflow or follows a dangling pointer.
200
-
201
- function checkSkills(errors) {
202
- const skillsRoot = join(ROOT, "templates/default/.agents/skills");
203
- const agentsPath = join(ROOT, "templates/default/AGENTS.md");
204
- if (!existsSync(skillsRoot)) {
205
- errors.push("templates/default/.agents/skills: directory missing");
206
- return;
207
- }
208
- const agentsMd = readFileSync(agentsPath, "utf-8");
209
- const dirs = readdirSync(skillsRoot).filter((d) =>
210
- statSync(join(skillsRoot, d)).isDirectory(),
211
- );
212
- if (dirs.length === 0) errors.push("templates/default/.agents/skills: no skills found");
213
-
214
- for (const d of dirs) {
215
- const skillFile = join(skillsRoot, d, "SKILL.md");
216
- if (!existsSync(skillFile)) {
217
- errors.push(`templates/default/.agents/skills/${d}: missing SKILL.md`);
218
- continue;
219
- }
220
- const fm = readFileSync(skillFile, "utf-8").match(/^---\r?\n([\s\S]*?)\r?\n---/);
221
- if (!fm) {
222
- errors.push(`${rel(skillFile)}: missing YAML frontmatter`);
223
- continue;
224
- }
225
- const name = fm[1].match(/^name:\s*(\S+)\s*$/m)?.[1];
226
- const description = fm[1].match(/^description:\s*(.+)$/m)?.[1]?.trim();
227
- if (name !== d) errors.push(`${rel(skillFile)}: frontmatter name "${name}" ≠ directory "${d}"`);
228
- if (!description) errors.push(`${rel(skillFile)}: frontmatter description missing or empty`);
229
- if (!agentsMd.includes(`.agents/skills/${d}/SKILL.md`)) {
230
- errors.push(`templates/default/AGENTS.md: routing table does not reference .agents/skills/${d}/SKILL.md`);
231
- }
232
- }
233
-
234
- for (const m of agentsMd.matchAll(/\.agents\/skills\/([\w-]+)\/SKILL\.md/g)) {
235
- if (!dirs.includes(m[1])) {
236
- errors.push(`templates/default/AGENTS.md: references .agents/skills/${m[1]}/SKILL.md which does not exist`);
237
- }
238
- }
239
- }
240
-
241
- // ── 5. Validate ──────────────────────────────────────────────────────────────
242
-
243
- function main() {
244
- const cli = parseCliSurface();
245
- const sdk = parseSdkSurface();
246
- const docs = collectDocs();
247
- const errors = [];
248
-
249
- for (const file of docs) {
250
- const md = readFileSync(file, "utf-8");
251
- for (const code of codeRegions(md)) {
252
- for (const lineRaw of code.split("\n")) {
253
- const line = lineRaw.trim();
254
-
255
- // ── CLI: `sailor <word1> [<word2>]` ──────────────────────────────────
256
- for (const m of line.matchAll(/\bsailor\s+([a-z][\w-]*)(?:\s+([a-z][\w-]*))?/g)) {
257
- const [, w1, w2] = m;
258
- const two = w2 ? `${w1} ${w2}` : null;
259
- if (cli.leaves.has(w1) || (two && cli.leaves.has(two))) continue; // top-level leaf
260
- if (cli.groups.has(w1)) {
261
- // Group: if a subcommand word follows (and isn't a flag), it must exist.
262
- if (!w2 || w2.startsWith("-")) continue; // bare group invocation, or a flag
263
- if (cli.groups.get(w1).has(w2)) continue;
264
- errors.push(
265
- `${rel(file)}: \`sailor ${w1} ${w2}\` — "${w2}" is not a subcommand of "${w1}" (have: ${[...cli.groups.get(w1)].join(", ")})`,
266
- );
267
- continue;
268
- }
269
- errors.push(`${rel(file)}: \`sailor ${w1}\` — unknown command`);
270
- }
271
-
272
- // ── SDK: `client.<ns>[.<method>]` ────────────────────────────────────
273
- for (const m of line.matchAll(/\bclient\.(\w+)(?:\.(\w+))?/g)) {
274
- const [, ns, method] = m;
275
- if (sdk.namespaces.has(ns)) {
276
- if (!method) continue;
277
- if (sdk.namespaces.get(ns).has(method)) continue;
278
- errors.push(
279
- `${rel(file)}: \`client.${ns}.${method}\` — "${method}" is not a method of the "${ns}" namespace (have: ${[...sdk.namespaces.get(ns)].join(", ")})`,
280
- );
281
- continue;
282
- }
283
- if (sdk.clientMethods.has(ns)) continue; // direct client method, e.g. capabilities/withSigner
284
- errors.push(`${rel(file)}: \`client.${ns}\` — unknown SDK namespace or method`);
285
- }
286
- }
287
- }
288
- }
289
-
290
- checkSkills(errors);
291
-
292
- // ── Report ───────────────────────────────────────────────────────────────
293
- const cliCount = cli.leaves.size + [...cli.groups.values()].reduce((n, s) => n + s.size, 0);
294
- const sdkCount =
295
- [...sdk.namespaces.values()].reduce((n, s) => n + s.size, 0) + sdk.clientMethods.size;
296
- console.log(
297
- `Doc-drift check: ${docs.length} docs vs ${cliCount} CLI commands, ${sdkCount} SDK methods`,
298
- );
299
-
300
- if (errors.length > 0) {
301
- console.error(`\n✗ ${errors.length} stale reference(s):\n`);
302
- for (const e of [...new Set(errors)].sort()) console.error(` ${e}`);
303
- console.error("\nFix the doc, or update the CLI/SDK so the referenced surface exists.");
304
- process.exit(1);
305
- }
306
- console.log("✓ Every sailor command and client.* method referenced in docs exists.");
307
- }
308
-
309
- main();
@@ -1,123 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * `sailor init` smoke test.
4
- *
5
- * PASS 1 — fresh init: scaffolds a new project and asserts expected files exist.
6
- *
7
- * Template files are read live from disk (not bundled), so no rebuild is needed
8
- * between runs — the in-tree templates/default/ IS the "latest version".
9
- *
10
- * Run: node scripts/check-init.mjs (CI builds the CLI first)
11
- * Exit: 0 = all passes OK, 1 = failure (prints what went wrong).
12
- */
13
-
14
- import { execFileSync } from "node:child_process";
15
- import fs from "node:fs";
16
- import os from "node:os";
17
- import path from "node:path";
18
- import { fileURLToPath } from "node:url";
19
-
20
- const ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), "..");
21
- const BUNDLE = path.join(ROOT, "packages/cli/dist/index.cjs");
22
- const PROJECT = "smoke-agent";
23
-
24
- function fail(msg) {
25
- console.error(`✗ init smoke test FAILED: ${msg}`);
26
- process.exit(1);
27
- }
28
-
29
- if (!fs.existsSync(BUNDLE)) {
30
- fail(`CLI bundle not found at ${BUNDLE}.\n Build it first: pnpm --filter sailor build`);
31
- }
32
-
33
- // Scaffold into a temp dir. `init` requires the destination to live inside the
34
- // process cwd, so we run the bundle with cwd set to a fresh temp root.
35
- const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "sailor-init-smoke-"));
36
- const dest = path.join(tmpRoot, PROJECT);
37
-
38
- try {
39
- let stdout = "";
40
- try {
41
- stdout = execFileSync(process.execPath, [BUNDLE, "init", PROJECT], {
42
- cwd: tmpRoot,
43
- encoding: "utf-8",
44
- stdio: ["ignore", "pipe", "pipe"],
45
- });
46
- } catch (err) {
47
- const out = `${err.stdout ?? ""}${err.stderr ?? ""}`.trim();
48
- fail(`\`sailor init ${PROJECT}\` exited non-zero.\n ${out || err.message}`);
49
- }
50
-
51
- // A successful fresh init prints the AGENTS.md onboarding banner.
52
- if (!/AGENTS\.md/i.test(stdout)) {
53
- fail(`init did not report success.\n stdout: ${stdout.trim()}`);
54
- }
55
-
56
- // Assert the scaffold landed.
57
- const mustExist = [
58
- ".sail/config.json",
59
- "package.json",
60
- "foundry.toml",
61
- "mandates",
62
- "AGENTS.md",
63
- "CLAUDE.md",
64
- "Dockerfile",
65
- ".dockerignore",
66
- ".sail/contracts/interfaces/IPermission.sol",
67
- ".sail/contracts/interfaces/IBatchPermission.sol",
68
- "test/BoundedCallPermission.t.sol",
69
- "examples/custom-mandate/README.md",
70
- ".agents/skills/sail-onboarding/SKILL.md",
71
- ".agents/skills/sail-project-info/SKILL.md",
72
- ".agents/skills/sail-servers/SKILL.md",
73
- ".agents/skills/sail-transactions/SKILL.md",
74
- ".agents/skills/sail-mandates/SKILL.md",
75
- ".agents/skills/sail-mandates/references/approvals.md",
76
- ".agents/skills/sail-automation/SKILL.md",
77
- ".agents/skills/sail-automation/references/docker-vm.md",
78
- ".agents/skills/sail-automation/references/github-actions.md",
79
- ".agents/skills/sail-automation/references/local-daemon.md",
80
- ".agents/skills/sail-automation/references/self-hosted-runner.md",
81
- ".agents/skills/sail-extend/SKILL.md",
82
- ];
83
- for (const rel of mustExist) {
84
- if (!fs.existsSync(path.join(dest, rel))) fail(`expected scaffolded "${rel}" — not found`);
85
- }
86
-
87
- // config.json is valid JSON named after the project.
88
- const config = JSON.parse(fs.readFileSync(path.join(dest, ".sail/config.json"), "utf-8"));
89
- if (config.name !== PROJECT) fail(`config.json name is "${config.name}", expected "${PROJECT}"`);
90
-
91
- // package.json is valid, renamed, and the workspace protocol was resolved away
92
- // (a leftover "workspace:*" would make the scaffold un-installable for users).
93
- const pkg = JSON.parse(fs.readFileSync(path.join(dest, "package.json"), "utf-8"));
94
- if (pkg.name !== PROJECT) fail(`package.json name is "${pkg.name}", expected "${PROJECT}"`);
95
- if (pkg.dependencies?.["@sail/sdk"] === "workspace:*") {
96
- fail(`package.json still has "@sail/sdk": "workspace:*" — init did not resolve it`);
97
- }
98
-
99
- // ── Regression guard: absolute path outside cwd ───────────────────────────
100
- // An absolute path outside the cwd must be REJECTED, not silently nested into
101
- // `<cwd>/<abs path>`. (Pre-fix, `path.join` swallowed the leading slash and
102
- // scaffolded a bogus nested tree while printing success.)
103
- const outside = path.join(os.tmpdir(), "sailor-init-outside", "agent");
104
- let rejected = false;
105
- try {
106
- execFileSync(process.execPath, [BUNDLE, "init", outside], {
107
- cwd: tmpRoot,
108
- stdio: ["ignore", "pipe", "pipe"],
109
- });
110
- } catch {
111
- rejected = true; // non-zero exit = correctly refused
112
- }
113
- if (!rejected) fail(`an absolute path outside cwd ("${outside}") was accepted — should be rejected`);
114
- if (fs.existsSync(path.join(outside, ".sail/config.json"))) {
115
- fail(`init scaffolded into an out-of-cwd absolute path "${outside}"`);
116
- }
117
-
118
- console.log(`✓ init smoke test passed — scaffolded ${PROJECT}/ from the in-tree bundle`);
119
- console.log("✓ init guard passed — absolute path outside cwd rejected, not silently nested");
120
- } finally {
121
- fs.rmSync(path.join(os.tmpdir(), "sailor-init-outside"), { recursive: true, force: true });
122
- fs.rmSync(tmpRoot, { recursive: true, force: true });
123
- }
@@ -1,177 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * `sailor update` smoke tests.
4
- *
5
- * PASS 1 — standard update:
6
- * Deletes a template-owned file and a user skill, runs `sailor update`,
7
- * asserts the template file is restored and the user skill is untouched.
8
- *
9
- * PASS 2 — seeds missing user-space files:
10
- * Deletes a user-space file (package.json) and dir (src/), runs update,
11
- * asserts they are re-added. Asserts AGENTS.md / CLAUDE.md / Dockerfile
12
- * are NOT overwritten when they already exist.
13
- *
14
- * PASS 3 — stale path pruning:
15
- * Manually creates .agents/skills/sail-ci/ (orphan from old template version),
16
- * runs update, asserts it is deleted and sail-automation is present.
17
- *
18
- * PASS 4 — init-on-existing errors:
19
- * Runs `sailor init` inside the already-initialized project;
20
- * asserts it exits non-zero with an "already initialized" message.
21
- *
22
- * Run: node scripts/check-update.mjs (CI builds the CLI first)
23
- * Exit: 0 = all passes OK, 1 = failure (prints what went wrong).
24
- */
25
-
26
- import { execFileSync } from "node:child_process";
27
- import fs from "node:fs";
28
- import os from "node:os";
29
- import path from "node:path";
30
- import { fileURLToPath } from "node:url";
31
-
32
- const ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), "..");
33
- const BUNDLE = path.join(ROOT, "packages/cli/dist/index.cjs");
34
- const PROJECT = "smoke-update";
35
-
36
- function fail(msg) {
37
- console.error(`✗ update smoke test FAILED: ${msg}`);
38
- process.exit(1);
39
- }
40
-
41
- if (!fs.existsSync(BUNDLE)) {
42
- fail(`CLI bundle not found at ${BUNDLE}.\n Build it first: pnpm --filter sailor build`);
43
- }
44
-
45
- const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "sailor-update-smoke-"));
46
- const dest = path.join(tmpRoot, PROJECT);
47
-
48
- try {
49
- // Bootstrap: fresh init so we have a valid project to update.
50
- try {
51
- execFileSync(process.execPath, [BUNDLE, "init", PROJECT], {
52
- cwd: tmpRoot,
53
- encoding: "utf-8",
54
- stdio: ["ignore", "pipe", "pipe"],
55
- });
56
- } catch (err) {
57
- const out = `${err.stdout ?? ""}${err.stderr ?? ""}`.trim();
58
- fail(`bootstrap \`sailor init ${PROJECT}\` exited non-zero.\n ${out || err.message}`);
59
- }
60
-
61
- // ── PASS 1 — standard update ───────────────────────────────────────────────
62
- // Delete a template-owned file and add a user skill that must survive update.
63
- const templateOwned = path.join(dest, ".agents/skills/sail-automation/SKILL.md");
64
- const cursorRules = path.join(dest, ".cursor/rules");
65
- const userSkill = path.join(dest, ".agents/skills/my-custom-skill/SKILL.md");
66
-
67
- fs.rmSync(templateOwned);
68
- fs.rmSync(cursorRules);
69
- fs.mkdirSync(path.dirname(userSkill), { recursive: true });
70
- fs.writeFileSync(userSkill, "# custom skill\n", "utf-8");
71
-
72
- try {
73
- execFileSync(process.execPath, [BUNDLE, "update"], {
74
- cwd: dest,
75
- encoding: "utf-8",
76
- stdio: ["ignore", "pipe", "pipe"],
77
- });
78
- } catch (err) {
79
- const out = `${err.stdout ?? ""}${err.stderr ?? ""}`.trim();
80
- fail(`Pass 1: \`sailor update\` exited non-zero.\n ${out || err.message}`);
81
- }
82
-
83
- if (!fs.existsSync(templateOwned))
84
- fail(`Pass 1: update did not restore "${path.relative(dest, templateOwned)}"`);
85
- if (!fs.existsSync(cursorRules))
86
- fail(`Pass 1: update did not restore "${path.relative(dest, cursorRules)}"`);
87
- if (!fs.existsSync(userSkill))
88
- fail(`Pass 1: update removed user file "${path.relative(dest, userSkill)}" — must be preserved`);
89
-
90
- console.log("✓ Pass 1 passed — template files restored, user skill preserved");
91
-
92
- // ── PASS 2 — seeds missing user-space files ───────────────────────────────
93
- const agentsMd = path.join(dest, "AGENTS.md");
94
- const claudeMd = path.join(dest, "CLAUDE.md");
95
- const dockerfile = path.join(dest, "Dockerfile");
96
- const packageJson = path.join(dest, "package.json");
97
- const srcDir = path.join(dest, "src");
98
-
99
- // Record AGENTS.md content before update — it must not change.
100
- const agentsContentBefore = fs.readFileSync(agentsMd, "utf-8");
101
-
102
- fs.rmSync(packageJson);
103
- fs.rmSync(srcDir, { recursive: true });
104
-
105
- try {
106
- execFileSync(process.execPath, [BUNDLE, "update"], {
107
- cwd: dest,
108
- encoding: "utf-8",
109
- stdio: ["ignore", "pipe", "pipe"],
110
- });
111
- } catch (err) {
112
- const out = `${err.stdout ?? ""}${err.stderr ?? ""}`.trim();
113
- fail(`Pass 2: \`sailor update\` exited non-zero.\n ${out || err.message}`);
114
- }
115
-
116
- if (!fs.existsSync(packageJson))
117
- fail(`Pass 2: update did not re-add missing "package.json"`);
118
- if (!fs.existsSync(srcDir))
119
- fail(`Pass 2: update did not re-add missing "src/" directory`);
120
-
121
- // AGENTS.md, CLAUDE.md, Dockerfile must not be overwritten.
122
- const agentsContentAfter = fs.readFileSync(agentsMd, "utf-8");
123
- if (agentsContentAfter !== agentsContentBefore)
124
- fail(`Pass 2: update overwrote "AGENTS.md" — user-space files must never be overwritten`);
125
- if (!fs.existsSync(claudeMd))
126
- fail(`Pass 2: "CLAUDE.md" is missing after update`);
127
- if (!fs.existsSync(dockerfile))
128
- fail(`Pass 2: "Dockerfile" is missing after update`);
129
-
130
- console.log("✓ Pass 2 passed — missing user-space files seeded, existing ones untouched");
131
-
132
- // ── PASS 3 — stale path pruning ───────────────────────────────────────────
133
- const staleSkill = path.join(dest, ".agents/skills/sail-ci/SKILL.md");
134
- fs.mkdirSync(path.dirname(staleSkill), { recursive: true });
135
- fs.writeFileSync(staleSkill, "# old sail-ci skill\n", "utf-8");
136
-
137
- try {
138
- execFileSync(process.execPath, [BUNDLE, "update"], {
139
- cwd: dest,
140
- encoding: "utf-8",
141
- stdio: ["ignore", "pipe", "pipe"],
142
- });
143
- } catch (err) {
144
- const out = `${err.stdout ?? ""}${err.stderr ?? ""}`.trim();
145
- fail(`Pass 3: \`sailor update\` exited non-zero.\n ${out || err.message}`);
146
- }
147
-
148
- if (fs.existsSync(path.join(dest, ".agents/skills/sail-ci")))
149
- fail(`Pass 3: stale ".agents/skills/sail-ci" was not removed`);
150
- if (!fs.existsSync(path.join(dest, ".agents/skills/sail-automation/SKILL.md")))
151
- fail(`Pass 3: ".agents/skills/sail-automation/SKILL.md" missing after update`);
152
-
153
- console.log("✓ Pass 3 passed — stale sail-ci skill pruned, sail-automation present");
154
-
155
- // ── PASS 4 — init-on-existing errors ──────────────────────────────────────
156
- let initRejected = false;
157
- let initOutput = "";
158
- try {
159
- initOutput = execFileSync(process.execPath, [BUNDLE, "init"], {
160
- cwd: dest,
161
- encoding: "utf-8",
162
- stdio: ["ignore", "pipe", "pipe"],
163
- });
164
- } catch (err) {
165
- initRejected = true;
166
- initOutput = `${err.stdout ?? ""}${err.stderr ?? ""}`.trim();
167
- }
168
-
169
- if (!initRejected)
170
- fail(`Pass 4: \`sailor init\` on existing project did not exit non-zero — should refuse`);
171
- if (!/already initialized/i.test(initOutput))
172
- fail(`Pass 4: error message did not mention "already initialized".\n output: ${initOutput}`);
173
-
174
- console.log("✓ Pass 4 passed — init on existing project correctly refused");
175
- } finally {
176
- fs.rmSync(tmpRoot, { recursive: true, force: true });
177
- }
package/scripts/clean.mjs DELETED
@@ -1,17 +0,0 @@
1
- import { rmSync } from "node:fs";
2
-
3
- const targets = [
4
- "node_modules",
5
- "packages/sdk/node_modules",
6
- "packages/sdk/dist",
7
- "packages/sdk/tsconfig.tsbuildinfo",
8
- "packages/cli/node_modules",
9
- "packages/cli/dist",
10
- "packages/ui/node_modules",
11
- "packages/ui/dist",
12
- ];
13
-
14
- for (const p of targets) {
15
- rmSync(p, { recursive: true, force: true });
16
- console.log(` removed ${p}`);
17
- }