opkg 0.6.0 → 0.6.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.
Files changed (185) hide show
  1. package/README.md +35 -15
  2. package/dist/commands/duplicate.js +5 -2
  3. package/dist/commands/duplicate.js.map +1 -1
  4. package/dist/commands/init.js +4 -1
  5. package/dist/commands/init.js.map +1 -1
  6. package/dist/commands/install.js +8 -7
  7. package/dist/commands/install.js.map +1 -1
  8. package/dist/commands/list.js +9 -3
  9. package/dist/commands/list.js.map +1 -1
  10. package/dist/commands/login.js +58 -0
  11. package/dist/commands/login.js.map +1 -0
  12. package/dist/commands/logout.js +28 -0
  13. package/dist/commands/logout.js.map +1 -0
  14. package/dist/commands/pull.js +5 -299
  15. package/dist/commands/pull.js.map +1 -1
  16. package/dist/commands/push.js +5 -245
  17. package/dist/commands/push.js.map +1 -1
  18. package/dist/commands/save.js +20 -4
  19. package/dist/commands/save.js.map +1 -1
  20. package/dist/commands/show.js +18 -5
  21. package/dist/commands/show.js.map +1 -1
  22. package/dist/commands/status.js +19 -6
  23. package/dist/commands/status.js.map +1 -1
  24. package/dist/commands/tui.js +61 -0
  25. package/dist/commands/tui.js.map +1 -0
  26. package/dist/constants/index.js +1 -0
  27. package/dist/constants/index.js.map +1 -1
  28. package/dist/core/add/package-index-updater.js +4 -3
  29. package/dist/core/add/package-index-updater.js.map +1 -1
  30. package/dist/core/api-keys.js +6 -2
  31. package/dist/core/api-keys.js.map +1 -1
  32. package/dist/core/auth.js +25 -40
  33. package/dist/core/auth.js.map +1 -1
  34. package/dist/core/config.js +11 -3
  35. package/dist/core/config.js.map +1 -1
  36. package/dist/core/dependency-resolver.js +21 -10
  37. package/dist/core/dependency-resolver.js.map +1 -1
  38. package/dist/core/device-auth.js +81 -0
  39. package/dist/core/device-auth.js.map +1 -0
  40. package/dist/core/directory.js +14 -8
  41. package/dist/core/directory.js.map +1 -1
  42. package/dist/core/install/bulk-install-pipeline.js +2 -1
  43. package/dist/core/install/bulk-install-pipeline.js.map +1 -1
  44. package/dist/core/install/canonical-plan.js +11 -5
  45. package/dist/core/install/canonical-plan.js.map +1 -1
  46. package/dist/core/install/download-keys.js +2 -2
  47. package/dist/core/install/download-keys.js.map +1 -1
  48. package/dist/core/install/install-flow.js +6 -4
  49. package/dist/core/install/install-flow.js.map +1 -1
  50. package/dist/core/install/install-pipeline.js +74 -6
  51. package/dist/core/install/install-pipeline.js.map +1 -1
  52. package/dist/core/install/remote-flow.js +17 -6
  53. package/dist/core/install/remote-flow.js.map +1 -1
  54. package/dist/core/install/version-selection.js +25 -8
  55. package/dist/core/install/version-selection.js.map +1 -1
  56. package/dist/core/openpackage.js +6 -6
  57. package/dist/core/openpackage.js.map +1 -1
  58. package/dist/core/package.js +71 -8
  59. package/dist/core/package.js.map +1 -1
  60. package/dist/core/profiles.js +60 -3
  61. package/dist/core/profiles.js.map +1 -1
  62. package/dist/core/pull/pull-errors.js +62 -0
  63. package/dist/core/pull/pull-errors.js.map +1 -0
  64. package/dist/core/pull/pull-options.js +2 -0
  65. package/dist/core/pull/pull-options.js.map +1 -0
  66. package/dist/core/pull/pull-output.js +50 -0
  67. package/dist/core/pull/pull-output.js.map +1 -0
  68. package/dist/core/pull/pull-pipeline.js +141 -0
  69. package/dist/core/pull/pull-pipeline.js.map +1 -0
  70. package/dist/core/pull/pull-strategies.js +103 -0
  71. package/dist/core/pull/pull-strategies.js.map +1 -0
  72. package/dist/core/pull/pull-types.js +2 -0
  73. package/dist/core/pull/pull-types.js.map +1 -0
  74. package/dist/core/push/push-context.js +95 -0
  75. package/dist/core/push/push-context.js.map +1 -0
  76. package/dist/core/push/push-errors.js +133 -0
  77. package/dist/core/push/push-errors.js.map +1 -0
  78. package/dist/core/push/push-output.js +44 -0
  79. package/dist/core/push/push-output.js.map +1 -0
  80. package/dist/core/push/push-pipeline.js +74 -0
  81. package/dist/core/push/push-pipeline.js.map +1 -0
  82. package/dist/core/push/push-single-file.js +56 -0
  83. package/dist/core/push/push-single-file.js.map +1 -0
  84. package/dist/core/push/push-types.js +2 -0
  85. package/dist/core/push/push-types.js.map +1 -0
  86. package/dist/core/push/push-upload.js +68 -0
  87. package/dist/core/push/push-upload.js.map +1 -0
  88. package/dist/core/registry.js +4 -2
  89. package/dist/core/registry.js.map +1 -1
  90. package/dist/core/remote-pull.js +163 -35
  91. package/dist/core/remote-pull.js.map +1 -1
  92. package/dist/core/save/package-saver.js +37 -1
  93. package/dist/core/save/package-saver.js.map +1 -1
  94. package/dist/core/save/package-yml-generator.js +5 -4
  95. package/dist/core/save/package-yml-generator.js.map +1 -1
  96. package/dist/core/save/save-pipeline.js +17 -6
  97. package/dist/core/save/save-pipeline.js.map +1 -1
  98. package/dist/core/save/save-single-file.js +124 -0
  99. package/dist/core/save/save-single-file.js.map +1 -0
  100. package/dist/core/scoping/package-scoping.js +1 -1
  101. package/dist/core/scoping/package-scoping.js.map +1 -1
  102. package/dist/core/token-store.js +73 -0
  103. package/dist/core/token-store.js.map +1 -0
  104. package/dist/index.js +4 -0
  105. package/dist/index.js.map +1 -1
  106. package/dist/tui/app.js +95 -0
  107. package/dist/tui/app.js.map +1 -0
  108. package/dist/tui/components/package-list.js +73 -0
  109. package/dist/tui/components/package-list.js.map +1 -0
  110. package/dist/tui/controller.js +365 -0
  111. package/dist/tui/controller.js.map +1 -0
  112. package/dist/tui/index.js +12 -0
  113. package/dist/tui/index.js.map +1 -0
  114. package/dist/tui/services/file-index.js +64 -0
  115. package/dist/tui/services/file-index.js.map +1 -0
  116. package/dist/tui/services/packages.js +18 -0
  117. package/dist/tui/services/packages.js.map +1 -0
  118. package/dist/tui/services/save.js +21 -0
  119. package/dist/tui/services/save.js.map +1 -0
  120. package/dist/tui/state/app-state.js +15 -0
  121. package/dist/tui/state/app-state.js.map +1 -0
  122. package/dist/tui/state.js +17 -0
  123. package/dist/tui/state.js.map +1 -0
  124. package/dist/tui/types.js +2 -0
  125. package/dist/tui/types.js.map +1 -0
  126. package/dist/tui/views/add-file-modal.js +129 -0
  127. package/dist/tui/views/add-file-modal.js.map +1 -0
  128. package/dist/tui/views/file-preview.js +44 -0
  129. package/dist/tui/views/file-preview.js.map +1 -0
  130. package/dist/tui/views/list-packages.js +73 -0
  131. package/dist/tui/views/list-packages.js.map +1 -0
  132. package/dist/tui/views/main-menu.js +29 -0
  133. package/dist/tui/views/main-menu.js.map +1 -0
  134. package/dist/tui/views/manage-view.js +81 -0
  135. package/dist/tui/views/manage-view.js.map +1 -0
  136. package/dist/tui/views/package-hub.js +120 -0
  137. package/dist/tui/views/package-hub.js.map +1 -0
  138. package/dist/tui/views/placeholder.js +24 -0
  139. package/dist/tui/views/placeholder.js.map +1 -0
  140. package/dist/types/index.js.map +1 -1
  141. package/dist/utils/bun-bootstrap.js +72 -0
  142. package/dist/utils/bun-bootstrap.js.map +1 -0
  143. package/dist/utils/formatters.js +3 -1
  144. package/dist/utils/formatters.js.map +1 -1
  145. package/dist/utils/http-client.js +27 -5
  146. package/dist/utils/http-client.js.map +1 -1
  147. package/dist/utils/index-based-installer.js +25 -2
  148. package/dist/utils/index-based-installer.js.map +1 -1
  149. package/dist/utils/install-file-discovery.js +17 -23
  150. package/dist/utils/install-file-discovery.js.map +1 -1
  151. package/dist/utils/manifest-paths.js +27 -0
  152. package/dist/utils/manifest-paths.js.map +1 -0
  153. package/dist/utils/package-management.js +139 -45
  154. package/dist/utils/package-management.js.map +1 -1
  155. package/dist/utils/package-merge.js +48 -0
  156. package/dist/utils/package-merge.js.map +1 -0
  157. package/dist/utils/package-name.js +29 -0
  158. package/dist/utils/package-name.js.map +1 -1
  159. package/dist/utils/package-versioning.js +16 -4
  160. package/dist/utils/package-versioning.js.map +1 -1
  161. package/dist/utils/package-yml.js +17 -12
  162. package/dist/utils/package-yml.js.map +1 -1
  163. package/dist/utils/prompts.js +0 -31
  164. package/dist/utils/prompts.js.map +1 -1
  165. package/dist/utils/registry-entry-filter.js +2 -2
  166. package/dist/utils/registry-entry-filter.js.map +1 -1
  167. package/dist/utils/registry-paths.js +38 -0
  168. package/dist/utils/registry-paths.js.map +1 -1
  169. package/dist/utils/tarball.js +6 -2
  170. package/dist/utils/tarball.js.map +1 -1
  171. package/dist/utils/version-ranges.js +11 -8
  172. package/dist/utils/version-ranges.js.map +1 -1
  173. package/package.json +2 -2
  174. package/specs/auth/auth-device-flow.md +70 -0
  175. package/specs/install/install-behavior.md +30 -0
  176. package/specs/install/package-yml-canonical.md +21 -1
  177. package/specs/install/version-resolution.md +9 -7
  178. package/specs/login/login-device-flow.md +70 -0
  179. package/specs/push/push-behavior.md +38 -10
  180. package/specs/push/push-errors-and-hints.md +19 -6
  181. package/specs/push/push-remote-upload.md +3 -0
  182. package/specs/push/push-scoping.md +14 -22
  183. package/specs/push/push-version-selection.md +18 -16
  184. package/specs/save/save-modes-inputs.md +3 -1
  185. package/specs/save-pack.md +1 -0
@@ -0,0 +1,70 @@
1
+ # opkg login – Device Authorization Flow (RFC 8628)
2
+
3
+ ## Goals
4
+ - Add `opkg login [--profile <name>]` using OAuth 2.0 Device Authorization Grant.
5
+ - Store bearer tokens per profile; keep API-key auth backward compatible.
6
+
7
+ ## Command behavior
8
+ - Syntax: `opkg login [--profile <name>]`
9
+ - Profile: optional; defaults to `default`.
10
+ - Flow:
11
+ 1) Start device authorization → get `device_code`, `user_code`, `verification_uri`, `verification_uri_complete`, `expires_in`, `interval`.
12
+ 2) Print code/URL; attempt to open browser to `verification_uri_complete`.
13
+ 3) Poll token endpoint until success/denied/expired/timeout.
14
+ 4) On success, persist access/refresh tokens to the selected profile.
15
+ 5) On failure, show actionable error and exit non-zero.
16
+
17
+ ## HTTP interactions (backend contract)
18
+ - POST `/auth/device/authorize` (no auth):
19
+ - Body: `{ clientId: 'opkg-cli', scope?: 'openid', deviceName?: 'opkg-cli' }`
20
+ - Response: `{ device_code, user_code, verification_uri, verification_uri_complete, expires_in, interval }`
21
+ - POST `/auth/device/token` (no auth):
22
+ - Body: `{ deviceCode }`
23
+ - Success: `{ access_token, refresh_token, token_type: 'bearer', expires_in }`
24
+ - Error codes (HTTP 400): `authorization_pending`, `slow_down`, `expired_token`, `access_denied`
25
+ - POST `/auth/refresh` (no auth) for refresh flow:
26
+ - Body: `{ refreshToken }`
27
+ - Success: `{ accessToken, refreshToken }`
28
+
29
+ ## CLI storage & auth selection
30
+ - Extend `ProfileCredentials` to include `access_token`, `refresh_token`, `expires_at`, `token_type`.
31
+ - Credentials persisted in the existing profile credentials INI; preserve existing `api_key`.
32
+ - Header selection:
33
+ - Prefer non-expired access token → `Authorization: Bearer <token>`.
34
+ - If expired and refresh token present → call `/auth/refresh`, persist new pair.
35
+ - Fallback: `X-API-Key` if present.
36
+ - If none: instruct user to run `opkg login` or configure API key.
37
+
38
+ ## UX requirements
39
+ - Print user code and verification URL.
40
+ - Open browser best-effort; if it fails, user can manually visit the URL.
41
+ - Poll respecting `interval`; on `slow_down` add +5s each time.
42
+ - Time out when `expires_in` elapses; show “code expired, rerun opkg login.”
43
+ - Errors:
44
+ - `access_denied`: “Access denied. Please restart opkg login.”
45
+ - `expired_token`: “Code expired. Please rerun opkg login.”
46
+
47
+ ## Persistence details
48
+ - `expires_at` derived from `Date.now() + expires_in*1000` or JWT `exp`.
49
+ - When refreshing, keep existing `api_key` intact in the profile record.
50
+
51
+ ## Edge cases & fallbacks
52
+ - If profile missing: create credentials entry when saving tokens.
53
+ - If refresh fails: drop to API key if present; otherwise require login.
54
+ - Platform-specific browser open: `open` (mac), `start` (win), `xdg-open` (linux); ignore failures.
55
+
56
+ ## Logout command
57
+ - Syntax: `opkg logout [--profile <name>]`
58
+ - Requires stored OAuth tokens for the profile; no-op if none.
59
+ - Sends `POST /auth/logout` with bearer auth and `{ refreshToken }` body.
60
+ - On success or failure, clears local token store entry; keeps any configured API key.
61
+ - If profile resolved as direct API key usage, exit with “no OAuth session.”
62
+
63
+ ## Telemetry/logging (minimal)
64
+ - Debug log start/stop of poll, slow_down adjustments, refresh attempts.
65
+ - Do not log tokens.
66
+
67
+ ## Non-goals (future)
68
+ - Device-name flag, headless/no-browser flag.
69
+ - Multi-factor UX in CLI.
70
+
@@ -3,7 +3,10 @@
3
3
  ### Overview
4
4
 
5
5
  The `opkg push` command uploads a local package version from the **local registry** to the **remote registry**.
6
- It is **strictly limited to stable versions** (no prerelease versions like `1.2.3-dev.abc`).
6
+ It allows:
7
+ - **Stable versions** (`x.y.z`).
8
+ - **Unversioned packages** (when `package.yml` omits `version`, represented as `0.0.0`).
9
+ It still rejects prerelease versions like `1.2.3-dev.abc`.
7
10
 
8
11
  This document focuses on user-facing behavior:
9
12
  - CLI shapes and arguments.
@@ -20,11 +23,15 @@ This document focuses on user-facing behavior:
20
23
  - **Package syntax**:
21
24
  - `<name>` – package name, optionally unscoped.
22
25
  - `<name>@<version>` – optional explicit version.
26
+ - `<name@version>/<registry-path>` – partial push of specific registry paths.
27
+ - `--paths <list>` – comma-separated registry paths for partial push.
23
28
 
24
29
  Examples:
25
30
  - `opkg push my-pack`
26
31
  - `opkg push @scope/my-pack`
27
32
  - `opkg push my-pack@1.2.3`
33
+ - `opkg push @scope/my-pack/specs/readme.md` (partial push of a single file)
34
+ - `opkg push @scope/my-pack@1.2.3 --paths specs/readme.md,specs/guide.md`
28
35
 
29
36
  ---
30
37
 
@@ -57,24 +64,25 @@ Examples:
57
64
 
58
65
  ## Implicit version behavior: `opkg push <pkg>`
59
66
 
60
- When no version is specified, the command **only considers stable versions**.
67
+ When no version is specified, the command prefers **stable versions** and can fall back to a **`0.0.0`** package if no stable exists.
61
68
 
62
69
  High-level flow:
63
70
 
64
71
  1. Discover all versions of `<pkg>` from the local registry.
65
72
  2. Compute the latest **stable** version.
66
- 3. If no stable versions exist:
73
+ 3. If no stable versions exist but a **`0.0.0`** entry exists:
74
+ - Use the `0.0.0` package as the candidate.
75
+ 4. If neither stable nor unversioned exists:
67
76
  - Inform the user and exit **gracefully** (non-error).
68
- 4. If a stable version exists:
69
- - Prompt the user to confirm pushing that version.
77
+ 5. If a candidate exists:
78
+ - Prompt the user to confirm pushing that candidate.
70
79
 
71
80
  **Details**
72
81
 
73
- - If no stable versions are found:
74
- - The CLI prints:
75
- - `❌ No stable versions found for package '<pkg>'`
76
- - `💡 Stable versions can be created using "opkg pack <package>".`
77
- - The command exits with a **success** result (no global error message).
82
+ - If no stable versions are found but a `0.0.0` entry exists:
83
+ - The CLI notes it will push the `0.0.0` package.
84
+ - If no stable versions and no unversioned entry:
85
+ - The CLI prints the existing “no stable versions” message and exits successfully.
78
86
  - If a stable version (e.g. `1.2.3`) is found:
79
87
  - The CLI prompts:
80
88
  - `Push latest stable version '1.2.3'?` (default: yes).
@@ -91,6 +99,26 @@ High-level flow:
91
99
 
92
100
  ---
93
101
 
102
+ ## Partial push behavior (paths)
103
+
104
+ - Partial pushes upload only specific registry paths from an existing local package version.
105
+ - Paths can be provided via:
106
+ - `<pkg[@ver]>/<registry-path>`
107
+ - `--paths specs/readme.md,specs/guide.md`
108
+ - Behavior:
109
+ 1. Scope resolution and version selection run first (explicit or latest-stable).
110
+ 2. Requested paths are normalized and validated against the local package files.
111
+ - Missing paths fail the push with a clear missing-path message.
112
+ 3. Tarball is narrowed to:
113
+ - The requested file set.
114
+ - `.openpackage/package.yml`.
115
+ 4. Upload uses the standard `/packages/push` endpoint.
116
+
117
+ Notes:
118
+ - This replaces the previous single-file `f` flow; single-file pushes are just partial pushes with one path.
119
+ - Manifest is required; if `.openpackage/package.yml` is missing locally, the CLI errors.
120
+
121
+
94
122
  ## Stable-only guarantees (behavioral view)
95
123
 
96
124
  From the user’s perspective:
@@ -79,6 +79,16 @@ The following behavior is preserved from the broader CLI error-handling design,
79
79
  - The command returns an error result:
80
80
  - `error: "Version not found"`.
81
81
 
82
+ ### Requested path not found (partial push)
83
+
84
+ - Condition:
85
+ - User requests a partial push (via `--paths` or `<pkg@ver>/<registry-path>`) and one or more paths are missing locally.
86
+ - Behavior:
87
+ - The CLI prints, for each missing path:
88
+ - `❌ Path '<path>' not found in local registry for '<pkg>@<version>'`
89
+ - The command returns an error result:
90
+ - `error: "Requested path not found in local registry"`.
91
+
82
92
  ### Explicit prerelease version
83
93
 
84
94
  - Condition:
@@ -97,12 +107,15 @@ The following behavior is preserved from the broader CLI error-handling design,
97
107
  - User runs `opkg push <pkg>` without specifying a version.
98
108
  - No **stable** versions of `<pkg>` exist in the local registry.
99
109
  - Behavior:
100
- - The CLI prints:
101
- - `❌ No stable versions found for package '<pkg>'`
102
- - `💡 Stable versions can be created using "opkg pack <package>".`
103
- - The command returns a **success** result (no error string), so the global error handler:
104
- - Does **not** print an additional plain `No stable versions found` line.
105
- - This is treated as an informational exit: the user simply needs to create a stable version first.
110
+ - If a **`0.0.0`** entry exists:
111
+ - The CLI attempts to push the `0.0.0` package.
112
+ - If no `0.0.0` entry exists:
113
+ - The CLI prints:
114
+ - `❌ No stable versions found for package '<pkg>'`
115
+ - `💡 Stable versions can be created using "opkg pack <package>".`
116
+ - The command returns a **success** result (no error string), so the global error handler:
117
+ - Does **not** print an additional plain `No stable versions found` line.
118
+ - This is treated as an informational exit: the user needs to create a stable (or unversioned) package first.
106
119
 
107
120
  ---
108
121
 
@@ -41,6 +41,9 @@ Assuming a valid stable package `pkg` and `versionToPush` have been selected:
41
41
  4. **Tarball creation**
42
42
 
43
43
  - `createTarballFromPackage(pkg)` builds a tarball from the package files.
44
+ - For **partial pushes** (paths provided via spec or `--paths`):
45
+ - The tarball is narrowed to only the requested registry paths plus `.openpackage/package.yml`.
46
+ - File count reflects only the selected files.
44
47
  - The CLI prints:
45
48
  - `✓ Creating tarball...`
46
49
  - `✓ Created tarball (<file-count> files, <formatted-size>)`
@@ -5,44 +5,36 @@
5
5
  The `opkg push` command may need to **scope** an unscoped package before pushing it.
6
6
  Scoping ensures that packages in the remote registry are properly namespaced, e.g. `@user/package`.
7
7
 
8
- This document describes how `push` handles scoping and how it keeps the local registry and workspace in sync.
8
+ This document describes how `push` handles scoping for upload. Scoping is applied **only to the upload payload**; the local registry and workspace stay unchanged.
9
9
 
10
10
  ---
11
11
 
12
- ## Scope handling
12
+ ## Scope handling (upload-only)
13
13
 
14
- 1. The command looks up `<package-name>` in the local registry.
14
+ 1. The command looks up `<package-name>` in the local registry using the **input name** (unscoped is allowed).
15
15
  2. If the name is **unscoped** (e.g. `test`):
16
16
  - Authentication is validated using the provided profile/API key.
17
17
  - The current username (or profile’s default scope) is resolved.
18
- - A scoped name is computed (e.g. `@user/test`).
19
- - The local registry package is renamed to the scoped name using `renameRegistryPackage`.
20
- - The workspace package is updated (where possible) using `tryRenameWorkspacePackage`, so:
21
- - `package.yml` and related workspace configuration reflect the new scoped name.
22
- 3. After scoping:
23
- - **All further logic** (version resolution, checks, push) operates on the **scoped name**.
18
+ - A scoped upload name is computed (e.g. `@user/test`) via the existing prompt/default-scope flow.
19
+ - No local rename occurs; the local registry and workspace remain on the unscoped name.
20
+ 3. Before tarball creation:
21
+ - The package is cloned in-memory and its `.openpackage/package.yml` `name` field is rewritten to the scoped upload name.
22
+ - The upload payload (full or partial) uses this in-memory manifest, so the remote receives the scoped identity.
23
+ 4. Version selection and path validation still operate on the local name and local files.
24
24
 
25
25
  ---
26
26
 
27
27
  ## Invariants
28
28
 
29
- - After a successful scope operation:
30
- - The local registry no longer stores the unscoped name as the active location.
31
- - The scoped name (e.g. `@user/test`) is the canonical identity used by:
32
- - Version selection.
33
- - Tarball creation.
34
- - Remote upload.
35
- - The workspace is updated where possible so that:
36
- - Future `save`, `pack`, and `push` operations use the scoped name naturally.
37
- - References within the workspace (e.g. `package.yml`) do not drift from the registry identity.
29
+ - The local registry and workspace are **not** renamed by `push`; the unscoped layout remains intact.
30
+ - The upload payload always carries a scoped name (manifest rewritten in-memory) when pushing an unscoped package.
31
+ - Version selection and missing-path validation use the local name and files; only the upload name changes for remote interaction.
38
32
 
39
33
  ---
40
34
 
41
35
  ## Relationship to version selection
42
36
 
43
- - Scoping happens **before** version selection.
44
- - Once the package name is scoped:
45
- - All lookups for versions (`listPackageVersions`, `packageManager.loadPackage`) use the scoped name.
46
- - The version-selection rules (see `push-version-selection.md`) are applied strictly to the scoped name.
37
+ - Scoping for upload is determined before version selection, but local lookups use the **input name**.
38
+ - Version selection (`listPackageVersions`, `packageManager.loadPackage`) is driven by the local name; only the upload payload uses the scoped name.
47
39
 
48
40
 
@@ -1,9 +1,10 @@
1
- ## Push version selection (stable-only)
1
+ ## Push version selection (stable + unversioned)
2
2
 
3
3
  ### Terminology
4
4
 
5
5
  - **Stable version**: A semver-valid version with no prerelease segment, e.g. `1.2.3`.
6
6
  - **Prerelease version**: A semver-valid version with a prerelease segment, e.g. `1.2.3-dev.abc123`.
7
+ - **Unversioned package**: A package whose `package.yml` omits `version`; represented and stored as semver `0.0.0` locally (one per package) and can be pushed like any other stable version.
7
8
  - **Local registry**: The on-disk package store used by `opkg` (managed via `packageManager`).
8
9
 
9
10
  This document defines the rules `opkg push` uses to decide **which local version** is eligible to be pushed to the remote registry.
@@ -29,8 +30,7 @@ Given `<pkg>@<version>`:
29
30
 
30
31
  3. **Safety check**
31
32
  - After loading, the resulting `pkg.metadata.version` must still be stable.
32
- - If it is not (should only occur in pathological cases):
33
- - The push is rejected as if a prerelease was requested.
33
+ - If it is not (pathological), the push is rejected as a prerelease.
34
34
 
35
35
  **Result**
36
36
 
@@ -42,11 +42,12 @@ Given `<pkg>@<version>`:
42
42
 
43
43
  ## Implicit version: `opkg push <pkg>`
44
44
 
45
- When no version is explicitly supplied, `opkg push` must:
45
+ When no version is explicitly supplied, `opkg push`:
46
46
 
47
- - Consider **all local versions** of `<pkg>`.
48
- - Select the **latest stable version** only.
49
- - Treat the absence of stable versions as a **non-error** (informational) outcome.
47
+ - Considers **all local versions** of `<pkg>`.
48
+ - Prefers the **latest stable** version.
49
+ - If no stable exists but a `0.0.0` entry exists, uses that entry.
50
+ - Treats absence of both stable and `0.0.0` as a **non-error** (informational) outcome.
50
51
 
51
52
  ### Algorithm
52
53
 
@@ -61,28 +62,29 @@ Let `versions = listPackageVersions(pkg)` (all local versions, as directory name
61
62
  - Otherwise, returns the highest stable version using `semver.rsort`.
62
63
 
63
64
  2. If `latestStable === null`:
64
- - There is **no stable version** to push.
65
- - The CLI prints a “no stable versions found” message and a hint to use `opkg pack`.
66
- - The command exits with a **success** result (no error propagated to the global handler).
65
+ - No semver-stable versions exist (including `0.0.0`), so:
66
+ - Print the “no stable versions” message and exit with success.
67
67
 
68
- 3. If `latestStable` is present:
68
+ 3. If `latestStable` is present (including the case where it is `0.0.0`):
69
69
  - That version becomes the candidate `versionToPush`.
70
70
  - The user is asked to confirm before pushing (see behavior spec).
71
71
 
72
72
  **Notes**
73
73
 
74
- - Prerelease-only packages (e.g. `1.0.0-dev.abc`, `1.0.0-dev.def`) result in `latestStable === null`.
74
+ - Prerelease-only packages (e.g. `1.0.0-dev.abc`, `1.0.0-dev.def`) result in `latestStable === null`; if an unversioned entry exists, it is chosen, otherwise informational exit.
75
75
  - Mixed stable and prerelease sets (e.g. `1.0.0`, `1.1.0-dev.abc`, `1.2.0`) always choose the numerically highest stable (`1.2.0`).
76
76
 
77
77
  ---
78
78
 
79
- ## Stable-only guarantees (versioning view)
79
+ ## Stable/unversioned guarantees (versioning view)
80
80
 
81
81
  From the version-selection point of view:
82
82
 
83
- - `push` **never** picks a prerelease version as `versionToPush`.
83
+ - `push` **never** picks a prerelease version.
84
84
  - Explicit prerelease inputs are rejected up front.
85
- - Implicit selection ignores all prerelease versions when computing candidates.
86
- - Existing helper functions (`filterStableVersions`, `getLatestStableVersion`) centralize the definition of stable”.
85
+ - Implicit selection:
86
+ - Prefers latest stable (with `0.0.0` treated as a normal stable version).
87
+ - `0.0.0` pushes are treated the same as other stable versions.
88
+ - Ignores prereleases for candidacy.
87
89
 
88
90
 
@@ -40,7 +40,8 @@ The pipeline runs in one of two **modes**:
40
40
  #### 3. Inputs
41
41
 
42
42
  - **Working directory (`cwd`)** – establishes the workspace.
43
- - **Optional package name argument** – may be omitted (context detection) or provided explicitly.
43
+ - **Package name argument (optional)** – may be omitted (context detection) or provided explicitly.
44
+ - **Optional path argument (when package is provided)** – `opkg save <package> <path>` first runs the add pipeline for that path (including conflict handling and optional platform-specific transforms) and then saves the package snapshot.
44
45
 
45
46
  ---
46
47
 
@@ -50,4 +51,5 @@ The pipeline runs in one of two **modes**:
50
51
  - In WIP mode: can suppress prompts and allow overwriting existing WIP versions.
51
52
  - In stable mode: allows overwriting existing stable registry entries.
52
53
  - **`rename <newName>`** – optional new package name to apply during this pipeline run.
54
+ - **`platform-specific` (save only, when path is provided)** – forwarded to the add stage to generate platform-scoped variants for platform subdirectories.
53
55
 
@@ -7,6 +7,7 @@ For `save` command:
7
7
  - On each save, remove older WIP versions for the same workspace (per `workspaceHash`) to keep the registry clean.
8
8
  - Prefer not to update the `package.yml` version number; instead, keep WIP/stable details in `package.index.yml` and registry metadata.
9
9
  - The `save` command always saves the next prerelease version based on the current stable (for example: `1.0.0` then `1.0.1-000fz8.a3k`, `1.0.1-000fz9.a3k`, `1.0.1-000fza.a3k`).
10
+ - **Usage:** `opkg save` (cwd package), `opkg save <package>`, or `opkg save <package> <path>` (runs add for the path, then saves).
10
11
 
11
12
  For `pack` command:
12
13
  - This is essentially the same as “promote current workspace state to a stable version”.