directus-template-cli 0.7.7 → 0.8.0-partials.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.
Files changed (52) hide show
  1. package/README.md +134 -37
  2. package/dist/commands/apply.d.ts +5 -0
  3. package/dist/commands/apply.js +32 -68
  4. package/dist/commands/extract.d.ts +30 -0
  5. package/dist/commands/extract.js +14 -5
  6. package/dist/lib/extract/expand-deep-plan.d.ts +2 -0
  7. package/dist/lib/extract/expand-deep-plan.js +54 -0
  8. package/dist/lib/extract/expand-schema-plan.d.ts +2 -0
  9. package/dist/lib/extract/expand-schema-plan.js +55 -0
  10. package/dist/lib/extract/extract-assets.js +19 -6
  11. package/dist/lib/extract/extract-collections.d.ts +2 -1
  12. package/dist/lib/extract/extract-collections.js +5 -2
  13. package/dist/lib/extract/extract-content.d.ts +2 -1
  14. package/dist/lib/extract/extract-content.js +105 -12
  15. package/dist/lib/extract/extract-fields.d.ts +2 -1
  16. package/dist/lib/extract/extract-fields.js +5 -5
  17. package/dist/lib/extract/extract-relations.d.ts +2 -1
  18. package/dist/lib/extract/extract-relations.js +6 -4
  19. package/dist/lib/extract/index.d.ts +2 -1
  20. package/dist/lib/extract/index.js +67 -29
  21. package/dist/lib/load/apply-flags.d.ts +5 -2
  22. package/dist/lib/load/apply-flags.js +0 -50
  23. package/dist/lib/load/finalize-collections.d.ts +2 -0
  24. package/dist/lib/load/finalize-collections.js +28 -0
  25. package/dist/lib/load/finalize-fields.d.ts +2 -0
  26. package/dist/lib/load/finalize-fields.js +25 -0
  27. package/dist/lib/load/index.js +36 -19
  28. package/dist/lib/load/load-collections.d.ts +2 -1
  29. package/dist/lib/load/load-collections.js +17 -30
  30. package/dist/lib/load/load-data.d.ts +2 -1
  31. package/dist/lib/load/load-data.js +46 -34
  32. package/dist/lib/load/load-files.js +8 -8
  33. package/dist/lib/load/load-relations.d.ts +2 -1
  34. package/dist/lib/load/load-relations.js +17 -7
  35. package/dist/lib/template-plan/collections.d.ts +4 -0
  36. package/dist/lib/template-plan/collections.js +26 -0
  37. package/dist/lib/template-plan/flags.d.ts +18 -0
  38. package/dist/lib/template-plan/flags.js +61 -0
  39. package/dist/lib/template-plan/index.d.ts +16 -0
  40. package/dist/lib/template-plan/index.js +77 -0
  41. package/dist/lib/template-plan/junctions.d.ts +10 -0
  42. package/dist/lib/template-plan/junctions.js +19 -0
  43. package/dist/lib/template-plan/metadata-plan.d.ts +2 -0
  44. package/dist/lib/template-plan/metadata-plan.js +33 -0
  45. package/dist/lib/template-plan/metadata.d.ts +5 -0
  46. package/dist/lib/template-plan/metadata.js +39 -0
  47. package/dist/lib/template-plan/types.d.ts +34 -0
  48. package/dist/lib/template-plan/types.js +1 -0
  49. package/oclif.manifest.json +173 -16
  50. package/package.json +1 -2
  51. package/dist/lib/load/update-required-fields.d.ts +0 -1
  52. package/dist/lib/load/update-required-fields.js +0 -20
package/README.md CHANGED
@@ -3,6 +3,7 @@
3
3
  A streamlined CLI tool for creating new Directus projects and managing Directus templates - making it easy to apply and extract template configurations across instances.
4
4
 
5
5
  This tool is best suited for:
6
+
6
7
  - Proof of Concept (POC) projects
7
8
  - Demo environments
8
9
  - New project setups
@@ -10,8 +11,9 @@ This tool is best suited for:
10
11
  ⚠️ We strongly recommend against using this tool in existing production environments or as a critical part of your CI/CD pipeline without thorough testing. Always create backups before applying templates.
11
12
 
12
13
  **Important Notes:**
14
+
13
15
  - **Primary Purpose**: Built to deploy templates created by the Directus Core Team. While community templates are supported, the unlimited possible configurations make comprehensive support challenging.
14
- - **Database Compatibility**: PostgreSQL is recommended. Applying templates that are extracted and applied between different databases (Extract from SQLite -> Apply to Postgres) can caused issues and is not recommended. MySQL users may encounter known issues.
16
+ - **Database Compatibility**: PostgreSQL is recommended. Applying templates that are extracted and applied between different databases (Extract from SQLite -> Apply to Postgres) can caused issues and is not recommended. MySQL users may encounter known issues.
15
17
  - **Performance**: Remote operations (extract/apply) are rate-limited to 10 requests/second using bottleneck. Processing time varies based on your instance size (collections, items, assets).
16
18
  - **Version Compatibility**:
17
19
  - v0.5.0+: Compatible with Directus 11 and up
@@ -30,6 +32,7 @@ npx directus-template-cli@latest init
30
32
  ```
31
33
 
32
34
  You'll be guided through:
35
+
33
36
  - Selecting a directory for your new project
34
37
  - Choosing a Directus backend template
35
38
  - Selecting a frontend framework (if available for the template)
@@ -51,7 +54,6 @@ npx directus-template-cli@latest init my-project --frontend=nextjs --template=cm
51
54
  npx directus-template-cli@latest init --template=https://github.com/directus-labs/starters/tree/main/cms
52
55
  ```
53
56
 
54
-
55
57
  Available flags:
56
58
 
57
59
  - `--frontend`: Frontend framework to use (e.g., nextjs, nuxt, astro)
@@ -112,6 +114,7 @@ The `directus:template` property contains:
112
114
  - Each frontend has a `name` (display name) and `path` (directory containing the frontend code)
113
115
 
114
116
  When you use this template with the `init` command, it will:
117
+
115
118
  1. Copy the Directus template files from the specified template directory
116
119
  2. Copy the selected frontend code based on your choice or the `--frontend` flag
117
120
  3. Set up the project structure with both backend and frontend integrated
@@ -133,12 +136,10 @@ npx directus-template-cli@latest apply
133
136
 
134
137
  You can choose from our community maintained templates or you can also choose a template from a local directory or a public GitHub repository.
135
138
 
136
-
137
139
  ### Programmatic Mode
138
140
 
139
141
  By default, the CLI will run in interactive mode. For CI/CD pipelines or automated scripts, you can use the programmatic mode:
140
142
 
141
-
142
143
  Using a token:
143
144
 
144
145
  ```
@@ -151,11 +152,10 @@ Using email/password:
151
152
  npx directus-template-cli@latest apply -p --directusUrl="http://localhost:8055" --userEmail="admin@example.com" --userPassword="admin" --templateLocation="./my-template" --templateType="local"
152
153
  ```
153
154
 
154
- Partial apply (apply only some of the parts of a template to the instance):
155
+ Partial apply (apply only some parts of a template):
155
156
 
156
157
  ```
157
- npx directus-template-cli@latest apply -p --directusUrl="http://localhost:8055" --userEmail="admin@example.com" --userPassword="your-password" --templateLocation="./my-template" --templateType="local" --partial --schema --permissions --no-content
158
-
158
+ npx directus-template-cli@latest apply -p --directusUrl="http://localhost:8055" --userEmail="admin@example.com" --userPassword="your-password" --templateLocation="./my-template" --templateType="local" --schema --permissions --no-content
159
159
  ```
160
160
 
161
161
  Available flags:
@@ -166,7 +166,7 @@ Available flags:
166
166
  - `--userPassword`: Password for Directus authentication (required if not using token)
167
167
  - `--templateLocation`: Location of the template to apply (required)
168
168
  - `--templateType`: Type of template to apply. Options: community, local, github. Defaults to `local`.
169
- - `--partial`: Enable partial template application
169
+ - `--partial`: Enable partial template mode explicitly. Component and collection flags also imply partial mode.
170
170
  - `--content`: Load Content (data)
171
171
  - `--dashboards`: Load Dashboards
172
172
  - `--extensions`: Load Extensions
@@ -176,6 +176,11 @@ Available flags:
176
176
  - `--schema`: Load Schema
177
177
  - `--settings`: Load Settings
178
178
  - `--users`: Load Users
179
+ - `--collections`: Only apply these comma-separated collections
180
+ - `--exclude-collections`: Exclude these comma-separated collections
181
+ - `--relation-strategy`: How to handle omitted relation targets. Options: `empty`, `preserve`, `deep`.
182
+ - `--allow-broken-relations`: Apply templates that intentionally preserve references to omitted records
183
+ - `--no-assets`: Shorthand for `--no-files` and excluding `directus_files`
179
184
  - `--disableTelemetry`: Disable telemetry collection
180
185
 
181
186
  When using `--partial`, you can also use `--no` flags to exclude specific components from being applied. For example:
@@ -191,34 +196,12 @@ This command will apply the template but exclude content and users. Available `-
191
196
  - `--no-extensions`: Skip loading Extensions
192
197
  - `--no-files`: Skip loading Files
193
198
  - `--no-flows`: Skip loading Flows
194
- - `--no-permissions`: Skip loading PermissionsI
199
+ - `--no-permissions`: Skip loading Permissions
195
200
  - `--no-schema`: Skip loading Schema
196
201
  - `--no-settings`: Skip loading Settings
197
202
  - `--no-users`: Skip loading Users
198
203
 
199
-
200
- #### Template Component Dependencies
201
-
202
- When applying templates, certain components have dependencies on others. Here are the key relationships to be aware of:
203
-
204
- - `--users`: Depends on `--permissions`. If you include users, permissions will automatically be included.
205
- - `--permissions`: Depends on `--schema`. If you include permissions, the schema will automatically be included.
206
- - `--content`: Depends on `--schema`. If you include content, the schema will automatically be included.
207
- - `--files`: No direct dependencies, but often related to content. Consider including `--content` if you're including files.
208
- - `--flows`: No direct dependencies, but may interact with other components. Consider your specific use case.
209
- - `--dashboards`: No direct dependencies, but often rely on data from other components.
210
- - `--extensions`: No direct dependencies, but may interact with other components.
211
- - `--settings`: No direct dependencies, but affects the overall system configuration.
212
-
213
- When using the `--partial` flag, keep these dependencies in mind. For example:
214
-
215
- ```
216
- npx directus-template-cli@latest apply -p --directusUrl="http://localhost:8055" --directusToken="admin-token-here" --templateLocation="./my-template" --templateType="local" --partial --users
217
- ```
218
-
219
- This command will automatically include `--permissions` and `--schema` along with `--users`, even if not explicitly specified.
220
-
221
- If you use `--no-` flags, be cautious about excluding dependencies. For instance, using `--no-schema` while including `--content` may lead to errors or incomplete application of the template.
204
+ For how partial templates handle schema, content, and relations, see [Partial Templates and Relation Strategies](#partial-templates-and-relation-strategies).
222
205
 
223
206
  #### Using Environment Variables
224
207
 
@@ -231,7 +214,6 @@ You can also pass flags as environment variables. This can be useful for CI/CD p
231
214
  - `TEMPLATE_LOCATION`: Equivalent to `--templateLocation`
232
215
  - `TEMPLATE_TYPE`: Equivalent to `--templateType`
233
216
 
234
-
235
217
  ### Existing Data
236
218
 
237
219
  You can apply a template to an existing Directus instance. This is nice because you can have smaller templates that you can "compose" for various use cases. The CLI tries to be smart about existing items in the target Directus instance. But mileage may vary depending on the size and complexity of the template and the existing instance.
@@ -254,7 +236,9 @@ For data in your own user-created collections, if an item has the same primary k
254
236
 
255
237
  The CLI can also extract a template from a Directus instance so that it can be applied to other instances.
256
238
 
257
- Note: We do not currently support partial extraction. The entire template will be extracted. We thought it better to have the data and not need it, than need it and not have it.
239
+ Full extraction remains the default. Partial extraction is available with component flags, collection filters, and relation strategies.
240
+
241
+ For how partial templates handle schema, content, and relations, see [Partial Templates and Relation Strategies](#partial-templates-and-relation-strategies).
258
242
 
259
243
  1. Make sure you remove any sensitive data from the Directus instance you don't want to include in the template.
260
244
  2. Login and create a Static Access Token for the admin user.
@@ -289,8 +273,45 @@ Available flags:
289
273
  - `--userPassword`: Password for Directus authentication (required if not using token)
290
274
  - `--templateLocation`: Directory to extract the template to (required)
291
275
  - `--templateName`: Name of the template (required)
276
+ - `--partial`: Enable partial template mode explicitly. Component and collection flags also imply partial mode.
277
+ - `--schema` / `--no-schema`: Include or skip schema
278
+ - `--content` / `--no-content`: Include or skip content
279
+ - `--files` / `--no-files`: Include or skip files and assets
280
+ - `--flows` / `--no-flows`: Include or skip flows
281
+ - `--dashboards` / `--no-dashboards`: Include or skip dashboards
282
+ - `--permissions` / `--no-permissions`: Include or skip permissions
283
+ - `--settings` / `--no-settings`: Include or skip settings
284
+ - `--extensions` / `--no-extensions`: Include or skip extensions
285
+ - `--users` / `--no-users`: Include or skip users
286
+ - `--collections`: Only extract these comma-separated collections
287
+ - `--exclude-collections`: Exclude these comma-separated collections
288
+ - `--relation-strategy`: How to handle omitted relation targets. Options: `empty`, `preserve`, `deep`.
289
+ - `--allow-broken-relations`: Mark intentionally incomplete relation references as allowed in metadata
290
+ - `--no-assets`: Shorthand for `--no-files` and excluding `directus_files`
292
291
  - `--disableTelemetry`: Disable telemetry collection
293
292
 
293
+ Examples:
294
+
295
+ `--collections` limits content scope. Schema may still include additional collections needed by the data model.
296
+
297
+ Skip assets safely:
298
+
299
+ ```
300
+ npx directus-template-cli@latest extract -p --templateName="My Template" --templateLocation="./my-template" --directusToken="admin-token-here" --directusUrl="http://localhost:8055" --schema --content --collections posts,pages --no-assets --relation-strategy empty
301
+ ```
302
+
303
+ Preserve asset IDs but do not export assets:
304
+
305
+ ```
306
+ npx directus-template-cli@latest extract -p --templateName="My Template" --templateLocation="./my-template" --directusToken="admin-token-here" --directusUrl="http://localhost:8055" --schema --content --collections posts,pages --no-assets --relation-strategy preserve --allow-broken-relations
307
+ ```
308
+
309
+ Portable partial snapshot:
310
+
311
+ ```
312
+ npx directus-template-cli@latest extract -p --templateName="My Template" --templateLocation="./my-template" --directusToken="admin-token-here" --directusUrl="http://localhost:8055" --schema --content --collections posts,pages --relation-strategy deep
313
+ ```
314
+
294
315
  #### Using Environment Variables
295
316
 
296
317
  Similar to the Apply command, you can use environment variables for the Extract command as well:
@@ -301,14 +322,90 @@ Similar to the Apply command, you can use environment variables for the Extract
301
322
  - `DIRECTUS_PASSWORD`: Equivalent to `--userPassword`
302
323
  - `TEMPLATE_LOCATION`: Equivalent to `--templateLocation`
303
324
 
325
+ ## Partial Templates and Relation Strategies
326
+
327
+ Partial templates can intentionally omit components or collections. The CLI writes and reads `src/template-meta.json` so apply can understand what was extracted.
328
+
329
+ Partial templates have two scopes:
330
+
331
+ - **Schema scope**: the data model needed to keep selected collections valid. This can include related, junction, translation, page-builder, and grouping collections even when no records are exported for them.
332
+ - **Content scope**: the records exported from collections requested with `--collections`, or records added by `deep`.
333
+
334
+ Example: extracting `pages,posts` may still include schema for `page_blocks`, block collections, translations, and groups. Only `pages.json` and `posts.json` are exported unless you use `deep`.
335
+
336
+ Relation strategies:
337
+
338
+ | Strategy | What it exports | What happens to omitted relations | Best for |
339
+ | ---------- | -------------------------- | --------------------------------------------------------------------------- | ------------------------------------------- |
340
+ | `empty` | Selected content only | M2O fields become `null`; alias/O2M/M2M/M2A fields are omitted from records | Clean subsets, skipping assets safely |
341
+ | `preserve` | Selected content only | Keeps IDs/arrays as-is and writes warnings | Targets where related records already exist |
342
+ | `deep` | Selected + related content | Keeps relation values and recursively exports related records | Portable partial snapshots |
343
+
344
+ Tradeoffs:
345
+
346
+ - `empty` produces directly applyable templates, but applying to an existing instance can clear omitted M2O references.
347
+ - `preserve` can intentionally leave references to records that are not in the template. Apply requires `--allow-broken-relations` when metadata reports these references.
348
+ - `deep` is the most portable, but can expand into many collections and export much more data.
349
+
350
+ For example, a source record might contain both a file relation and a page-builder alias field:
351
+
352
+ ```json
353
+ {
354
+ "id": "post-1",
355
+ "image": "file-uuid",
356
+ "blocks": ["block-1", "block-2"]
357
+ }
358
+ ```
359
+
360
+ With `--relation-strategy empty`, omitted relations are cleared or skipped:
361
+
362
+ ```json
363
+ {
364
+ "id": "post-1",
365
+ "image": null
366
+ }
367
+ ```
368
+
369
+ With `--relation-strategy preserve`, relation values stay in the record and metadata records warnings:
370
+
371
+ ```json
372
+ {
373
+ "id": "post-1",
374
+ "image": "file-uuid",
375
+ "blocks": ["block-1", "block-2"]
376
+ }
377
+ ```
378
+
379
+ With `--relation-strategy deep`, the record stays the same and related content collections are exported too:
380
+
381
+ ```jsonc
382
+ // content/posts.json
383
+ {
384
+ "id": "post-1",
385
+ "image": "file-uuid",
386
+ "blocks": ["block-1", "block-2"],
387
+ }
388
+
389
+ // also exported:
390
+ // content/page_blocks.json
391
+ // content/block_hero.json
392
+ ```
393
+
394
+ Skip assets safely:
395
+
396
+ ```
397
+ npx directus-template-cli@latest apply -p --directusUrl="http://localhost:8055" --directusToken="admin-token-here" --templateLocation="./my-template" --templateType="local" --content --collections posts,pages --no-assets --relation-strategy empty
398
+ ```
399
+
304
400
  ## Logs
305
401
 
306
402
  The Directus Template CLI logs information to a file in the `.directus-template-cli/logs` directory.
307
403
 
308
404
  Logs are automatically generated for each run of the CLI. Here's how the logging system works:
309
- - A new log file is created for each CLI run.
310
- - Log files are stored in the `.directus-template-cli/logs` directory within your current working directory.
311
- - Each log file is named `run-[timestamp].log`, where `[timestamp]` is the ISO timestamp of when the CLI was initiated.
405
+
406
+ - A new log file is created for each CLI run.
407
+ - Log files are stored in the `.directus-template-cli/logs` directory within your current working directory.
408
+ - Each log file is named `run-[timestamp].log`, where `[timestamp]` is the ISO timestamp of when the CLI was initiated.
312
409
 
313
410
  The logger automatically sanitizes sensitive information such as passwords, tokens, and keys before writing to the log file. But it may not catch everything. Just be aware of this and make sure to remove the log files when they are no longer needed.
314
411
 
@@ -3,18 +3,23 @@ export default class ApplyCommand extends BaseCommand {
3
3
  static description: string;
4
4
  static examples: string[];
5
5
  static flags: {
6
+ allowBrokenRelations: import("@oclif/core/interfaces").BooleanFlag<boolean>;
7
+ collections: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
6
8
  content: import("@oclif/core/interfaces").BooleanFlag<boolean>;
7
9
  dashboards: import("@oclif/core/interfaces").BooleanFlag<boolean>;
8
10
  directusToken: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
9
11
  directusUrl: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
10
12
  disableTelemetry: import("@oclif/core/interfaces").BooleanFlag<boolean>;
13
+ excludeCollections: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
11
14
  extensions: import("@oclif/core/interfaces").BooleanFlag<boolean>;
12
15
  files: import("@oclif/core/interfaces").BooleanFlag<boolean>;
13
16
  flows: import("@oclif/core/interfaces").BooleanFlag<boolean>;
17
+ noAssets: import("@oclif/core/interfaces").BooleanFlag<boolean>;
14
18
  noExit: import("@oclif/core/interfaces").BooleanFlag<boolean>;
15
19
  partial: import("@oclif/core/interfaces").BooleanFlag<boolean>;
16
20
  permissions: import("@oclif/core/interfaces").BooleanFlag<boolean>;
17
21
  programmatic: import("@oclif/core/interfaces").BooleanFlag<boolean>;
22
+ relationStrategy: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
18
23
  schema: import("@oclif/core/interfaces").BooleanFlag<boolean>;
19
24
  settings: import("@oclif/core/interfaces").BooleanFlag<boolean>;
20
25
  templateLocation: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
@@ -3,13 +3,14 @@ import { Flags, ux } from '@oclif/core';
3
3
  import chalk from 'chalk';
4
4
  import * as path from 'pathe';
5
5
  import * as customFlags from '../flags/common.js';
6
- import { BSL_LICENSE_CTA, BSL_LICENSE_HEADLINE, BSL_LICENSE_TEXT, DIRECTUS_PINK, DIRECTUS_PURPLE, SEPARATOR } from '../lib/constants.js';
7
- import { validateInteractiveFlags, validateProgrammaticFlags } from '../lib/load/apply-flags.js';
6
+ import { BSL_LICENSE_CTA, BSL_LICENSE_HEADLINE, BSL_LICENSE_TEXT, DIRECTUS_PINK, DIRECTUS_PURPLE, SEPARATOR, } from '../lib/constants.js';
7
+ import { validateProgrammaticFlags } from '../lib/load/apply-flags.js';
8
8
  import apply from '../lib/load/index.js';
9
+ import * as templatePlanFlags from '../lib/template-plan/flags.js';
9
10
  import { animatedBunny } from '../lib/utils/animated-bunny.js';
10
- import { getDirectusEmailAndPassword, getDirectusToken, getDirectusUrl, initializeDirectusApi } from '../lib/utils/auth.js';
11
+ import { getDirectusEmailAndPassword, getDirectusToken, getDirectusUrl, initializeDirectusApi, } from '../lib/utils/auth.js';
11
12
  import catchError from '../lib/utils/catch-error.js';
12
- import { getCommunityTemplates, getGithubTemplate, getInteractiveLocalTemplate, getLocalTemplate } from '../lib/utils/get-template.js';
13
+ import { getCommunityTemplates, getGithubTemplate, getInteractiveLocalTemplate, getLocalTemplate, } from '../lib/utils/get-template.js';
13
14
  import { logger } from '../lib/utils/logger.js';
14
15
  import openUrl from '../lib/utils/open-url.js';
15
16
  import { shutdown, track } from '../services/posthog.js';
@@ -22,60 +23,28 @@ export default class ApplyCommand extends BaseCommand {
22
23
  '$ directus-template-cli@beta apply -p --directusUrl="http://localhost:8055" --directusToken="admin-token-here" --templateLocation="./my-template" --templateType="local" --partial --no-content --no-users',
23
24
  ];
24
25
  static flags = {
25
- content: Flags.boolean({
26
- allowNo: true,
27
- default: undefined,
28
- description: 'Load Content (data)',
29
- }),
30
- dashboards: Flags.boolean({
31
- allowNo: true,
32
- default: undefined,
33
- description: 'Load Dashboards (dashboards, panels)',
34
- }),
26
+ allowBrokenRelations: templatePlanFlags.allowBrokenRelations,
27
+ collections: templatePlanFlags.collections,
28
+ content: templatePlanFlags.componentFlags.content,
29
+ dashboards: templatePlanFlags.componentFlags.dashboards,
35
30
  directusToken: customFlags.directusToken,
36
31
  directusUrl: customFlags.directusUrl,
37
32
  disableTelemetry: customFlags.disableTelemetry,
38
- extensions: Flags.boolean({
39
- allowNo: true,
40
- default: undefined,
41
- description: 'Load Extensions',
42
- }),
43
- files: Flags.boolean({
44
- allowNo: true,
45
- default: undefined,
46
- description: 'Load Files (files, folders)',
47
- }),
48
- flows: Flags.boolean({
49
- allowNo: true,
50
- default: undefined,
51
- description: 'Load Flows (operations, flows)',
52
- }),
33
+ excludeCollections: templatePlanFlags.excludeCollections,
34
+ extensions: templatePlanFlags.componentFlags.extensions,
35
+ files: templatePlanFlags.componentFlags.files,
36
+ flows: templatePlanFlags.componentFlags.flows,
37
+ noAssets: templatePlanFlags.noAssets,
53
38
  noExit: Flags.boolean({
54
39
  default: false,
55
40
  hidden: true,
56
41
  }),
57
- partial: Flags.boolean({
58
- dependsOn: ['programmatic'],
59
- description: 'Enable partial template application (all components enabled by default)',
60
- summary: 'Enable partial template application',
61
- }),
62
- permissions: Flags.boolean({
63
- allowNo: true,
64
- default: undefined,
65
- description: 'Loads permissions data. Collections include: directus_roles, directus_policies, directus_access, directus_permissions.',
66
- summary: 'Load permissions (roles, policies, access, permissions)',
67
- }),
42
+ partial: templatePlanFlags.partial,
43
+ permissions: templatePlanFlags.componentFlags.permissions,
68
44
  programmatic: customFlags.programmatic,
69
- schema: Flags.boolean({
70
- allowNo: true,
71
- default: undefined,
72
- description: 'Load schema (collections, relations)',
73
- }),
74
- settings: Flags.boolean({
75
- allowNo: true,
76
- default: undefined,
77
- description: 'Load settings (project settings, translations, presets)',
78
- }),
45
+ relationStrategy: templatePlanFlags.relationStrategy,
46
+ schema: templatePlanFlags.componentFlags.schema,
47
+ settings: templatePlanFlags.componentFlags.settings,
79
48
  templateLocation: customFlags.templateLocation,
80
49
  templateType: Flags.string({
81
50
  default: 'local',
@@ -87,11 +56,7 @@ export default class ApplyCommand extends BaseCommand {
87
56
  }),
88
57
  userEmail: customFlags.userEmail,
89
58
  userPassword: customFlags.userPassword,
90
- users: Flags.boolean({
91
- allowNo: true,
92
- default: undefined,
93
- description: 'Load users',
94
- }),
59
+ users: templatePlanFlags.componentFlags.users,
95
60
  };
96
61
  /**
97
62
  * MAIN
@@ -110,9 +75,9 @@ export default class ApplyCommand extends BaseCommand {
110
75
  * @returns {Promise<void>} - Returns nothing
111
76
  */
112
77
  async runInteractive(flags) {
113
- const validatedFlags = validateInteractiveFlags(flags);
78
+ const validatedFlags = flags;
114
79
  // Show animated intro
115
- await animatedBunny('Let\'s apply a template!');
80
+ await animatedBunny("Let's apply a template!");
116
81
  intro(`${chalk.bgHex(DIRECTUS_PURPLE).white.bold('Directus Template CLI')} - Apply Template`);
117
82
  const templateType = await select({
118
83
  message: 'What type of template would you like to apply?',
@@ -129,11 +94,18 @@ export default class ApplyCommand extends BaseCommand {
129
94
  const templates = await getCommunityTemplates();
130
95
  const selectedTemplate = await select({
131
96
  message: 'Select a template.',
132
- options: templates.map(t => ({ label: t.templateName, value: t })),
97
+ options: templates.map((t) => ({ label: t.templateName, value: t })),
133
98
  });
134
99
  template = selectedTemplate;
135
100
  break;
136
101
  }
102
+ case 'directus-plus': {
103
+ openUrl('https://directus.io/plus?utm_source=directus-template-cli&utm_content=apply-command');
104
+ log.info('Redirecting to Directus website.');
105
+ if (!validatedFlags.noExit)
106
+ process.exit(0);
107
+ return;
108
+ }
137
109
  case 'github': {
138
110
  const ghTemplateUrl = await text({
139
111
  message: 'What is the public GitHub repository URL?',
@@ -148,13 +120,6 @@ export default class ApplyCommand extends BaseCommand {
148
120
  template = await this.selectLocalTemplate(localTemplateDir);
149
121
  break;
150
122
  }
151
- case 'directus-plus': {
152
- openUrl('https://directus.io/plus?utm_source=directus-template-cli&utm_content=apply-command');
153
- log.info('Redirecting to Directus website.');
154
- if (!validatedFlags.noExit)
155
- process.exit(0);
156
- return;
157
- }
158
123
  }
159
124
  log.info(`You selected ${ux.colorize(DIRECTUS_PINK, template.templateName)}`);
160
125
  // Get Directus URL
@@ -222,7 +187,6 @@ export default class ApplyCommand extends BaseCommand {
222
187
  ux.stdout('Template applied successfully.');
223
188
  if (!validatedFlags.noExit)
224
189
  process.exit(0);
225
- return;
226
190
  }
227
191
  }
228
192
  /**
@@ -237,7 +201,7 @@ export default class ApplyCommand extends BaseCommand {
237
201
  switch (validatedFlags.templateType) {
238
202
  case 'community': {
239
203
  const templates = await getCommunityTemplates();
240
- template = templates.find(t => t.templateName === validatedFlags.templateLocation) || templates[0];
204
+ template = templates.find((t) => t.templateName === validatedFlags.templateLocation) || templates[0];
241
205
  break;
242
206
  }
243
207
  case 'github': {
@@ -315,7 +279,7 @@ export default class ApplyCommand extends BaseCommand {
315
279
  }
316
280
  const selectedTemplate = await select({
317
281
  message: 'Multiple templates found. Please select one:',
318
- options: templates.map(t => ({
282
+ options: templates.map((t) => ({
319
283
  label: `${t.templateName} (${path.basename(t.directoryPath)})`,
320
284
  value: t,
321
285
  })),
@@ -1,21 +1,51 @@
1
1
  import { BaseCommand } from './base.js';
2
2
  export interface ExtractFlags {
3
+ allowBrokenRelations?: boolean;
4
+ collections?: string;
5
+ content?: boolean;
6
+ dashboards?: boolean;
3
7
  directusToken: string;
4
8
  directusUrl: string;
5
9
  disableTelemetry?: boolean;
10
+ excludeCollections?: string;
11
+ extensions?: boolean;
12
+ files?: boolean;
13
+ flows?: boolean;
14
+ noAssets?: boolean;
15
+ partial?: boolean;
16
+ permissions?: boolean;
6
17
  programmatic: boolean;
18
+ relationStrategy?: 'deep' | 'empty' | 'preserve';
19
+ schema?: boolean;
20
+ settings?: boolean;
7
21
  templateLocation: string;
8
22
  templateName: string;
9
23
  userEmail: string;
10
24
  userPassword: string;
25
+ users?: boolean;
11
26
  }
12
27
  export default class ExtractCommand extends BaseCommand {
13
28
  static description: string;
14
29
  static examples: string[];
15
30
  static flags: {
31
+ allowBrokenRelations: import("@oclif/core/interfaces").BooleanFlag<boolean>;
32
+ collections: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
33
+ excludeCollections: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
34
+ noAssets: import("@oclif/core/interfaces").BooleanFlag<boolean>;
35
+ relationStrategy: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
36
+ content: import("@oclif/core/interfaces").BooleanFlag<boolean>;
37
+ dashboards: import("@oclif/core/interfaces").BooleanFlag<boolean>;
38
+ extensions: import("@oclif/core/interfaces").BooleanFlag<boolean>;
39
+ files: import("@oclif/core/interfaces").BooleanFlag<boolean>;
40
+ flows: import("@oclif/core/interfaces").BooleanFlag<boolean>;
41
+ permissions: import("@oclif/core/interfaces").BooleanFlag<boolean>;
42
+ schema: import("@oclif/core/interfaces").BooleanFlag<boolean>;
43
+ settings: import("@oclif/core/interfaces").BooleanFlag<boolean>;
44
+ users: import("@oclif/core/interfaces").BooleanFlag<boolean>;
16
45
  directusToken: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
17
46
  directusUrl: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
18
47
  disableTelemetry: import("@oclif/core/interfaces").BooleanFlag<boolean>;
48
+ partial: import("@oclif/core/interfaces").BooleanFlag<boolean>;
19
49
  programmatic: import("@oclif/core/interfaces").BooleanFlag<boolean>;
20
50
  templateLocation: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
21
51
  templateName: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
@@ -5,12 +5,14 @@ import chalk from 'chalk';
5
5
  import fs from 'node:fs';
6
6
  import path from 'pathe';
7
7
  import * as customFlags from '../flags/common.js';
8
- import { BSL_LICENSE_CTA, BSL_LICENSE_HEADLINE, BSL_LICENSE_TEXT, DIRECTUS_PINK, DIRECTUS_PURPLE, SEPARATOR } from '../lib/constants.js';
8
+ import { BSL_LICENSE_CTA, BSL_LICENSE_HEADLINE, BSL_LICENSE_TEXT, DIRECTUS_PINK, DIRECTUS_PURPLE, SEPARATOR, } from '../lib/constants.js';
9
9
  import extract from '../lib/extract/index.js';
10
+ import * as templatePlanFlags from '../lib/template-plan/flags.js';
11
+ import { buildTemplatePlan } from '../lib/template-plan/index.js';
10
12
  import { animatedBunny } from '../lib/utils/animated-bunny.js';
11
- import { getDirectusEmailAndPassword, getDirectusToken, getDirectusUrl, initializeDirectusApi, validateAuthFlags } from '../lib/utils/auth.js';
13
+ import { getDirectusEmailAndPassword, getDirectusToken, getDirectusUrl, initializeDirectusApi, validateAuthFlags, } from '../lib/utils/auth.js';
12
14
  import catchError from '../lib/utils/catch-error.js';
13
- import { generatePackageJsonContent, generateReadmeContent, } from '../lib/utils/template-defaults.js';
15
+ import { generatePackageJsonContent, generateReadmeContent } from '../lib/utils/template-defaults.js';
14
16
  import { shutdown, track } from '../services/posthog.js';
15
17
  import { BaseCommand } from './base.js';
16
18
  export default class ExtractCommand extends BaseCommand {
@@ -23,11 +25,18 @@ export default class ExtractCommand extends BaseCommand {
23
25
  directusToken: customFlags.directusToken,
24
26
  directusUrl: customFlags.directusUrl,
25
27
  disableTelemetry: customFlags.disableTelemetry,
28
+ partial: templatePlanFlags.partial,
26
29
  programmatic: customFlags.programmatic,
27
30
  templateLocation: customFlags.templateLocation,
28
31
  templateName: customFlags.templateName,
29
32
  userEmail: customFlags.userEmail,
30
33
  userPassword: customFlags.userPassword,
34
+ ...templatePlanFlags.componentFlags,
35
+ allowBrokenRelations: templatePlanFlags.allowBrokenRelations,
36
+ collections: templatePlanFlags.collections,
37
+ excludeCollections: templatePlanFlags.excludeCollections,
38
+ noAssets: templatePlanFlags.noAssets,
39
+ relationStrategy: templatePlanFlags.relationStrategy,
31
40
  };
32
41
  /**
33
42
  * Main run method for the ExtractCommand
@@ -82,7 +91,7 @@ export default class ExtractCommand extends BaseCommand {
82
91
  }
83
92
  ux.stdout(SEPARATOR);
84
93
  ux.action.start(`Extracting template - ${ux.colorize(DIRECTUS_PINK, templateName)} from ${ux.colorize(DIRECTUS_PINK, flags.directusUrl)} to ${ux.colorize(DIRECTUS_PINK, directory)}`);
85
- await extract(directory);
94
+ await extract(directory, buildTemplatePlan(flags));
86
95
  ux.action.stop();
87
96
  // Track completion before final messages/exit
88
97
  if (!flags.disableTelemetry) {
@@ -114,7 +123,7 @@ export default class ExtractCommand extends BaseCommand {
114
123
  * @returns {Promise<void>} - Returns nothing
115
124
  */
116
125
  async runInteractive(flags) {
117
- await animatedBunny('Let\'s extract a template!');
126
+ await animatedBunny("Let's extract a template!");
118
127
  intro(`${chalk.bgHex(DIRECTUS_PURPLE).white.bold('Directus Template CLI')} - Extract Template`);
119
128
  const templateName = await text({
120
129
  message: 'What is the name of the template you would like to extract?',
@@ -0,0 +1,2 @@
1
+ import { type TemplatePlan } from '../template-plan/index.js';
2
+ export declare function expandDeepPlan(plan: TemplatePlan): Promise<TemplatePlan>;
@@ -0,0 +1,54 @@
1
+ import { readCollections, readRelations } from '@directus/sdk';
2
+ import { ux } from '@oclif/core';
3
+ import { api } from '../sdk.js';
4
+ import { includesCollection } from '../template-plan/index.js';
5
+ export async function expandDeepPlan(plan) {
6
+ if (!plan.partial || plan.relationStrategy !== 'deep' || !plan.collections)
7
+ return plan;
8
+ const collections = await api.client.request(readCollections());
9
+ const availableCollections = collections
10
+ .filter((collection) => !collection.collection.startsWith('directus_', 0))
11
+ .filter((collection) => collection.schema !== null)
12
+ .map((collection) => collection.collection)
13
+ .filter((collection) => includesCollection(collection, { ...plan, collections: undefined }));
14
+ const available = new Set(availableCollections);
15
+ const missingCollections = plan.collections.filter((collection) => !available.has(collection));
16
+ if (missingCollections.length > 0) {
17
+ ux.warn(`Requested collections not found or excluded: ${missingCollections.join(', ')}`);
18
+ }
19
+ const selected = new Set(plan.collections.filter((collection) => available.has(collection)));
20
+ const relations = (await api.client.request(readRelations()));
21
+ let changed = true;
22
+ while (changed) {
23
+ changed = false;
24
+ const candidates = [];
25
+ for (const relation of relations) {
26
+ if (selected.has(relation.collection) && relation.related_collection) {
27
+ candidates.push(relation.related_collection);
28
+ }
29
+ if (relation.related_collection && selected.has(relation.related_collection)) {
30
+ candidates.push(relation.collection);
31
+ }
32
+ if (selected.has(relation.collection) && relation.meta?.one_allowed_collections) {
33
+ candidates.push(...relation.meta.one_allowed_collections);
34
+ }
35
+ }
36
+ for (const collection of candidates) {
37
+ if (!available.has(collection))
38
+ continue;
39
+ if (selected.has(collection))
40
+ continue;
41
+ selected.add(collection);
42
+ changed = true;
43
+ }
44
+ }
45
+ const expandedCollections = [...selected];
46
+ const addedCollections = expandedCollections.filter((collection) => !plan.collections?.includes(collection));
47
+ if (addedCollections.length > 0) {
48
+ ux.warn(`Deep relation strategy expanded collections: ${addedCollections.join(', ')}`);
49
+ }
50
+ return {
51
+ ...plan,
52
+ collections: expandedCollections,
53
+ };
54
+ }