td-ai-tools 1.3.4 → 1.3.6

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 (26) hide show
  1. package/package.json +1 -1
  2. package/skills/forge-cli/SKILL.md +30 -16
  3. package/skills/forge-cli/references/api-v2-certificates-and-security.md +28 -0
  4. package/skills/forge-cli/references/api-v2-databases-and-backups.md +35 -0
  5. package/skills/forge-cli/references/api-v2-overview.md +102 -0
  6. package/skills/forge-cli/references/api-v2-php-services-recipes.md +64 -0
  7. package/skills/forge-cli/references/api-v2-processes-and-scheduler.md +67 -0
  8. package/skills/forge-cli/references/api-v2-sites.md +84 -0
  9. package/skills/forge-cli/references/command-reference-v2.md +145 -0
  10. package/skills/shopify-lint/SKILL.md +1 -1
  11. package/skills/shopify-lint/theme-check-theory/.theme-check.example.yml +4 -0
  12. package/skills/shopify-lint/theme-check-theory/README.md +43 -2
  13. package/skills/shopify-lint/theme-check-theory/configs/recommended.yml +4 -0
  14. package/skills/shopify-lint/theme-check-theory/src/checks/hardcoded-text.test.ts +6 -10
  15. package/skills/shopify-lint/theme-check-theory/src/checks/hardcoded-text.ts +0 -11
  16. package/skills/shopify-lint/theme-check-theory/src/checks/required-liquid-doc.test.ts +46 -0
  17. package/skills/shopify-lint/theme-check-theory/src/checks/required-liquid-doc.ts +111 -0
  18. package/skills/shopify-lint/theme-check-theory/src/index.test.ts +2 -0
  19. package/skills/shopify-lint/theme-check-theory/src/index.ts +3 -0
  20. package/skills/forge-cli/references/api-databases-and-backups.md +0 -48
  21. package/skills/forge-cli/references/api-overview.md +0 -90
  22. package/skills/forge-cli/references/api-php-services-recipes.md +0 -88
  23. package/skills/forge-cli/references/api-sites.md +0 -135
  24. package/skills/forge-cli/references/api-ssl-and-security.md +0 -41
  25. package/skills/forge-cli/references/api-workers-and-scheduler.md +0 -77
  26. package/skills/forge-cli/references/command-reference.md +0 -176
@@ -12,6 +12,10 @@ HardcodedText:
12
12
  enabled: true
13
13
  severity: warning
14
14
 
15
+ RequiredLiquidDoc:
16
+ enabled: true
17
+ severity: warning
18
+
15
19
  # Upstream check, pinned here so the branch lint always fails on it: Shopify
16
20
  # serves `{% stylesheet %}` and `{% javascript %}` bodies verbatim, so Liquid
17
21
  # written inside one ships to the browser as literal text.
@@ -13,6 +13,7 @@ integrated into Shopify CLI. It is not compatible with the archived Ruby
13
13
  | --- | --- | --- |
14
14
  | `DisallowedScriptOrStyleTag` | warning | An inline executable `<script>` or any `<style>` tag in a Liquid file. |
15
15
  | `HardcodedText` | warning | Rendered hard-coded storefront copy that should use a dynamic content source. |
16
+ | `RequiredLiquidDoc` | warning | A `td-` snippet or theme block without a complete Liquid doc contract. |
16
17
  | `UnusedSectionSettings` | warning | A setting declared in `{% schema %}` that is never referenced in the file. |
17
18
  | `UnguardedNullableSetting` | warning | A potentially blank setting output with `{{ }}` and no presence guard (`{% if %}`/`{% unless %}`) or `\| default`. |
18
19
  | `UnguardedMetafield` | warning | A metafield output with no presence guard or `\| default`. |
@@ -33,12 +34,18 @@ Rendered storefront copy should come from a section setting, translation key,
33
34
  metafield, or metaobject. The check reports text nodes, direct quoted output
34
35
  with `{{ }}` or `{% echo %}`, quoted `default` filter fallbacks, hard-coded
35
36
  `alt`, `title`, `placeholder`, and ARIA text attributes, and labels in the
36
- `value` attribute of button-like inputs. Text inside inline SVG elements and
37
- string literals passed as named `render` or `include` arguments are also
37
+ `value` attribute of button-like inputs. Text inside inline SVG elements is also
38
38
  checked. It ignores technical attributes such as classes and IDs, Liquid logic
39
39
  strings, comments, punctuation-only fragments, and text made entirely of HTML
40
40
  character references.
41
41
 
42
+ String literals passed as named `{% render %}` and `{% include %}` arguments are
43
+ ignored. A snippet argument is as often a class name, icon key, size token, or
44
+ heading level as it is storefront copy, and nothing at the call site separates
45
+ the two, so checking them produced mostly false positives. Copy that a snippet
46
+ renders is still reported inside the snippet itself, where it appears as a text
47
+ node or a quoted output.
48
+
42
49
  Text beginning with `--` is ignored as a CSS custom property name: Liquid
43
50
  routinely assembles a custom property declaration before handing it to a
44
51
  `style` attribute or a `{% style %}` block, neither of which this check reads.
@@ -52,6 +59,36 @@ accepted. Short alphabetic tokens and copyright years remain reportable because
52
59
  the check cannot reliably distinguish user-facing copy from currency codes,
53
60
  units, or other intentional literals.
54
61
 
62
+ ### RequiredLiquidDoc
63
+
64
+ Files whose name contains `td-` under `snippets/` or `blocks/` must contain a
65
+ `{% doc %}` tag with a prose description and a non-empty `@example`. Every
66
+ `@param` that is present must use LiquidDoc's `{type} name - description`
67
+ shape; optional names such as `[heading]` are supported. LiquidDoc types are
68
+ descriptive rather than a closed type system, so Shopify Liquid object names,
69
+ primitive names, and project-specific type expressions are all accepted.
70
+
71
+ ```liquid
72
+ {% doc %}
73
+ Renders a product card.
74
+
75
+ @param {product} product - The product to display.
76
+ @param {string} [heading] - An optional card heading.
77
+
78
+ @example
79
+ {% render 'td-product-card', product: product, heading: 'Featured' %}
80
+ {% enddoc %}
81
+ ```
82
+
83
+ The check does not infer missing parameters from variable lookups inside the
84
+ file. Liquid exposes many global objects, and a snippet parameter is introduced
85
+ at its call site, so a single-file lookup cannot reliably distinguish the two.
86
+ To require complete parameter coverage, add a separate project-level analysis
87
+ that indexes named arguments at static `{% render %}` call sites and compares
88
+ their union with each target file's documented parameters. Dynamic snippet
89
+ names and values passed with `with`/`for` require an explicit project policy or
90
+ an allowlist.
91
+
55
92
  ### UnusedSectionSettings
56
93
 
57
94
  This single-file check collects every `id` under `settings` and
@@ -185,6 +222,10 @@ HardcodedText:
185
222
  enabled: true
186
223
  severity: warning
187
224
 
225
+ RequiredLiquidDoc:
226
+ enabled: true
227
+ severity: warning
228
+
188
229
  # Upstream check, pinned here so the branch lint always fails on it: Shopify
189
230
  # serves `{% stylesheet %}` and `{% javascript %}` bodies verbatim, so Liquid
190
231
  # written inside one ships to the browser as literal text.
@@ -6,6 +6,10 @@ HardcodedText:
6
6
  enabled: true
7
7
  severity: warning
8
8
 
9
+ RequiredLiquidDoc:
10
+ enabled: true
11
+ severity: warning
12
+
9
13
  UnusedSectionSettings:
10
14
  enabled: false
11
15
  severity: warning
@@ -98,22 +98,18 @@ describe('HardcodedText', () => {
98
98
  ]);
99
99
  });
100
100
 
101
- it('reports string literals passed as render and include arguments', async () => {
101
+ it('ignores string literals passed as render and include arguments', async () => {
102
+ // A snippet argument is as often a class name, icon key, size token, or
103
+ // heading level as it is storefront copy, and the call site carries nothing
104
+ // that separates the two. The snippet body is still checked on its own.
102
105
  const source = `
103
106
  {% render 'button', label: 'Add to cart' %}
104
107
  {% include 'icon', accessible_name: "Search icon" %}
105
- {% render 'button', label: section.settings.button_label %}
108
+ {% render 'button', variant: 'primary', size: 'large' %}
106
109
  {% render 'icon' %}
107
110
  `;
108
111
 
109
- const offenses = await runLiquidCheck(HardcodedText, source);
110
-
111
- expect(offenses).toHaveLength(2);
112
- expect(
113
- offenses.map(({ start, end }) =>
114
- source.slice(start.index, end.index),
115
- ),
116
- ).toEqual(["'Add to cart'", '"Search icon"']);
112
+ expect(await runLiquidCheck(HardcodedText, source)).toEqual([]);
117
113
  });
118
114
 
119
115
  it('reports hard-coded default filter fallbacks', async () => {
@@ -308,17 +308,6 @@ export const HardcodedText: LiquidCheckDefinition = {
308
308
 
309
309
  reportInlineSvgText(node);
310
310
  },
311
-
312
- async RenderMarkup(node) {
313
- for (const argument of node.args) {
314
- if (
315
- argument.type === NodeTypes.NamedArgument &&
316
- argument.value.type === NodeTypes.String
317
- ) {
318
- reportString(argument.value);
319
- }
320
- }
321
- },
322
311
  };
323
312
  },
324
313
  };
@@ -0,0 +1,46 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { liquidDocProblems, requiresLiquidDoc } from './required-liquid-doc';
4
+
5
+ describe('RequiredLiquidDoc', () => {
6
+ it('targets td- Liquid snippets and blocks only', () => {
7
+ expect(requiresLiquidDoc('file:///theme/snippets/td-card.liquid')).toBe(true);
8
+ expect(requiresLiquidDoc('/theme/blocks/product-td-card.liquid')).toBe(true);
9
+ expect(requiresLiquidDoc('/theme/sections/td-card.liquid')).toBe(false);
10
+ expect(requiresLiquidDoc('/theme/snippets/card.liquid')).toBe(false);
11
+ });
12
+
13
+ it('accepts a description, typed parameters, and an example', () => {
14
+ const body = `
15
+ Renders a card for a product.
16
+ @param {product} product - The product displayed by the card.
17
+ @param {string} [heading] - An optional heading.
18
+ @example
19
+ {% render 'td-card', product: product, heading: 'Featured' %}
20
+ `;
21
+ expect(liquidDocProblems(body)).toEqual([]);
22
+ });
23
+
24
+ it('requires both a description and non-empty example', () => {
25
+ expect(liquidDocProblems('@example\n').map(({ message }) => message)).toEqual([
26
+ 'Liquid doc must include a description before its annotations',
27
+ 'Liquid doc must include an @example with example markup',
28
+ ]);
29
+ });
30
+
31
+ it('requires types and descriptions on documented parameters', () => {
32
+ const body = `
33
+ Renders a card.
34
+ @param product - The product.
35
+ @param {string} heading
36
+ @example
37
+ {% render 'td-card', product: product %}
38
+ `;
39
+ expect(liquidDocProblems(body).map(({ message }) => message)).toEqual([
40
+ 'Liquid doc @param must include a type in braces',
41
+ 'Liquid doc @param must include a description after " - "',
42
+ 'Liquid doc @param must include a type in braces',
43
+ 'Liquid doc @param must include a description after " - "',
44
+ ]);
45
+ });
46
+ });
@@ -0,0 +1,111 @@
1
+ import type { LiquidRawTag } from '@shopify/liquid-html-parser';
2
+ import {
3
+ LiquidCheckDefinition,
4
+ Severity,
5
+ SourceCodeType,
6
+ } from '@shopify/theme-check-common';
7
+
8
+ type DocumentationProblem = { message: string; offset: number; length: number };
9
+
10
+ const PARAM_LINE = /^[ \t]*@param[ \t]+(.*)$/gm;
11
+ const COMPLETE_PARAM = /^\{([^}\n]+)\}\s+(\[[^\]\n]+\]|[^\s-]+)\s+-\s+(.+)$/;
12
+
13
+ /** Returns true for td-named snippets and theme blocks. */
14
+ export function requiresLiquidDoc(uri: string): boolean {
15
+ const path = decodeURIComponent(uri).replace(/\\/g, '/').split(/[?#]/, 1)[0];
16
+ const match = path.match(/(?:^|\/)(snippets|blocks)\/([^/]+)\.liquid$/i);
17
+ return Boolean(match?.[2].toLowerCase().includes('td-'));
18
+ }
19
+
20
+ /** Validates the documented contract without mistaking Shopify globals for parameters. */
21
+ export function liquidDocProblems(body: string): DocumentationProblem[] {
22
+ const problems: DocumentationProblem[] = [];
23
+ const firstAnnotation = body.search(/^[ \t]*@\w+/m);
24
+ const description = body.slice(0, firstAnnotation < 0 ? body.length : firstAnnotation).trim();
25
+
26
+ if (!description) {
27
+ problems.push({
28
+ message: 'Liquid doc must include a description before its annotations',
29
+ offset: 0,
30
+ length: Math.max(body.length, 1),
31
+ });
32
+ }
33
+
34
+ const example = body.match(
35
+ /^[ \t]*@example[^\n]*\n([\s\S]*?)(?=^[ \t]*@\w+|(?![\s\S]))/m,
36
+ );
37
+ if (!example || !example[1].trim()) {
38
+ problems.push({
39
+ message: 'Liquid doc must include an @example with example markup',
40
+ offset: 0,
41
+ length: Math.max(body.length, 1),
42
+ });
43
+ }
44
+
45
+ for (const match of body.matchAll(PARAM_LINE)) {
46
+ const complete = match[1].trim().match(COMPLETE_PARAM);
47
+ const offset = match.index ?? 0;
48
+ if (!complete?.[1].trim()) {
49
+ problems.push({
50
+ message: 'Liquid doc @param must include a type in braces',
51
+ offset,
52
+ length: match[0].length,
53
+ });
54
+ }
55
+ if (!complete?.[3].trim()) {
56
+ problems.push({
57
+ message: 'Liquid doc @param must include a description after " - "',
58
+ offset,
59
+ length: match[0].length,
60
+ });
61
+ }
62
+ }
63
+
64
+ return problems;
65
+ }
66
+
67
+ export const RequiredLiquidDoc: LiquidCheckDefinition = {
68
+ meta: {
69
+ code: 'RequiredLiquidDoc',
70
+ name: 'Require documentation for td- snippets and theme blocks',
71
+ docs: {
72
+ description:
73
+ 'Requires td- snippets and blocks to have a Liquid doc description and example, and validates documented parameters.',
74
+ recommended: true,
75
+ },
76
+ type: SourceCodeType.LiquidHtml,
77
+ severity: Severity.WARNING,
78
+ schema: {},
79
+ },
80
+ create(context) {
81
+ const uri = (context as unknown as { file?: { uri?: string } }).file?.uri ?? '';
82
+ if (!requiresLiquidDoc(uri)) return {};
83
+
84
+ const docs: LiquidRawTag[] = [];
85
+ return {
86
+ async LiquidRawTag(node) {
87
+ if (node.name === 'doc') docs.push(node);
88
+ },
89
+ async onCodePathEnd() {
90
+ if (docs.length === 0) {
91
+ context.report({
92
+ message: 'td- snippets and blocks must include a {% doc %} tag',
93
+ startIndex: 0,
94
+ endIndex: 1,
95
+ });
96
+ return;
97
+ }
98
+
99
+ const doc = docs[0];
100
+ for (const problem of liquidDocProblems(doc.body.value)) {
101
+ const startIndex = doc.body.position.start + problem.offset;
102
+ context.report({
103
+ message: problem.message,
104
+ startIndex,
105
+ endIndex: startIndex + problem.length,
106
+ });
107
+ }
108
+ },
109
+ };
110
+ },
111
+ };
@@ -8,6 +8,7 @@ describe('theme-check-theory module', () => {
8
8
  expect(checks.map(({ meta }) => meta.code)).toEqual([
9
9
  'DisallowedScriptOrStyleTag',
10
10
  'HardcodedText',
11
+ 'RequiredLiquidDoc',
11
12
  'UnusedSectionSettings',
12
13
  'UnguardedNullableSetting',
13
14
  'UnguardedMetafield',
@@ -24,6 +25,7 @@ describe('theme-check-theory module', () => {
24
25
  ).toEqual({
25
26
  DisallowedScriptOrStyleTag: true,
26
27
  HardcodedText: true,
28
+ RequiredLiquidDoc: true,
27
29
  UnusedSectionSettings: false,
28
30
  UnguardedNullableSetting: true,
29
31
  UnguardedMetafield: true,
@@ -1,5 +1,6 @@
1
1
  import { DisallowedScriptOrStyleTag } from './checks/disallowed-script-or-style-tag';
2
2
  import { HardcodedText } from './checks/hardcoded-text';
3
+ import { RequiredLiquidDoc } from './checks/required-liquid-doc';
3
4
  import { UnusedSectionSettings } from './checks/unused-section-settings';
4
5
  import { UnguardedMetafield } from './checks/unguarded-metafield';
5
6
  import { UnguardedMetaobject } from './checks/unguarded-metaobject';
@@ -7,6 +8,7 @@ import { UnguardedNullableSetting } from './checks/unguarded-nullable-setting';
7
8
 
8
9
  export { DisallowedScriptOrStyleTag } from './checks/disallowed-script-or-style-tag';
9
10
  export { HardcodedText } from './checks/hardcoded-text';
11
+ export { RequiredLiquidDoc } from './checks/required-liquid-doc';
10
12
  export { UnusedSectionSettings } from './checks/unused-section-settings';
11
13
  export { UnguardedMetafield } from './checks/unguarded-metafield';
12
14
  export { UnguardedMetaobject } from './checks/unguarded-metaobject';
@@ -18,6 +20,7 @@ export {
18
20
  export const checks = [
19
21
  DisallowedScriptOrStyleTag,
20
22
  HardcodedText,
23
+ RequiredLiquidDoc,
21
24
  UnusedSectionSettings,
22
25
  UnguardedNullableSetting,
23
26
  UnguardedMetafield,
@@ -1,48 +0,0 @@
1
- # Forge API — Databases, Database Users, Backups
2
-
3
- Endpoint paths are relative to `https://forge.laravel.com/api/v1`.
4
-
5
- Statamic context: many Statamic sites are flat-file and need none of this. Use these endpoints when the project uses Statamic Eloquent, search drivers like MySQL, runs Laravel auth tables, or simply needs MySQL/Postgres for app-level data.
6
-
7
- ## Databases
8
-
9
- | Operation | Method | Path | Body |
10
- | --------------- | ------ | ----------------------------------------------------- | --------------------------------- |
11
- | List | GET | `/servers/{serverId}/databases` | — |
12
- | Create | POST | `/servers/{serverId}/databases` | required: `name`; optional: `user`, `password` (creates a user in one shot) |
13
- | Get | GET | `/servers/{serverId}/databases/{databaseId}` | — |
14
- | Delete | DELETE | `/servers/{serverId}/databases/{databaseId}` | Destructive — data is gone |
15
- | Sync | POST | `/servers/{serverId}/databases/sync` | Reconcile Forge state with what's actually on the server (use if databases were created outside Forge) |
16
-
17
- ## Database Users
18
-
19
- | Operation | Method | Path | Body |
20
- | --------------- | ------ | ------------------------------------------------------------- | ----------------------------------------------------------------- |
21
- | List | GET | `/servers/{serverId}/database-users` | — |
22
- | Create | POST | `/servers/{serverId}/database-users` | required: `name`, `password`, `databases[]` (array of DB IDs) |
23
- | Get | GET | `/servers/{serverId}/database-users/{userId}` | — |
24
- | Update | PUT | `/servers/{serverId}/database-users/{userId}` | optional: `databases[]` — replaces the access set |
25
- | Delete | DELETE | `/servers/{serverId}/database-users/{userId}` | — |
26
-
27
- To grant an existing user access to a new database, PUT the full list of database IDs you want it to have, not just the addition.
28
-
29
- ## Backup Configurations
30
-
31
- Forge can run scheduled mysqldump/pg_dump to S3, DigitalOcean Spaces, or any S3-compatible store. For a Statamic site backed by a database, this is the standard way to get off-server backups.
32
-
33
- | Operation | Method | Path | Body / notes |
34
- | --------------------- | ------ | ----------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
35
- | List configs | GET | `/servers/{serverId}/backup-configs` | — |
36
- | Create config | POST | `/servers/{serverId}/backup-configs` | required: `provider` (`s3`, `spaces`, `custom`), `credentials`, `frequency`, `databases[]`; optional: `directory`, `email`, `retention` (number to keep) |
37
- | Get config | GET | `/servers/{serverId}/backup-configs/{backupConfigurationId}` | — |
38
- | Update config | PUT | `/servers/{serverId}/backup-configs/{backupConfigurationId}` | Same fields as create |
39
- | Run backup now | POST | `/servers/{serverId}/backup-configs/{backupConfigurationId}` | Triggers a one-off run, on top of the schedule |
40
- | Delete config | DELETE | `/servers/{serverId}/backup-configs/{backupConfigurationId}` | Stops future backups; does not remove existing archives from object storage |
41
- | Restore one backup | POST | `/servers/{serverId}/backup-configs/{backupConfigurationId}/backups/{backupId}` | optional: `database` (specific DB ID to restore into) |
42
- | Delete one backup | DELETE | `/servers/{serverId}/backup-configs/{backupConfigurationId}/backups/{backupId}` | Removes the archive from the remote store |
43
-
44
- Notes:
45
-
46
- - Statamic content lives on disk under `content/`, `assets/`, and (sometimes) `users/`. Database backups alone do not cover these — pair with a filesystem snapshot or Statamic's own backup add-on for full coverage.
47
- - The `credentials` shape varies per provider (e.g. S3 wants `key`, `secret`, `region`, `bucket`). Check the Forge UI for the exact field names if creating from scratch.
48
- - `frequency` accepts standard cron-style scheduling — confirm in the Forge UI before creating programmatically.
@@ -1,90 +0,0 @@
1
- # Forge HTTP API Overview
2
-
3
- Use this reference when the Forge CLI does not expose an operation you need. The CLI covers day-to-day deploys, env management, logs, restarts, and Tinker; the HTTP API covers everything else (site creation, PHP version changes, SSL issuance, workers, scheduler integration, backups, recipes, and more).
4
-
5
- The API pages in this directory are scoped to operations that apply to **Statamic sites** (a Laravel-based flat-file or DB-backed CMS). WordPress, Octane, Reverb, Pulse, Horizon, Inertia SSR, load balancers, and server provisioning are intentionally omitted.
6
-
7
- ## Authentication and Transport
8
-
9
- - Base URL: `https://forge.laravel.com/api/v1`
10
- - Header: `Authorization: Bearer <FORGE_API_TOKEN>`
11
- - Always send `Accept: application/json` and, for write requests, `Content-Type: application/json`.
12
- - Reuse the same `FORGE_API_TOKEN` the CLI uses. If it is not in the env, ask the user; do not try to read tokens from disk.
13
-
14
- ```bash
15
- curl -sS https://forge.laravel.com/api/v1/user \
16
- -H "Authorization: Bearer $FORGE_API_TOKEN" \
17
- -H "Accept: application/json"
18
- ```
19
-
20
- ## When to Use the API Instead of the CLI
21
-
22
- | Need | CLI? | API? |
23
- | --------------------------------------------- | --------------------- | ----------------------------------------------------------- |
24
- | Deploy a site, view logs, push env | Yes | Optional |
25
- | Create a new site / install a repo | No | Yes — `POST /servers/{id}/sites` + `POST .../git` |
26
- | Change a site's PHP version | No | Yes — `PUT .../sites/{id}/php` |
27
- | Issue or renew Let's Encrypt SSL | No | Yes — `POST .../certificates/letsencrypt` |
28
- | Edit deployment script or Nginx config | No | Yes — `PUT .../deployment/script` or `PUT .../nginx` |
29
- | Create queue workers, daemons, scheduled jobs | No | Yes |
30
- | Enable Laravel scheduler or maintenance mode | No | Yes |
31
- | Create/restore database backups | No | Yes |
32
- | Run a saved Recipe across servers | No | Yes |
33
- | Add a webhook for deploys | No | Yes |
34
-
35
- Statamic note: many sites do not run a queue or scheduler. Only create workers, daemons, or scheduled jobs when the project actually defines queue connections or `php please schedule:run` tasks; do not provision them by default.
36
-
37
- ## Finding IDs the API Needs
38
-
39
- Almost every endpoint needs `serverId` and most need `siteId`. The CLI can supply both without separate API calls:
40
-
41
- ```bash
42
- forge server:list # → server IDs and names
43
- forge server:current # → active server ID
44
- forge site:list # → site IDs on the active server
45
- ```
46
-
47
- If scripting, list via API:
48
-
49
- ```bash
50
- curl -sS "$BASE/servers" # all servers on the account
51
- curl -sS "$BASE/servers/$SERVER/sites" # all sites on a server
52
- ```
53
-
54
- ## Common Response Codes
55
-
56
- | Code | Meaning |
57
- | ---- | ------------------------------------------------------ |
58
- | 200 | Success |
59
- | 204 | Success with no body (e.g. delete, clear logs) |
60
- | 400 | Request failed despite valid data |
61
- | 401 | Invalid or missing API token |
62
- | 404 | Resource not found (wrong server/site ID is the usual) |
63
- | 422 | Validation error — body lists which fields are wrong |
64
- | 429 | Rate-limited — back off and retry |
65
- | 500 | Forge-side server error |
66
-
67
- 422 responses include a JSON body of field-level errors. Read it before retrying.
68
-
69
- ## Convention Used in These References
70
-
71
- Each endpoint table shows:
72
-
73
- - Method and path (relative to `https://forge.laravel.com/api/v1`)
74
- - Required body fields, then optional ones
75
- - Notes that are easy to miss (enum values, side effects, "this returns plain text not JSON", etc.)
76
-
77
- Example invocation pattern used throughout:
78
-
79
- ```bash
80
- BASE="https://forge.laravel.com/api/v1"
81
- AUTH=(-H "Authorization: Bearer $FORGE_API_TOKEN" -H "Accept: application/json")
82
- JSON=(-H "Content-Type: application/json")
83
-
84
- curl -sS "${AUTH[@]}" "${JSON[@]}" \
85
- -X POST "$BASE/servers/$SERVER/sites/$SITE/deployment/deploy"
86
- ```
87
-
88
- ## Deprecation Notice
89
-
90
- The official docs flag the v1 API as deprecated with a discontinuation date of June 30, 2026. Until a v2 is announced for general use, v1 is what the CLI itself targets and what these references describe. Re-check the [official API docs](https://forge.laravel.com/api-documentation) before doing migration work.
@@ -1,88 +0,0 @@
1
- # Forge API — PHP, Services, Server Logs, Recipes, Composer Auth
2
-
3
- Endpoint paths are relative to `https://forge.laravel.com/api/v1`.
4
-
5
- ## PHP Versions on a Server
6
-
7
- | Operation | Method | Path | Body / notes |
8
- | ----------------- | ------ | ------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------- |
9
- | List installed | GET | `/servers/{serverId}/php` | Each entry has `status`, `displayable_version`, `binary_name`, and flags for default and CLI version |
10
- | Install a version | POST | `/servers/{serverId}/php` | `version`: `php84`, `php83`, `php82`, `php81`, `php80`, `php74`, `php73`, `php72`, `php71`, `php70`, `php56` |
11
- | Patch update | POST | `/servers/{serverId}/php/update` | `version`: same enum — patches the named major.minor to its latest point release |
12
- | Enable OPCache | POST | `/servers/{serverId}/php/opcache` | Use in production after the site is stable; do not enable on dev servers where you're actively editing PHP |
13
- | Disable OPCache | DELETE | `/servers/{serverId}/php/opcache` | — |
14
-
15
- Statamic version floor — install the matching PHP version on the server before pointing a site at it:
16
-
17
- | Statamic | Required PHP |
18
- | -------- | ------------ |
19
- | 4.x | 8.1+ |
20
- | 5.x | 8.2+ |
21
-
22
- After installing a new PHP version, set it on the site with `PUT /servers/{id}/sites/{siteId}/php` (`version`).
23
-
24
- ## Service Control (start / stop / restart)
25
-
26
- | Service | Method | Path | Body / notes |
27
- | ------------------- | ------ | ------------------------------------------ | -------------------------------------------------- |
28
- | MySQL reboot | POST | `/servers/{serverId}/mysql/reboot` | — |
29
- | MySQL stop | POST | `/servers/{serverId}/mysql/stop` | — |
30
- | Postgres reboot | POST | `/servers/{serverId}/postgres/reboot` | — |
31
- | Postgres stop | POST | `/servers/{serverId}/postgres/stop` | — |
32
- | Nginx reboot | POST | `/servers/{serverId}/nginx/reboot` | — |
33
- | Nginx stop | POST | `/servers/{serverId}/nginx/stop` | — |
34
- | Nginx test config | GET | `/servers/{serverId}/nginx/test` | Returns parse errors, if any. Run this before reloading after edits to site nginx blocks. |
35
- | PHP-FPM reboot | POST | `/servers/{serverId}/php/reboot` | Body: `version` — same enum as PHP install |
36
- | Generic start | POST | `/servers/{serverId}/services/start` | Body: service name |
37
- | Generic stop | POST | `/servers/{serverId}/services/stop` | Body: service name |
38
- | Generic restart | POST | `/servers/{serverId}/services/restart` | Body: service name |
39
-
40
- After editing site Nginx config via the API, hit `GET .../nginx/test`. If it passes, `POST .../nginx/reboot` (or rely on Forge to reload — the API also accepts a reload via the update endpoint).
41
-
42
- ## Server Logs
43
-
44
- | Operation | Method | Path | Body / notes |
45
- | --------------- | ------ | ------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
46
- | Read a log | GET | `/servers/{serverId}/logs` | Query `?file=` with one of `nginx_access`, `nginx_error`, `database`, `php7x`, `php56`. Returns a tail of the log as text. |
47
-
48
- For day-to-day tailing, `forge nginx:logs`, `forge php:logs`, and `forge database:logs` are simpler than the API.
49
-
50
- ## Recipes
51
-
52
- Recipes are saved provisioning scripts you can run across one or many servers. Handy for installing extra extensions Statamic add-ons might require (Imagick variants, FFmpeg for video transcoding, GhostScript for PDF assets, etc.) without redoing manual SSH steps.
53
-
54
- | Operation | Method | Path | Body |
55
- | --------------- | ------ | ------------------------------------- | --------------------------------------------------------------------- |
56
- | List | GET | `/recipes` | — |
57
- | Create | POST | `/recipes` | required: `name`, `user`, `script` |
58
- | Get | GET | `/recipes/{recipeId}` | — |
59
- | Update | PUT | `/recipes/{recipeId}` | `name`, `user`, `script` |
60
- | Delete | DELETE | `/recipes/{recipeId}` | — |
61
- | Run | POST | `/recipes/{recipeId}/run` | `servers[]` (server IDs), `notify` (bool — email when finished) |
62
-
63
- `user` is the OS user the script runs as (`root` for installing system packages, `forge` for user-scoped work).
64
-
65
- ## Composer Package Authentication (per site)
66
-
67
- Statamic Pro and many commercial add-ons live behind `https://composer.statamic.com` with a license-keyed username/password. Forge stores these as Composer auth credentials so deploys can install them.
68
-
69
- | Operation | Method | Path | Body |
70
- | --------------- | ------ | --------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
71
- | Get | GET | `/servers/{serverId}/sites/{siteId}/packages` | — |
72
- | Update | PUT | `/servers/{serverId}/sites/{siteId}/packages` | `credentials[]` with `{repository_url, username, password}` per entry — PUT replaces the full list |
73
-
74
- Typical Statamic entry:
75
-
76
- ```json
77
- {
78
- "credentials": [
79
- {
80
- "repository_url": "composer.statamic.com",
81
- "username": "<site-domain>",
82
- "password": "<statamic-pro-license-key>"
83
- }
84
- ]
85
- }
86
- ```
87
-
88
- After updating, trigger a deploy so `composer install` runs with the new auth.