@tulipes/cli 0.2.0 → 0.3.0-rc.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/dist/config-check.js +1 -1
  2. package/dist/env-check.d.ts +5 -3
  3. package/dist/env-check.js +26 -19
  4. package/dist/env-check.js.map +1 -1
  5. package/dist/main.js +83 -19
  6. package/dist/main.js.map +1 -1
  7. package/dist/migrate-0.12-env.d.ts +36 -0
  8. package/dist/migrate-0.12-env.js +250 -0
  9. package/dist/migrate-0.12-env.js.map +1 -0
  10. package/dist/migrate-0.12-scaffold.d.ts +1 -0
  11. package/dist/migrate-0.12-scaffold.js +197 -0
  12. package/dist/migrate-0.12-scaffold.js.map +1 -0
  13. package/dist/migrate-0.12.d.ts +47 -0
  14. package/dist/migrate-0.12.js +210 -0
  15. package/dist/migrate-0.12.js.map +1 -0
  16. package/dist/migrate.d.ts +3 -1
  17. package/dist/migrate.js +11 -5
  18. package/dist/migrate.js.map +1 -1
  19. package/dist/new-module.d.ts +7 -0
  20. package/dist/new-module.js +35 -16
  21. package/dist/new-module.js.map +1 -1
  22. package/dist/roles.d.ts +15 -0
  23. package/dist/roles.js +54 -0
  24. package/dist/roles.js.map +1 -0
  25. package/dist/routes.d.ts +7 -2
  26. package/dist/routes.js +19 -10
  27. package/dist/routes.js.map +1 -1
  28. package/dist/spec.d.ts +2 -0
  29. package/dist/spec.js +13 -11
  30. package/dist/spec.js.map +1 -1
  31. package/dist/sync.js +66 -53
  32. package/dist/sync.js.map +1 -1
  33. package/package.json +4 -4
  34. package/templates/AGENTS.md +35 -23
  35. package/templates/app/.envs/.env.development +2 -1
  36. package/templates/app/.envs/.env.production +2 -2
  37. package/templates/app/.envs/.env.staging +2 -2
  38. package/templates/app/README.md +44 -29
  39. package/templates/app/config/app.config.ts +60 -7
  40. package/templates/app/docs/agent-manifest.json +8 -681
  41. package/templates/app/modules/agent/controllers/{agent.controllers.ts → web.agent.controllers.ts} +1 -1
  42. package/templates/app/modules/agent/routes/{agent.routes.ts → web.agent.routes.ts} +4 -4
  43. package/templates/app/modules/auth/bootstrap/credentials.bootstrap.ts +20 -12
  44. package/templates/app/modules/auth/bootstrap/seed-admin.bootstrap.ts +4 -2
  45. package/templates/app/modules/auth/controllers/{auth.controllers.ts → web.auth.controllers.ts} +1 -1
  46. package/templates/app/modules/auth/helpers/passport.ts +1 -1
  47. package/templates/app/modules/auth/helpers/session-cache.ts +3 -3
  48. package/templates/app/modules/auth/helpers/tokens.ts +3 -5
  49. package/templates/app/modules/auth/module.acl.ts +31 -9
  50. package/templates/app/modules/auth/module.config.ts +5 -3
  51. package/templates/app/modules/auth/package.json +10 -10
  52. package/templates/app/modules/auth/routes/{auth.routes.ts → web.auth.routes.ts} +15 -10
  53. package/templates/app/modules/auth/tests/auth.test.ts +1 -1
  54. package/templates/app/modules/config/controllers/{config.controllers.ts → bo.config.controllers.ts} +3 -2
  55. package/templates/app/modules/config/helpers/audit.ts +1 -1
  56. package/templates/app/modules/config/helpers/describe.ts +35 -26
  57. package/templates/app/modules/config/module.acl.ts +1 -1
  58. package/templates/app/modules/config/routes/{config.routes.ts → bo.config.routes.ts} +5 -5
  59. package/templates/app/modules/config/tests/config.test.ts +14 -6
  60. package/templates/app/modules/core/helpers/cache/index.ts +8 -7
  61. package/templates/app/modules/core/helpers/lazy.ts +3 -3
  62. package/templates/app/modules/core/helpers/logger/index.ts +24 -16
  63. package/templates/app/modules/core/module.config.ts +5 -3
  64. package/templates/app/modules/core/package.json +46 -19
  65. package/templates/app/modules/core/routes/core.routes.ts +4 -4
  66. package/templates/app/modules/hello/agent/hello.agent.ts +2 -2
  67. package/templates/app/modules/hello/controllers/{hello.controllers.ts → web/hello.controllers.ts} +1 -1
  68. package/templates/app/modules/hello/controllers/{greetings.controllers.ts → web/hello.greetings.controllers.ts} +2 -2
  69. package/templates/app/modules/hello/module.acl.ts +12 -7
  70. package/templates/app/modules/hello/queues/hello.queues.ts +1 -1
  71. package/templates/app/modules/hello/routes/{greetings.routes.ts → web/hello.greetings.routes.ts} +4 -4
  72. package/templates/app/modules/hello/routes/{hello.routes.ts → web/hello.routes.ts} +14 -8
  73. package/templates/app/modules/security/helpers/rate-limit.ts +3 -2
  74. package/templates/app/modules/security/routes/security.routes.ts +4 -4
  75. package/templates/app/modules/settings/bootstrap/log-streams.bootstrap.ts +2 -2
  76. package/templates/app/modules/settings/controllers/{settings.controllers.ts → bo.settings.controllers.ts} +7 -6
  77. package/templates/app/modules/settings/helpers/log-stream.ts +6 -5
  78. package/templates/app/modules/settings/routes/{settings.routes.ts → bo.settings.routes.ts} +3 -3
  79. package/templates/app/modules/settings/tests/settings.test.ts +21 -25
  80. package/templates/app/modules/users/controllers/{users.controllers.ts → bo.users.controllers.ts} +5 -55
  81. package/templates/{overlays/no-queues/modules/users/controllers/users.controllers.ts → app/modules/users/controllers/web.users.controllers.ts} +4 -42
  82. package/templates/app/modules/users/module.acl.ts +4 -4
  83. package/templates/app/modules/users/queues/users.queues.ts +1 -1
  84. package/templates/app/modules/users/routes/bo.users.routes.ts +39 -0
  85. package/templates/app/modules/users/routes/web.users.routes.ts +45 -0
  86. package/templates/app/nginx/production.conf +1 -1
  87. package/templates/app/scripts/deploy-nginx.ts +3 -2
  88. package/templates/app/tests/support.ts +4 -3
  89. package/templates/claude/skills/tulipes-agent-tool/SKILL.md +6 -4
  90. package/templates/claude/skills/tulipes-boot-errors/SKILL.md +13 -7
  91. package/templates/claude/skills/tulipes-config/SKILL.md +6 -4
  92. package/templates/claude/skills/tulipes-endpoint/SKILL.md +34 -15
  93. package/templates/claude/skills/tulipes-env-variable/SKILL.md +62 -37
  94. package/templates/claude/skills/tulipes-i18n/SKILL.md +20 -1
  95. package/templates/claude/skills/tulipes-module/SKILL.md +10 -5
  96. package/templates/claude/skills/tulipes-permissions/SKILL.md +123 -35
  97. package/templates/claude/skills/tulipes-queue/SKILL.md +3 -2
  98. package/templates/claude/skills/tulipes-settings/SKILL.md +9 -8
  99. package/templates/claude/skills/tulipes-spec/SKILL.md +12 -2
  100. package/templates/minimal/config/app.config.ts +13 -3
  101. package/templates/minimal/modules/agent/routes/{agent.routes.ts → web.agent.routes.ts} +2 -2
  102. package/templates/minimal/modules/core/module.acl.ts +7 -1
  103. package/templates/minimal/modules/core/package.json +4 -4
  104. package/templates/minimal/modules/hello/module.acl.ts +1 -1
  105. package/templates/minimal/modules/hello/routes/{health.routes.ts → web.hello.health.routes.ts} +1 -1
  106. package/templates/minimal/modules/hello/routes/{hello.routes.ts → web.hello.routes.ts} +1 -1
  107. package/templates/overlays/no-queues/modules/hello/controllers/{greetings.controllers.ts → web/hello.greetings.controllers.ts} +1 -1
  108. package/templates/overlays/no-queues/modules/users/controllers/bo.users.controllers.ts +57 -0
  109. package/templates/versions.json +8 -8
  110. package/templates/app/modules/users/routes/users.routes.ts +0 -60
@@ -0,0 +1 @@
1
+ export declare function migrateScaffoldIdioms(rootDir: string, changed: string[]): void;
@@ -0,0 +1,197 @@
1
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { join, relative } from "node:path";
3
+ const REWRITES = [
4
+ {
5
+ file: "modules/auth/bootstrap/credentials.bootstrap.ts",
6
+ // The names are the ones the environment step derives from the variables
7
+ // (`AUTH_ADMIN_EMAIL` → `authAdminEmail`), not the fresh scaffold's.
8
+ note: "reads config.auth.*, names the variables in its message",
9
+ from: `export default function credentials({ Environment }: RuntimeCtx): void {
10
+ if (Environment.appEnv !== "production" && Environment.appEnv !== "staging") return;
11
+ const invalid: string[] = [];
12
+ const defaults = [
13
+ ["JWT_ACCESS_SECRET", "dev-only-access-secret-change-me"],
14
+ ["JWT_REFRESH_SECRET", "dev-only-refresh-secret-change-me"],
15
+ ["AUTH_ADMIN_PASSWORD", "dev-admin-password"],
16
+ ["AUTH_ADMIN_EMAIL", "admin@example.com"],
17
+ ] as const;
18
+ for (const [name, developmentValue] of defaults) {
19
+ const value = String(Environment.get(name)).trim();
20
+ if (!value || value === developmentValue) invalid.push(name);
21
+ }
22
+ if (Environment.get("JWT_ACCESS_SECRET") === Environment.get("JWT_REFRESH_SECRET")) {
23
+ invalid.push("JWT_ACCESS_SECRET / JWT_REFRESH_SECRET (use distinct keys)");
24
+ }
25
+ if (invalid.length) throw new Error("Unsafe production credentials: configure " + invalid.join(", "));
26
+ }`,
27
+ to: `const PLACEHOLDERS = [
28
+ ["jwtAccessSecret", "JWT_ACCESS_SECRET", "dev-only-access-secret-change-me"],
29
+ ["jwtRefreshSecret", "JWT_REFRESH_SECRET", "dev-only-refresh-secret-change-me"],
30
+ ["authAdminPassword", "AUTH_ADMIN_PASSWORD", "dev-admin-password"],
31
+ ["authAdminEmail", "AUTH_ADMIN_EMAIL", "admin@example.com"],
32
+ ] as const;
33
+
34
+ export default function credentials({ appEnv, config }: RuntimeCtx): void {
35
+ if (appEnv !== "production" && appEnv !== "staging") return;
36
+
37
+ // The values are this module's environment values; the message names the
38
+ // variables an operator sets, not the config names.
39
+ const auth = config.auth!;
40
+ const invalid: string[] = [];
41
+
42
+ for (const [name, variable, developmentValue] of PLACEHOLDERS) {
43
+ const value = auth[name].trim();
44
+ if (!value || value === developmentValue) invalid.push(variable);
45
+ }
46
+ if (auth.jwtAccessSecret === auth.jwtRefreshSecret) {
47
+ invalid.push("JWT_ACCESS_SECRET / JWT_REFRESH_SECRET (use distinct keys)");
48
+ }
49
+
50
+ if (invalid.length) throw new Error("Unsafe production credentials: configure " + invalid.join(", "));
51
+ }`,
52
+ },
53
+ {
54
+ file: "modules/auth/helpers/tokens.ts",
55
+ note: "the signing keys are config.auth.*",
56
+ from: `const secret = (ctx: Ctx, typ: "access" | "refresh"): string =>
57
+ String(
58
+ ctx.Environment.get(
59
+ typ === "access" ? "JWT_ACCESS_SECRET" : "JWT_REFRESH_SECRET",
60
+ ),
61
+ );`,
62
+ to: `const secret = (ctx: Ctx, typ: "access" | "refresh"): string =>
63
+ typ === "access" ? ctx.config.auth!.jwtAccessSecret : ctx.config.auth!.jwtRefreshSecret;`,
64
+ },
65
+ {
66
+ file: "modules/config/helpers/describe.ts",
67
+ note: "the console describes environment values through ctx.settings.describeEnv()",
68
+ from: ` const variables = ctx.Environment.store.describe()
69
+ .filter((variable) => variable.owner === name)
70
+ .map((variable) => ({
71
+ name: variable.spec.name,
72
+ type: variable.spec.type,
73
+ description: variable.spec.description,
74
+ group: variable.spec.group ?? null,
75
+ required: variable.spec.required ?? false,
76
+ enum: variable.spec.enum ?? null,
77
+ default: variable.spec.type === "secret" ? null : (variable.spec.default ?? null),
78
+ source: variable.source,
79
+ is_secret: variable.isSecret,
80
+ is_set: variable.isSet,
81
+ // What this process resolved; a secret is never here.
82
+ value: variable.isSecret ? null : (variable.value ?? null),
83
+ }));`,
84
+ to: ` // The env half of the module's namespace, as the framework describes it:
85
+ // a secret's value and default are already withheld.
86
+ const variables = ctx.settings.describeEnv(name).map((variable) => ({
87
+ name: variable.name,
88
+ key: variable.key,
89
+ type: variable.type,
90
+ description: variable.description,
91
+ group: variable.group ?? null,
92
+ required: variable.required,
93
+ enum: variable.enum ?? null,
94
+ default: variable.default ?? null,
95
+ source: variable.source ?? null,
96
+ is_secret: variable.isSecret,
97
+ is_set: variable.isSet,
98
+ value: variable.value ?? null,
99
+ }));`,
100
+ },
101
+ {
102
+ file: "modules/settings/tests/settings.test.ts",
103
+ note: "the stream tests boot with file logging on, into a directory they own",
104
+ from: `const app = useTestApp({ rootDir, env: testEnv(), settings: testSettings() }, { beforeAll, afterAll });
105
+ `,
106
+ to: `/**
107
+ * Where this boot's logger writes and the stream reads: environment values
108
+ * are bound once at boot, so the file logging the stream needs is on for
109
+ * the whole file, into a directory the suite owns and empties per test.
110
+ */
111
+ const logDir = mkdtempSync(join(tmpdir(), "tulipes-log-http-"));
112
+ const app = useTestApp({ rootDir, env: { ...testEnv(), LOG_FILE: "true", LOG_DIR: logDir }, settings: testSettings() }, { beforeAll, afterAll });
113
+ afterAll(() => rm(logDir, { recursive: true, force: true }));
114
+ `,
115
+ },
116
+ {
117
+ file: "modules/settings/tests/settings.test.ts",
118
+ note: "logFiles() empties the directory instead of changing read-only values",
119
+ from: `async function logFiles(run: (dir: string) => Promise<void>) {
120
+ const { mkdtemp, rm } = await import("node:fs/promises");
121
+ const { tmpdir } = await import("node:os");
122
+ const { join } = await import("node:path");
123
+ const dir = await mkdtemp(join(tmpdir(), "tulipes-log-http-"));
124
+ const env = app.handle.ctx.Environment;
125
+
126
+ const previous = {
127
+ file: env.get("LOG_FILE"),
128
+ dir: env.get("LOG_DIR"),
129
+ };
130
+
131
+ env.store.set("LOG_FILE", true);
132
+
133
+ env.store.set("LOG_DIR", dir);
134
+
135
+ try {
136
+ await run(dir);
137
+ } finally {
138
+ env.store.set("LOG_FILE", previous.file);
139
+
140
+ env.store.set("LOG_DIR", previous.dir);
141
+
142
+ await rm(dir, { recursive: true, force: true });
143
+ }
144
+ }`,
145
+ to: `/** \`run\` sees the log directory empty: one test's files never serve another's stream. */
146
+ async function logFiles(run: (dir: string) => Promise<void>) {
147
+ const empty = async () => {
148
+ for (const entry of await readdir(logDir)) await rm(join(logDir, entry), { recursive: true, force: true });
149
+ };
150
+ await empty();
151
+ try {
152
+ await run(logDir);
153
+ } finally {
154
+ await empty();
155
+ }
156
+ }`,
157
+ },
158
+ {
159
+ file: "modules/settings/tests/settings.test.ts",
160
+ note: "a second boot reads the same LOG_FILE and LOG_DIR from the process",
161
+ from: ` try {
162
+ next.env.store.set("LOG_FILE", true);
163
+
164
+ next.env.store.set("LOG_DIR", dir);
165
+
166
+ const address = next.server!.address();`,
167
+ to: ` try {
168
+ // LOG_FILE and LOG_DIR are still set on the process, so this boot
169
+ // reads the same directory.
170
+ const address = next.server!.address();`,
171
+ },
172
+ {
173
+ file: "modules/settings/tests/settings.test.ts",
174
+ note: "the imports the rewritten helpers use",
175
+ from: `import { randomBytes } from "node:crypto";
176
+ import { fileURLToPath } from "node:url";`,
177
+ to: `import { randomBytes } from "node:crypto";
178
+ import { mkdtempSync } from "node:fs";
179
+ import { readdir, rm } from "node:fs/promises";
180
+ import { tmpdir } from "node:os";
181
+ import { join } from "node:path";
182
+ import { fileURLToPath } from "node:url";`,
183
+ },
184
+ ];
185
+ export function migrateScaffoldIdioms(rootDir, changed) {
186
+ for (const rewrite of REWRITES) {
187
+ const file = join(rootDir, rewrite.file);
188
+ if (!existsSync(file))
189
+ continue;
190
+ const source = readFileSync(file, "utf8");
191
+ if (!source.includes(rewrite.from))
192
+ continue; // edited, or already migrated
193
+ writeFileSync(file, source.replace(rewrite.from, rewrite.to));
194
+ changed.push(`${relative(rootDir, file)}: ${rewrite.note}`);
195
+ }
196
+ }
197
+ //# sourceMappingURL=migrate-0.12-scaffold.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"migrate-0.12-scaffold.js","sourceRoot":"","sources":["../src/migrate-0.12-scaffold.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAClE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAqB3C,MAAM,QAAQ,GAAc;IAC1B;QACE,IAAI,EAAE,iDAAiD;QACvD,yEAAyE;QACzE,qEAAqE;QACrE,IAAI,EAAE,yDAAyD;QAC/D,IAAI,EAAE;;;;;;;;;;;;;;;;;EAiBR;QACE,EAAE,EAAE;;;;;;;;;;;;;;;;;;;;;;;;EAwBN;KACC;IACD;QACE,IAAI,EAAE,gCAAgC;QACtC,IAAI,EAAE,oCAAoC;QAC1C,IAAI,EAAE;;;;;KAKL;QACD,EAAE,EAAE;2FACmF;KACxF;IACD;QACE,IAAI,EAAE,oCAAoC;QAC1C,IAAI,EAAE,6EAA6E;QACnF,IAAI,EAAE;;;;;;;;;;;;;;;SAeD;QACL,EAAE,EAAE;;;;;;;;;;;;;;;OAeD;KACJ;IACD;QACE,IAAI,EAAE,yCAAyC;QAC/C,IAAI,EAAE,uEAAuE;QAC7E,IAAI,EAAE;CACT;QACG,EAAE,EAAE;;;;;;;;CAQP;KACE;IACD;QACE,IAAI,EAAE,yCAAyC;QAC/C,IAAI,EAAE,uEAAuE;QAC7E,IAAI,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;EAyBR;QACE,EAAE,EAAE;;;;;;;;;;;EAWN;KACC;IACD;QACE,IAAI,EAAE,yCAAyC;QAC/C,IAAI,EAAE,oEAAoE;QAC1E,IAAI,EAAE;;;;;gDAKsC;QAC5C,EAAE,EAAE;;;gDAGwC;KAC7C;IACD;QACE,IAAI,EAAE,yCAAyC;QAC/C,IAAI,EAAE,uCAAuC;QAC7C,IAAI,EAAE;0CACgC;QACtC,EAAE,EAAE;;;;;0CAKkC;KACvC;CACF,CAAC;AAEF,MAAM,UAAU,qBAAqB,CAAC,OAAe,EAAE,OAAiB;IACtE,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;QAC/B,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;QACzC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;YAAE,SAAS;QAChC,MAAM,MAAM,GAAG,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;QAC1C,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,CAAC;YAAE,SAAS,CAAC,8BAA8B;QAC5E,aAAa,CAAC,IAAI,EAAE,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,EAAE,CAAC,CAAC,CAAC;QAC9D,OAAO,CAAC,IAAI,CAAC,GAAG,QAAQ,CAAC,OAAO,EAAE,IAAI,CAAC,KAAK,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC;IAC9D,CAAC;AACH,CAAC"}
@@ -0,0 +1,47 @@
1
+ /**
2
+ * `tulipes migrate 0.12` — every RAI id gains a scope.
3
+ *
4
+ * 0.12 ids are `module:scope:resource[:extra]`. A 0.11 app's are
5
+ * `module:resource`, so this inserts one scope (`web` unless `--scope`
6
+ * says otherwise) into:
7
+ *
8
+ * - every `id: "a:b"` in `routes/` (any depth) and `agent/`;
9
+ * - every two-segment grant inside an `allow(...)` call in
10
+ * `module.acl.ts` (`"a:*"` and `"*"` are already valid and stay);
11
+ * - every other string literal in the app's TypeScript that is exactly
12
+ * one of the ids rewritten above — an access check written by hand,
13
+ * an assertion in a test.
14
+ *
15
+ * It renames no file: scope-first names are a convention, not a contract.
16
+ * What it cannot read — an id built at runtime — is a TODO line in the
17
+ * report and in the record; no comment is written into the file. Idempotent: an id that already has a scope is left alone.
18
+ *
19
+ * The one judgement it leaves to a person is which ids belong to another
20
+ * scope: everything lands in one, and moving the back office to `bo` is a
21
+ * rename of the id, its grants and its references, by hand.
22
+ *
23
+ * The same command carries the environment into the config — see
24
+ * `migrate-0.12-env.ts` for the rules and `migrate-0.12-scaffold.ts` for
25
+ * the reference app's own idioms.
26
+ */
27
+ export interface Migration012Report {
28
+ scope: string;
29
+ changed: string[];
30
+ todos: string[];
31
+ /** Every id rewritten, old → new. */
32
+ ids: Record<string, string>;
33
+ /** Per module: variable → the config name it is read under. */
34
+ env: Record<string, Record<string, string>>;
35
+ }
36
+ export declare function migrate012(rootDir: string, options?: {
37
+ scope?: string;
38
+ }): Migration012Report;
39
+ /**
40
+ * Applies `rewrite` to every string literal inside each `allow(...)` call,
41
+ * and to nothing else in the file — an import specifier like "node:fs"
42
+ * also has two segments.
43
+ */
44
+ export declare function rewriteAllowCalls(source: string, rewrite: (grant: string) => string): string;
45
+ export declare function runMigrate012(rootDir: string, options: {
46
+ scope?: string;
47
+ }): Promise<void>;
@@ -0,0 +1,210 @@
1
+ import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
2
+ import { dirname, join, relative } from "node:path";
3
+ import { isRai, RAI_SEGMENT } from "@tulipes/core/acl";
4
+ import { walkTs } from "./migrate.js";
5
+ import { migrateEnv } from "./migrate-0.12-env.js";
6
+ import { migrateScaffoldIdioms } from "./migrate-0.12-scaffold.js";
7
+ const TWO_SEGMENTS = /^[a-z0-9][a-z0-9-]*:[a-z0-9][a-z0-9-]*$/;
8
+ /**
9
+ * Where an id is declared. In a routes file only inside `rai({ … })`, so a
10
+ * zod field or a payload named `id` is never mistaken for one; an agent
11
+ * file holds nothing but declarations, so any `id:` there is an action's.
12
+ */
13
+ const PATTERNS = {
14
+ routes: {
15
+ literal: /(\brai\s*\(\s*\{[^{}]*?\bid\s*:\s*)(["'])([^"'\n]*)\2/g,
16
+ computed: /\brai\s*\(\s*\{[^{}]*?\bid\s*:\s*(?:`|[A-Za-z_$])/g,
17
+ },
18
+ agent: {
19
+ literal: /(\bid\s*:\s*)(["'])([^"'\n]*)\2/g,
20
+ computed: /\bid\s*:\s*(?:`|[A-Za-z_$])/g,
21
+ },
22
+ };
23
+ export function migrate012(rootDir, options = {}) {
24
+ const scope = options.scope ?? "web";
25
+ if (!RAI_SEGMENT.test(scope)) {
26
+ throw new Error(`--scope "${scope}" is not a scope — lowercase, starting with a letter ([a-z][a-z0-9-]*)`);
27
+ }
28
+ const report = { scope, changed: [], todos: [], ids: {}, env: {} };
29
+ const scoped = (id) => {
30
+ const [module, resource] = id.split(":");
31
+ return `${module}:${scope}:${resource}`;
32
+ };
33
+ const modulesDir = join(rootDir, "modules");
34
+ const modules = existsSync(modulesDir)
35
+ ? readdirSync(modulesDir, { withFileTypes: true })
36
+ .filter((entry) => entry.isDirectory() && !entry.name.startsWith("."))
37
+ .map((entry) => entry.name)
38
+ .sort()
39
+ : [];
40
+ const declarations = new Set();
41
+ // ── declarations: routes (any depth) and agent actions ────────────────
42
+ for (const name of modules) {
43
+ const dir = join(modulesDir, name);
44
+ const files = [
45
+ ...(existsSync(join(dir, "routes")) ? walkTs(join(dir, "routes")).map((file) => [file, PATTERNS.routes]) : []),
46
+ ...(existsSync(join(dir, "agent")) ? walkTs(join(dir, "agent")).map((file) => [file, PATTERNS.agent]) : []),
47
+ ];
48
+ for (const [file, pattern] of files) {
49
+ declarations.add(file);
50
+ const label = relative(rootDir, file);
51
+ const source = readFileSync(file, "utf8");
52
+ let count = 0;
53
+ const next = source.replace(pattern.literal, (match, head, quote, id) => {
54
+ if (isRai(id))
55
+ return match;
56
+ if (!TWO_SEGMENTS.test(id)) {
57
+ report.todos.push(`${label}: id "${id}" is neither a 0.11 id nor a 0.12 one — rewrite it as module:scope:resource[:extra]`);
58
+ return match;
59
+ }
60
+ const target = scoped(id);
61
+ if (!isRai(target)) {
62
+ report.todos.push(`${label}: id "${id}" cannot take a scope as is — every segment must start with a letter ("${target}")`);
63
+ return match;
64
+ }
65
+ report.ids[id] = target;
66
+ count++;
67
+ return `${head}${quote}${target}${quote}`;
68
+ });
69
+ for (const computed of source.matchAll(pattern.computed)) {
70
+ const line = source.slice(0, computed.index).split("\n").length;
71
+ report.todos.push(`${label}:${line}: an id built at runtime — make sure it produces module:scope:resource[:extra]`);
72
+ }
73
+ if (next !== source) {
74
+ writeFileSync(file, next);
75
+ report.changed.push(`${label}: ${count} id(s) scoped`);
76
+ }
77
+ }
78
+ }
79
+ // ── grants: two-segment literals inside allow(...) ────────────────────
80
+ for (const name of modules) {
81
+ const file = join(modulesDir, name, "module.acl.ts");
82
+ if (!existsSync(file))
83
+ continue;
84
+ declarations.add(file);
85
+ const source = readFileSync(file, "utf8");
86
+ let count = 0;
87
+ const next = rewriteAllowCalls(source, (grant) => {
88
+ if (!TWO_SEGMENTS.test(grant))
89
+ return grant;
90
+ const target = report.ids[grant] ?? scoped(grant);
91
+ if (!isRai(target)) {
92
+ report.todos.push(`${relative(rootDir, file)}: grant "${grant}" cannot take a scope as is — every segment must start with a letter`);
93
+ return grant;
94
+ }
95
+ report.ids[grant] = target;
96
+ count++;
97
+ return target;
98
+ });
99
+ if (next !== source) {
100
+ writeFileSync(file, next);
101
+ report.changed.push(`${relative(rootDir, file)}: ${count} grant(s) scoped`);
102
+ }
103
+ }
104
+ // ── references: a literal equal to a rewritten id, anywhere else ──────
105
+ const known = Object.keys(report.ids);
106
+ if (known.length > 0) {
107
+ const literal = new RegExp(`(["'\`])(${known.map(escape).join("|")})\\1`, "g");
108
+ for (const file of walkTs(rootDir)) {
109
+ if (declarations.has(file))
110
+ continue;
111
+ const source = readFileSync(file, "utf8");
112
+ let count = 0;
113
+ const next = source.replace(literal, (_match, quote, id) => {
114
+ count++;
115
+ return `${quote}${report.ids[id]}${quote}`;
116
+ });
117
+ if (next !== source) {
118
+ writeFileSync(file, next);
119
+ report.changed.push(`${relative(rootDir, file)}: ${count} reference(s) to a rewritten id`);
120
+ }
121
+ }
122
+ }
123
+ // ── the environment: declarations keyed by config name, reads through ctx.config ──
124
+ // The scaffold's own idioms first, while their 0.11 text is still intact.
125
+ migrateScaffoldIdioms(rootDir, report.changed);
126
+ const env = migrateEnv(rootDir);
127
+ report.changed.push(...env.changed);
128
+ report.todos.push(...env.todos);
129
+ report.env = env.env;
130
+ const record = join(rootDir, "config", "migration-0.12.json");
131
+ mkdirSync(dirname(record), { recursive: true });
132
+ writeFileSync(record, JSON.stringify({ migrated_at: new Date().toISOString(), scope, ids: report.ids, env: report.env, todos: report.todos }, null, 2) + "\n");
133
+ report.changed.push(`wrote config/migration-0.12.json (${known.length} id(s) scoped "${scope}", ${Object.values(report.env).reduce((n, m) => n + Object.keys(m).length, 0)} variable(s) keyed)`);
134
+ return report;
135
+ }
136
+ /**
137
+ * Applies `rewrite` to every string literal inside each `allow(...)` call,
138
+ * and to nothing else in the file — an import specifier like "node:fs"
139
+ * also has two segments.
140
+ */
141
+ export function rewriteAllowCalls(source, rewrite) {
142
+ let output = "";
143
+ let cursor = 0;
144
+ const call = /\ballow\s*\(/g;
145
+ for (let match = call.exec(source); match; match = call.exec(source)) {
146
+ const open = match.index + match[0].length;
147
+ let depth = 1;
148
+ let index = open;
149
+ let quote;
150
+ for (; index < source.length && depth > 0; index++) {
151
+ const char = source[index];
152
+ if (quote) {
153
+ if (char === "\\")
154
+ index++;
155
+ else if (char === quote)
156
+ quote = undefined;
157
+ }
158
+ else if (char === '"' || char === "'" || char === "`")
159
+ quote = char;
160
+ else if (char === "(")
161
+ depth++;
162
+ else if (char === ")")
163
+ depth--;
164
+ }
165
+ const args = source.slice(open, index);
166
+ output += source.slice(cursor, open) + args.replace(/(["'])([^"'\n]*)\1/g, (_whole, q, value) => `${q}${rewrite(value)}${q}`);
167
+ cursor = index;
168
+ call.lastIndex = index;
169
+ }
170
+ return output + source.slice(cursor);
171
+ }
172
+ export async function runMigrate012(rootDir, options) {
173
+ let report;
174
+ try {
175
+ report = migrate012(rootDir, options);
176
+ }
177
+ catch (error) {
178
+ console.error(error.message);
179
+ process.exitCode = 2;
180
+ return;
181
+ }
182
+ for (const line of report.changed)
183
+ console.log(` ${line}`);
184
+ const ids = Object.entries(report.ids);
185
+ console.log(`\nscoped ${ids.length} id(s) "${report.scope}"; ${report.todos.length} TODO(s) to review`);
186
+ for (const [from, to] of ids)
187
+ console.log(` ${from} → ${to}`);
188
+ for (const [module, variables] of Object.entries(report.env)) {
189
+ for (const [variable, name] of Object.entries(variables))
190
+ console.log(` ${variable} → config.${module}.${name}`);
191
+ }
192
+ for (const todo of report.todos)
193
+ console.log(` ! ${todo}`);
194
+ console.log([
195
+ "",
196
+ "next:",
197
+ " every variable is read as ctx.config.<module>.<name>; config/app.config.ts wires",
198
+ " server.port, mongoose.uri and bullmq.redisUrl from them (add the MongoDB parts if you want them)",
199
+ ` every id is in scope "${report.scope}" now. Move the back office (and any other surface)`,
200
+ " by hand: rename the id in routes/ or agent/, its grants in module.acl.ts, and its",
201
+ " references — `pnpm tulipes routes` lists them, `pnpm typecheck` finds a two-segment id",
202
+ " optional: name routes and controllers scope-first (routes/web.users.routes.ts)",
203
+ " pnpm typecheck && pnpm test",
204
+ " review config/migration-0.12.json, then delete it",
205
+ ].join("\n"));
206
+ }
207
+ function escape(text) {
208
+ return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
209
+ }
210
+ //# sourceMappingURL=migrate-0.12.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"migrate-0.12.js","sourceRoot":"","sources":["../src/migrate-0.12.ts"],"names":[],"mappings":"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"}
package/dist/migrate.d.ts CHANGED
@@ -20,7 +20,9 @@ export interface MigrationReport {
20
20
  type: string;
21
21
  }>>;
22
22
  }
23
- export declare function runMigrate(rootDir: string, target: string): Promise<void>;
23
+ export declare function runMigrate(rootDir: string, target: string, options?: {
24
+ scope?: string;
25
+ }): Promise<void>;
24
26
  export declare function migrate(rootDir: string): MigrationReport;
25
27
  export declare function rewriteSpecifiers(source: string): string;
26
28
  /** `constructor(private readonly ctx: Ctx) {}` → a declared field assigned in the body. */
package/dist/migrate.js CHANGED
@@ -1,9 +1,14 @@
1
1
  import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
2
2
  import { basename, dirname, join, relative } from "node:path";
3
3
  const PNPM = "pnpm@10.28.0";
4
- export async function runMigrate(rootDir, target) {
4
+ export async function runMigrate(rootDir, target, options = {}) {
5
5
  if (target !== "0.11") {
6
- console.error(`unknown migration target "${target}" — this CLI migrates an app to 0.11`);
6
+ console.error(`unknown migration target "${target}" — this CLI migrates an app to 0.11 (from 0.10) or 0.12 (from 0.11)`);
7
+ process.exitCode = 2;
8
+ return;
9
+ }
10
+ if (options.scope !== undefined) {
11
+ console.error("--scope applies to tulipes migrate 0.12 only");
7
12
  process.exitCode = 2;
8
13
  return;
9
14
  }
@@ -263,9 +268,10 @@ function migrateModule(rootDir, dir, name, report) {
263
268
  // routes: defineRoutes/runtime → plain functions; `agent: false` gone
264
269
  const routesDir = join(dir, "routes");
265
270
  if (existsSync(routesDir)) {
266
- for (const file of readdirSync(routesDir).filter((entry) => entry.endsWith(".ts"))) {
267
- migrateRoutesFile(join(routesDir, file), dir, name, report);
268
- stripAgentOptOut(join(routesDir, file), `${label}/routes/${file}`, report);
271
+ for (const path of walkTs(routesDir)) {
272
+ const file = relative(routesDir, path);
273
+ migrateRoutesFile(path, dir, name, report);
274
+ stripAgentOptOut(path, `${label}/routes/${file}`, report);
269
275
  }
270
276
  }
271
277
  // agent: tools with route references → actions only (0.11.0-rc.3)