trmnl_preview 0.9.0 → 0.11.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cbbc567f2937e7dd1aabc75991df616728f88fa858d797384a4a63453265be5c
4
- data.tar.gz: 1b1e543aa9ea1dddbb456f061f1538c22cc0edd849fc6faced3d67fa5f682677
3
+ metadata.gz: bdc22b090a3269c3fdbfa52287c287cbab548e6f40aa08428d037903f40dbb5b
4
+ data.tar.gz: 92886e598aa5e83e750bee7fdba8ee1b9d8b5f13fb6c021e2653cfa169900c44
5
5
  SHA512:
6
- metadata.gz: 7b0ab9feab22239ce5e2eaa6c9b2fc7d1f889927e267c14d83eb6ec02e60a973acb9194fee59cae709ea7640c43741186788588b33e9d76ac023a5d5f09280d4
7
- data.tar.gz: c18a319c45dec945b5c8f71d56c47f4478b8a82bceaed429b475cce4f57cb8a29541bcf7b1312e8fb9624513aebbd434d05c6e64f02d32322e9628cffc08553f
6
+ metadata.gz: d4d8ef476cd8a5f52d49b546eccc28e6e75277cd881d01667ea137c069c15b8a3a93a5029a855ae10ade990f8c8a5ecc61383e7b7f1c5922f0cf71babdf203df
7
+ data.tar.gz: 8b1dfa0578b38e11a317bb47ff3f2985e27236f5199a248431a49acc2f76bd88ed8bac2c6980ab066d19fd3f931ca0f122f8b19501899f2e0e38182cace55f3c
data/CHANGELOG.md CHANGED
@@ -1,6 +1,16 @@
1
1
 
2
2
  # Changelog
3
3
 
4
+ ## 0.11.0
5
+
6
+ - `framework_version:` now resolves against the release manifest published by the design system, not only the version list shipped inside the gem, so a framework version released after your trmnlp install can be pinned without upgrading. The manifest is read once per run with a 2 second timeout. If the request fails, or the response is not a version list, the copy bundled in the gem is used and rendering continues. That bundled copy is refreshed here too: `latest` moves from 3.1.1 to 3.2.0.
7
+ - `rake framework:sync` reads the published manifest by default. Pass a local design-system checkout (`rake framework:sync[/path/to/repo]`) for the previous behaviour.
8
+
9
+ ## 0.10.0
10
+
11
+ - Added support for the `description` plugin setting, an optional one-line summary of the plugin. `trmnlp init` scaffolds the key in `src/settings.yml`, `trmnlp lint` reports descriptions longer than 35 characters (the hosted service's limit), and `trmnlp list` shows a DESCRIPTION column when any plugin has one. Only the length is checked. Storing the value needs the matching hosted service release; until then `trmnlp push` drops it, because it rewrites the local `settings.yml` from the server's response.
12
+ - `trmnlp push` now offers to delete the server-side transform script when the project has no local `transform.*` file. Previously, deleting the file locally was a silent no-op: the server kept executing its copy and every `trmnlp pull` re-created the file. Answering yes sends `serverless_language: none` in the pushed settings.yml, which the server consumes as an explicit removal request. `--force` pushes skip the question and behave exactly as before; a failed server check never blocks the push.
13
+
4
14
  ## 0.9.0
5
15
 
6
16
  - Added OAuth2 support for local plugin preview (beta). Configure a provider with the flat `oauth_*` keys in `src/settings.yml` (authorize and token URLs, scopes, optional PKCE), which round-trip through `trmnlp push` and `pull`. Set `TRMNL_OAUTH_CLIENT_ID` and `TRMNL_OAUTH_CLIENT_SECRET` in your environment (credentials stay local and are never synced), and register `http://localhost:4567/oauth/callback` as the redirect URI in your OAuth app. `trmnlp serve` shows a Connect banner that runs the authorization code flow in the browser, stores the tokens in the cache directory (never in the project), and refreshes them before they expire. The access token is exposed to polling templates as `{{ oauth_access_token }}`, matching the hosted service. This is a beta feature; please report incorrect behaviour at https://github.com/usetrmnl/trmnlp/issues.
data/README.md CHANGED
@@ -427,7 +427,9 @@ A complete worked example lives at [`examples/hn-stories/`](examples/hn-stories/
427
427
 
428
428
  The `settings.yml` file is part of the plugin definition, and is uploaded and downloaded by `trmnlp push` / `pull`.
429
429
 
430
- `framework_version:` pins the [TRMNL Design System](https://trmnl.com/framework) version this plugin renders against — `latest` (the default) tracks the newest release, or set a specific version for reproducibility. It lives here rather than in `.trmnlp.yml` so the value round-trips with the hosted plugin service.
430
+ `framework_version:` pins the [TRMNL Design System](https://trmnl.com/framework) version this plugin renders against — `latest` (the default) tracks the newest release, or set a specific version for reproducibility. It lives here rather than in `.trmnlp.yml` so the value round-trips with the hosted plugin service. The list of pinnable versions is read from the [design system's published manifest](https://github.com/usetrmnl/trmnl-framework) once per run, so a newly released version can be pinned without upgrading trmnlp. If that request fails, the copy bundled in the gem is used.
431
+
432
+ `description:` is an optional one-line summary of the plugin, up to 35 characters. `trmnlp lint` reports anything longer, and `trmnlp list` shows it next to each plugin name.
431
433
 
432
434
  See [TRMNL documentation](https://help.trmnl.com/en/articles/10542599-importing-and-exporting-private-plugins#h_581fb988f0) for details on this file's contents.
433
435
 
@@ -1,6 +1,6 @@
1
1
  # Mirrored from the TRMNL design-system source.
2
2
  # Refresh with `rake framework:sync` — do not edit manually.
3
- latest: 3.1.1
3
+ latest: 3.2.0
4
4
  versions:
5
5
  - number: 0.0.1
6
6
  released_at: '2024-06-22'
@@ -70,3 +70,19 @@ versions:
70
70
  released_at: '2026-04-29'
71
71
  - number: 3.1.1
72
72
  released_at: '2026-05-01'
73
+ - number: 3.1.2
74
+ released_at: '2026-07-09'
75
+ - number: 3.1.3
76
+ released_at: '2026-07-14'
77
+ - number: 3.1.4
78
+ released_at: '2026-07-15'
79
+ - number: 3.1.5
80
+ released_at: '2026-07-16'
81
+ - number: 3.1.6
82
+ released_at: '2026-07-17'
83
+ - number: 3.1.7
84
+ released_at: '2026-07-18'
85
+ - number: 3.1.8
86
+ released_at: '2026-07-23'
87
+ - number: 3.2.0
88
+ released_at: '2026-08-03'
@@ -9,6 +9,8 @@ module TRMNLP
9
9
  Options = Data.define(:dir, :quiet)
10
10
 
11
11
  PRIVATE_PLUGIN_ID = 37
12
+ ID_WIDTH = 8
13
+ RULE_WIDTH = 50
12
14
 
13
15
  def call
14
16
  authenticate!
@@ -25,15 +27,55 @@ module TRMNLP
25
27
  end
26
28
 
27
29
  reporter.info "Your plugins:\n\n"
28
- reporter.info ' ID NAME'
29
- reporter.info " #{'-' * 50}"
30
+ report_table(plugins)
31
+
32
+ reporter.info "\nTo clone a plugin:"
33
+ reporter.info ' trmnlp clone [folder_name] [id]'
34
+ end
35
+
36
+ private
37
+
38
+ def report_table(plugins)
39
+ return report_names(plugins) if plugins.none? { |plugin| description(plugin) }
40
+
41
+ report_names_and_descriptions(plugins)
42
+ end
43
+
44
+ def report_names(plugins)
45
+ ids = id_width(plugins)
46
+ report_header format("%-#{ids}s %s", 'ID', 'NAME')
30
47
 
31
48
  plugins.each do |plugin|
32
- reporter.info format(' %-8s %s', plugin['id'], plugin['name'])
49
+ reporter.info format(" %-#{ids}s %s", plugin['id'], plugin['name'])
33
50
  end
51
+ end
34
52
 
35
- reporter.info "\nTo clone a plugin:"
36
- reporter.info ' trmnlp clone [folder_name] [id]'
53
+ def report_names_and_descriptions(plugins)
54
+ ids = id_width(plugins)
55
+ names = column_width(plugins.map { |plugin| plugin['name'] }, 'NAME'.length)
56
+ report_header format("%-#{ids}s %-#{names}s %s", 'ID', 'NAME', 'DESCRIPTION')
57
+
58
+ plugins.each do |plugin|
59
+ row = format(" %-#{ids}s %-#{names}s %s", plugin['id'], plugin['name'], description(plugin))
60
+ reporter.info row.rstrip
61
+ end
62
+ end
63
+
64
+ # BYOS servers return a UUID for plugins created there and the original
65
+ # number for plugins imported from the hosted service, so one response
66
+ # can hold both widths.
67
+ def id_width(plugins) = column_width(plugins.map { |plugin| plugin['id'] }, ID_WIDTH)
68
+
69
+ def column_width(values, minimum) = [*values.map { |value| value.to_s.length }, minimum].max
70
+
71
+ def report_header(text)
72
+ reporter.info " #{text}"
73
+ reporter.info " #{'-' * [text.length, RULE_WIDTH].max}"
74
+ end
75
+
76
+ def description(plugin)
77
+ value = plugin['description'].to_s.strip
78
+ value unless value.empty?
37
79
  end
38
80
  end
39
81
  end
@@ -38,11 +38,8 @@ module TRMNLP
38
38
  raise Aborted, 'aborting' unless %w[y yes].include?(answer)
39
39
  end
40
40
 
41
- Zip::File.open(ZIP_PATH, create: true) do |zip_file|
42
- paths.src_files.each do |file|
43
- zip_file.add(File.basename(file), file)
44
- end
45
- end
41
+ removal_requested = !is_new && confirm_server_transform_removal?(api, plugin_settings_id)
42
+ build_archive(removal_requested)
46
43
 
47
44
  response = api.post_plugin_setting_archive(plugin_settings_id, ZIP_PATH)
48
45
  paths.plugin_config.write(response.dig('data', 'settings_yaml'))
@@ -72,6 +69,51 @@ module TRMNLP
72
69
  ensure
73
70
  FileUtils.rm_f(ZIP_PATH)
74
71
  end
72
+
73
+ private
74
+
75
+ def build_archive(removal_requested)
76
+ Zip::File.open(ZIP_PATH, create: true) do |zip_file|
77
+ paths.src_files.each do |file|
78
+ next if removal_requested && file == paths.plugin_config
79
+
80
+ zip_file.add(File.basename(file), file)
81
+ end
82
+ if removal_requested
83
+ zip_file.get_output_stream('settings.yml') { |entry| entry.write(settings_yaml_with_removal_request) }
84
+ end
85
+ end
86
+ end
87
+
88
+ # Without this, deleting a local transform.* is a silent no-op: the server
89
+ # keeps executing its copy and every pull re-creates the file. The pushed
90
+ # settings.yml carries `serverless_language: none`, which the importer
91
+ # consumes as an explicit removal request.
92
+ def confirm_server_transform_removal?(api, plugin_settings_id)
93
+ return false if options.force
94
+ return false if paths.transform_file.first
95
+ return false unless server_transform?(api, plugin_settings_id)
96
+
97
+ question = 'The server has a transform script, but this project has no transform file. ' \
98
+ 'Delete it on the server too? (y/N) '
99
+ %w[y yes].include?(prompt(question).downcase)
100
+ end
101
+
102
+ def server_transform?(api, plugin_settings_id)
103
+ archive = api.get_plugin_setting_archive(plugin_settings_id)
104
+ begin
105
+ Zip::File.open(archive.path) { |zip_file| zip_file.any? { |entry| entry.name.start_with?('transform.') } }
106
+ ensure
107
+ archive.close
108
+ end
109
+ rescue Error
110
+ # Advisory check only — an unreadable archive must not block the push.
111
+ false
112
+ end
113
+
114
+ def settings_yaml_with_removal_request
115
+ config.plugin.settings.merge('serverless_language' => 'none').to_yaml
116
+ end
75
117
  end
76
118
  end
77
119
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'date'
4
+ require 'faraday'
3
5
  require 'yaml'
4
6
 
5
7
  module TRMNLP
@@ -11,10 +13,31 @@ module TRMNLP
11
13
 
12
14
  DEFAULT_ASSET_HOST = 'https://trmnl.com'
13
15
  DATA_PATH = File.expand_path('../../db/data/framework_versions.yml', __dir__)
16
+ # DATA_PATH holds a copy of this file, refreshed by `rake framework:sync`.
17
+ MANIFEST_URL = 'https://raw.githubusercontent.com/usetrmnl/trmnl-framework/main/db/data/framework_versions.yml'
18
+ MANIFEST_TIMEOUT = 2
14
19
 
15
20
  attr_reader :number
16
21
 
17
- def self.config = @config ||= YAML.load_file(DATA_PATH).freeze
22
+ def self.config = @config ||= (remote_config || YAML.load_file(DATA_PATH)).freeze
23
+
24
+ # Answers nil on any failure, so rendering never depends on the network.
25
+ # The shape check rejects error pages, which are also valid YAML.
26
+ def self.remote_config
27
+ response = Faraday.get(MANIFEST_URL) do |request|
28
+ request.options.open_timeout = MANIFEST_TIMEOUT
29
+ request.options.timeout = MANIFEST_TIMEOUT
30
+ end
31
+ return unless response.success?
32
+
33
+ # Release dates are quoted today, but the manifest is generated
34
+ # upstream and an unquoted date would otherwise be rejected here.
35
+ manifest = YAML.safe_load(response.body, permitted_classes: [Date])
36
+ manifest if manifest.is_a?(Hash) && manifest.key?('latest') && manifest.key?('versions')
37
+ rescue StandardError
38
+ nil
39
+ end
40
+ private_class_method :remote_config
18
41
 
19
42
  def self.version_numbers = config.fetch('versions').map { |v| v['number'] }.freeze
20
43
 
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../check'
4
+
5
+ module TRMNLP
6
+ module Lint
7
+ module Checks
8
+ # Matches the hosted service's limit. There is no upstream file to sync
9
+ # from, so update this by hand when that limit changes, the same way
10
+ # db/data/form_fields.yml is kept current.
11
+ class DescriptionLength < Check
12
+ MAX_LENGTH = 35
13
+ MESSAGE = "Description should be <= #{MAX_LENGTH} characters long.".freeze
14
+
15
+ private
16
+
17
+ def pass? = source.plugin_description.length <= MAX_LENGTH
18
+ end
19
+ end
20
+ end
21
+ end
@@ -16,6 +16,7 @@ module TRMNLP
16
16
  end
17
17
 
18
18
  def plugin_name = settings['name'].to_s
19
+ def plugin_description = settings['description'].to_s
19
20
  def settings = config.plugin.settings
20
21
  def custom_field_values = config.project.custom_fields
21
22
  def custom_field_definitions = config.plugin.custom_field_definitions
data/lib/trmnlp/lint.rb CHANGED
@@ -4,6 +4,7 @@ require_relative 'lint/check'
4
4
  require_relative 'lint/source'
5
5
  require_relative 'lint/checks/title_casing'
6
6
  require_relative 'lint/checks/title_length'
7
+ require_relative 'lint/checks/description_length'
7
8
  require_relative 'lint/checks/layouts_have_content'
8
9
  require_relative 'lint/checks/no_async_functions'
9
10
  require_relative 'lint/checks/waits_for_dom_load'
@@ -23,6 +24,7 @@ module TRMNLP
23
24
  CHECKS = [
24
25
  Checks::TitleCasing,
25
26
  Checks::TitleLength,
27
+ Checks::DescriptionLength,
26
28
  Checks::LayoutsHaveContent,
27
29
  Checks::NoAsyncFunctions,
28
30
  Checks::WaitsForDomLoad,
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module TRMNLP
4
- VERSION = '0.9.0'
4
+ VERSION = '0.11.0'
5
5
  end
@@ -13,4 +13,5 @@ framework_version: latest
13
13
  polling_url: ''
14
14
  polling_headers: ''
15
15
  name: My Plugin
16
+ description: ''
16
17
  refresh_interval: 1440
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: trmnl_preview
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.0
4
+ version: 0.11.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rockwell Schrock
@@ -305,6 +305,7 @@ files:
305
305
  - lib/trmnlp/lint.rb
306
306
  - lib/trmnlp/lint/check.rb
307
307
  - lib/trmnlp/lint/checks/custom_fields_used.rb
308
+ - lib/trmnlp/lint/checks/description_length.rb
308
309
  - lib/trmnlp/lint/checks/form_fields_valid.rb
309
310
  - lib/trmnlp/lint/checks/highcharts_animations_disabled.rb
310
311
  - lib/trmnlp/lint/checks/highcharts_elements_unique.rb