@saasicat/cli 0.21.0 → 0.22.1
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/bin/saasicat.js +48 -48
- package/package.json +4 -4
package/bin/saasicat.js
CHANGED
|
@@ -10,8 +10,6 @@
|
|
|
10
10
|
// schema check [--prisma-schema=PATH] [--fragments=01,02,03]
|
|
11
11
|
// Reports what your schema is missing relative to the canonical
|
|
12
12
|
// fragments. Read-only; exits 1 on drift so CI can gate on it.
|
|
13
|
-
//
|
|
14
|
-
// Spec: handoff/superadmin/QUICKSTART_SIMPLIFICATIONS.md §P5.
|
|
15
13
|
|
|
16
14
|
import { readFile, writeFile, readdir } from 'node:fs/promises';
|
|
17
15
|
import { existsSync } from 'node:fs';
|
|
@@ -43,8 +41,8 @@ function resolveFragmentsDir() {
|
|
|
43
41
|
const candidate = join(root, 'prisma-fragments');
|
|
44
42
|
if (!existsSync(candidate)) {
|
|
45
43
|
throw new Error(
|
|
46
|
-
`prisma-fragments
|
|
47
|
-
'
|
|
44
|
+
`prisma-fragments/ directory not found at ${candidate}. ` +
|
|
45
|
+
'Make sure @saasicat/spec is installed.',
|
|
48
46
|
);
|
|
49
47
|
}
|
|
50
48
|
return candidate;
|
|
@@ -61,8 +59,8 @@ async function selectFragmentFiles(dir, filter) {
|
|
|
61
59
|
const unknown = filter.filter((prefix) => !available.has(prefix));
|
|
62
60
|
if (unknown.length > 0) {
|
|
63
61
|
console.error(
|
|
64
|
-
`✗
|
|
65
|
-
`
|
|
62
|
+
`✗ Unknown fragments: ${unknown.join(', ')}. ` +
|
|
63
|
+
`Available: ${[...available].join(', ')}`,
|
|
66
64
|
);
|
|
67
65
|
process.exit(1);
|
|
68
66
|
}
|
|
@@ -91,7 +89,7 @@ function parseFragmentFilter(raw) {
|
|
|
91
89
|
async function readSchemaOrExit(args) {
|
|
92
90
|
const schemaPath = resolve(args['prisma-schema'] ?? 'prisma/schema.prisma');
|
|
93
91
|
if (!existsSync(schemaPath)) {
|
|
94
|
-
console.error(`✗ schema.prisma
|
|
92
|
+
console.error(`✗ schema.prisma not found: ${schemaPath}`);
|
|
95
93
|
process.exit(1);
|
|
96
94
|
}
|
|
97
95
|
return { schemaPath, schema: await readFile(schemaPath, 'utf8') };
|
|
@@ -103,9 +101,7 @@ async function cmdSchemaApply(args) {
|
|
|
103
101
|
const fragmentsDir = resolveFragmentsDir();
|
|
104
102
|
const filter = parseFragmentFilter(args.fragments);
|
|
105
103
|
if (!filter && !args.all) {
|
|
106
|
-
console.error(
|
|
107
|
-
'✗ Entweder --fragments=01,02,03 oder --all übergeben. ' + 'Verfügbare Fragmente:',
|
|
108
|
-
);
|
|
104
|
+
console.error('✗ Pass either --fragments=01,02,03 or --all. Available fragments:');
|
|
109
105
|
const files = (await readdir(fragmentsDir)).filter((f) => f.endsWith('.prisma')).sort();
|
|
110
106
|
for (const f of files) console.error(` ${f}`);
|
|
111
107
|
process.exit(1);
|
|
@@ -113,7 +109,7 @@ async function cmdSchemaApply(args) {
|
|
|
113
109
|
|
|
114
110
|
const { files, blocks } = await loadFragments(fragmentsDir, filter);
|
|
115
111
|
if (blocks.size === 0) {
|
|
116
|
-
console.error('✗
|
|
112
|
+
console.error('✗ No models found in the selected fragments.');
|
|
117
113
|
process.exit(1);
|
|
118
114
|
}
|
|
119
115
|
|
|
@@ -122,14 +118,14 @@ async function cmdSchemaApply(args) {
|
|
|
122
118
|
});
|
|
123
119
|
|
|
124
120
|
if (result.added.length === 0) {
|
|
125
|
-
console.log(`→
|
|
121
|
+
console.log(`→ Nothing to do. Models already present: ${result.skipped.join(', ')}`);
|
|
126
122
|
return;
|
|
127
123
|
}
|
|
128
124
|
|
|
129
125
|
if (args['dry-run']) {
|
|
130
|
-
console.log(`(--dry-run)
|
|
126
|
+
console.log(`(--dry-run) Would append: ${result.added.join(', ')}`);
|
|
131
127
|
if (result.skipped.length) {
|
|
132
|
-
console.log(`(--dry-run)
|
|
128
|
+
console.log(`(--dry-run) Skipped (already present): ${result.skipped.join(', ')}`);
|
|
133
129
|
}
|
|
134
130
|
console.log('');
|
|
135
131
|
console.log(result.schema.slice(schema.length));
|
|
@@ -137,25 +133,25 @@ async function cmdSchemaApply(args) {
|
|
|
137
133
|
}
|
|
138
134
|
|
|
139
135
|
await writeFile(schemaPath, result.schema, 'utf8');
|
|
140
|
-
console.log(`✓ ${result.added.length}
|
|
136
|
+
console.log(`✓ Appended ${result.added.length} model(s): ${result.added.join(', ')}`);
|
|
141
137
|
if (result.skipped.length) {
|
|
142
|
-
console.log(`→
|
|
138
|
+
console.log(`→ Skipped (already present): ${result.skipped.join(', ')}`);
|
|
143
139
|
}
|
|
144
140
|
console.log('');
|
|
145
|
-
console.log('
|
|
146
|
-
console.log(' 1. schema.prisma
|
|
147
|
-
console.log(' 2. pnpm prisma migrate dev --name
|
|
141
|
+
console.log('Next steps:');
|
|
142
|
+
console.log(' 1. Review schema.prisma — especially the FK pointers to User/Tenant');
|
|
143
|
+
console.log(' 2. pnpm prisma migrate dev --name add_saasicat');
|
|
148
144
|
}
|
|
149
145
|
|
|
150
146
|
const MISMATCH_LABELS = {
|
|
151
|
-
type: '
|
|
152
|
-
optionality: '
|
|
153
|
-
list: '
|
|
147
|
+
type: 'type',
|
|
148
|
+
optionality: 'optionality',
|
|
149
|
+
list: 'list',
|
|
154
150
|
};
|
|
155
151
|
|
|
156
152
|
function printCheckReport(report) {
|
|
157
153
|
if (report.missingFields.length > 0) {
|
|
158
|
-
console.log(`✗
|
|
154
|
+
console.log(`✗ Missing fields (${report.missingFields.length}):`);
|
|
159
155
|
for (const { model, field, type } of report.missingFields) {
|
|
160
156
|
console.log(` ${`${model}.${field}`.padEnd(44)} ${type}`);
|
|
161
157
|
}
|
|
@@ -163,7 +159,7 @@ function printCheckReport(report) {
|
|
|
163
159
|
}
|
|
164
160
|
|
|
165
161
|
if (report.missingEnumValues.length > 0) {
|
|
166
|
-
console.log(`✗
|
|
162
|
+
console.log(`✗ Missing enum values (${report.missingEnumValues.length}):`);
|
|
167
163
|
for (const entry of report.missingEnumValues) {
|
|
168
164
|
console.log(` ${entry.enum}.${entry.value}`);
|
|
169
165
|
}
|
|
@@ -171,11 +167,11 @@ function printCheckReport(report) {
|
|
|
171
167
|
}
|
|
172
168
|
|
|
173
169
|
if (report.fieldMismatches.length > 0) {
|
|
174
|
-
console.log(`✗
|
|
170
|
+
console.log(`✗ Diverging fields (${report.fieldMismatches.length}):`);
|
|
175
171
|
for (const { model, field, reason, expected, actual } of report.fieldMismatches) {
|
|
176
172
|
const location = `${model}.${field}`.padEnd(44);
|
|
177
173
|
console.log(
|
|
178
|
-
` ${location} [${MISMATCH_LABELS[reason]}]
|
|
174
|
+
` ${location} [${MISMATCH_LABELS[reason]}] expected ${expected}, found ${actual}`,
|
|
179
175
|
);
|
|
180
176
|
}
|
|
181
177
|
console.log('');
|
|
@@ -183,9 +179,9 @@ function printCheckReport(report) {
|
|
|
183
179
|
|
|
184
180
|
const breaking = report.missingBlockAttributes.filter((a) => a.kind !== 'index');
|
|
185
181
|
if (breaking.length > 0) {
|
|
186
|
-
console.log(`✗
|
|
182
|
+
console.log(`✗ Missing constraints (${breaking.length}):`);
|
|
187
183
|
for (const { model, kind, expected, actual } of breaking) {
|
|
188
|
-
const suffix = kind === 'map' ? ` —
|
|
184
|
+
const suffix = kind === 'map' ? ` — found: @@map("${actual}")` : '';
|
|
189
185
|
console.log(` ${model.padEnd(28)} ${expected}${suffix}`);
|
|
190
186
|
}
|
|
191
187
|
console.log('');
|
|
@@ -193,18 +189,18 @@ function printCheckReport(report) {
|
|
|
193
189
|
|
|
194
190
|
const missingIndexes = report.missingBlockAttributes.filter((a) => a.kind === 'index');
|
|
195
191
|
if (missingIndexes.length > 0) {
|
|
196
|
-
console.log(`→
|
|
192
|
+
console.log(`→ Missing indexes (${missingIndexes.length}):`);
|
|
197
193
|
for (const { model, expected } of missingIndexes) {
|
|
198
194
|
console.log(` ${model.padEnd(28)} ${expected}`);
|
|
199
195
|
}
|
|
200
|
-
console.log('
|
|
196
|
+
console.log(' Not an error — costs query time, but breaks nothing.');
|
|
201
197
|
console.log('');
|
|
202
198
|
}
|
|
203
199
|
|
|
204
200
|
const absent = [...report.absentModels, ...report.absentEnums];
|
|
205
201
|
if (absent.length > 0) {
|
|
206
|
-
console.log(`→
|
|
207
|
-
console.log('
|
|
202
|
+
console.log(`→ Not adopted (${absent.length}): ${absent.join(', ')}`);
|
|
203
|
+
console.log(' Not an error — the app does not use these fragments.');
|
|
208
204
|
console.log('');
|
|
209
205
|
}
|
|
210
206
|
}
|
|
@@ -216,7 +212,7 @@ async function cmdSchemaCheck(args) {
|
|
|
216
212
|
const files = await selectFragmentFiles(fragmentsDir, filter);
|
|
217
213
|
|
|
218
214
|
if (files.length === 0) {
|
|
219
|
-
console.error('✗
|
|
215
|
+
console.error('✗ No fragments selected.');
|
|
220
216
|
process.exit(1);
|
|
221
217
|
}
|
|
222
218
|
|
|
@@ -225,7 +221,7 @@ async function cmdSchemaCheck(args) {
|
|
|
225
221
|
fragments.push(await readFile(join(fragmentsDir, file), 'utf8'));
|
|
226
222
|
}
|
|
227
223
|
|
|
228
|
-
console.log(`→
|
|
224
|
+
console.log(`→ Checking ${schemaPath} against ${files.length} fragment(s) from @saasicat/spec`);
|
|
229
225
|
console.log('');
|
|
230
226
|
|
|
231
227
|
const report = checkSchema(fragments.join('\n'), schema);
|
|
@@ -233,13 +229,13 @@ async function cmdSchemaCheck(args) {
|
|
|
233
229
|
|
|
234
230
|
const checked = `${report.checkedModelCount} Model(s), ${report.checkedEnumCount} Enum(s)`;
|
|
235
231
|
if (report.ok) {
|
|
236
|
-
console.log(`✓
|
|
232
|
+
console.log(`✓ No drift. Checked ${checked}.`);
|
|
237
233
|
return;
|
|
238
234
|
}
|
|
239
235
|
|
|
240
|
-
console.log(`✗ Drift
|
|
241
|
-
console.log('
|
|
242
|
-
console.log('
|
|
236
|
+
console.log(`✗ Drift found (checked ${checked}).`);
|
|
237
|
+
console.log(' Add the missing fields and enum values, then migrate.');
|
|
238
|
+
console.log(' Review diverging fields: platform code reads them with the spec type.');
|
|
243
239
|
process.exit(1);
|
|
244
240
|
}
|
|
245
241
|
|
|
@@ -256,22 +252,24 @@ function runChild(cmd, args, opts = {}) {
|
|
|
256
252
|
|
|
257
253
|
async function cmdSchemaMigrate(args) {
|
|
258
254
|
if (!args.name) {
|
|
259
|
-
console.error('✗ --name=<migration_name>
|
|
255
|
+
console.error('✗ --name=<migration_name> is required.');
|
|
260
256
|
process.exit(1);
|
|
261
257
|
}
|
|
262
258
|
|
|
263
259
|
console.log(
|
|
264
|
-
`→
|
|
260
|
+
`→ Step 1/2: saasicat schema apply ${args['fragments'] ? `--fragments=${args['fragments']}` : '--all'}`,
|
|
265
261
|
);
|
|
266
262
|
await cmdSchemaApply({
|
|
267
263
|
...args,
|
|
268
264
|
all: args['fragments'] ? undefined : true,
|
|
269
265
|
});
|
|
270
266
|
|
|
271
|
-
console.log(
|
|
267
|
+
console.log(
|
|
268
|
+
`→ Step 2/2: ${args['package-manager'] ?? 'pnpm'} prisma migrate dev --name ${args.name}`,
|
|
269
|
+
);
|
|
272
270
|
const pmRunner = args['package-manager'] ?? 'pnpm';
|
|
273
271
|
await runChild(pmRunner, ['prisma', 'migrate', 'dev', '--name', args.name]);
|
|
274
|
-
console.log('✓ schema migrate
|
|
272
|
+
console.log('✓ schema migrate succeeded.');
|
|
275
273
|
}
|
|
276
274
|
|
|
277
275
|
async function main() {
|
|
@@ -289,18 +287,20 @@ async function main() {
|
|
|
289
287
|
console.log('Usage: saasicat <command> [...args]');
|
|
290
288
|
console.log('');
|
|
291
289
|
console.log('Commands:');
|
|
292
|
-
console.log(' schema apply --all
|
|
293
|
-
console.log(' schema apply --fragments=01,02
|
|
294
|
-
console.log(' schema apply --dry-run
|
|
295
|
-
console.log(
|
|
296
|
-
|
|
290
|
+
console.log(' schema apply --all insert all platform models');
|
|
291
|
+
console.log(' schema apply --fragments=01,02 insert only these fragments');
|
|
292
|
+
console.log(' schema apply --dry-run print the diff only');
|
|
293
|
+
console.log(
|
|
294
|
+
' schema check report drift against @saasicat/spec',
|
|
295
|
+
);
|
|
296
|
+
console.log(' schema check --fragments=01,02 check only these fragments');
|
|
297
297
|
console.log(' schema migrate --name=<name> apply --all + prisma migrate dev');
|
|
298
298
|
console.log('');
|
|
299
299
|
console.log('Optional --prisma-schema=PATH (default prisma/schema.prisma).');
|
|
300
300
|
console.log('Optional --package-manager=pnpm|npm|yarn (default pnpm).');
|
|
301
301
|
return;
|
|
302
302
|
}
|
|
303
|
-
console.error(`
|
|
303
|
+
console.error(`Unknown command: ${cmd} ${sub ?? ''}`);
|
|
304
304
|
process.exit(1);
|
|
305
305
|
}
|
|
306
306
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@saasicat/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.22.1",
|
|
4
4
|
"description": "CLI helpers for SaaS platform consumers. Provides CliContextService (identity, MFA, production confirm, audit tag).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -27,9 +27,9 @@
|
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"qrcode-terminal": "^0.12.0",
|
|
30
|
-
"@saasicat/nest": "^0.
|
|
31
|
-
"@saasicat/spec": "^0.
|
|
32
|
-
"@saasicat/types": "^0.
|
|
30
|
+
"@saasicat/nest": "^0.22.1",
|
|
31
|
+
"@saasicat/spec": "^0.22.1",
|
|
32
|
+
"@saasicat/types": "^0.22.1"
|
|
33
33
|
},
|
|
34
34
|
"peerDependencies": {
|
|
35
35
|
"@nestjs/common": "^11.0.0",
|