@yargote/artifacts-cli 0.1.0-beta.5 → 0.1.0-beta.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.
package/README.md CHANGED
@@ -11,9 +11,9 @@ Use Node 24 or later. The beta targets macOS and Linux. Windows has not been ver
11
11
  Choose one persistent installation:
12
12
 
13
13
  ```sh
14
- npm install --global @yargote/artifacts-cli@0.1.0-beta.5
14
+ npm install --global @yargote/artifacts-cli@0.1.0-beta.6
15
15
  # Or, with pnpm and an existing global bin directory on PATH:
16
- pnpm add --global @yargote/artifacts-cli@0.1.0-beta.5
16
+ pnpm add --global @yargote/artifacts-cli@0.1.0-beta.6
17
17
  artifacts --version
18
18
  ```
19
19
 
@@ -22,8 +22,8 @@ If pnpm reports that no global bin directory exists, run `pnpm setup` and reopen
22
22
  For one-off execution, pin the same version:
23
23
 
24
24
  ```sh
25
- npx --yes @yargote/artifacts-cli@0.1.0-beta.5 --help
26
- pnpm dlx @yargote/artifacts-cli@0.1.0-beta.5 --help
25
+ npx --yes @yargote/artifacts-cli@0.1.0-beta.6 --help
26
+ pnpm dlx @yargote/artifacts-cli@0.1.0-beta.6 --help
27
27
  ```
28
28
 
29
29
  `@beta` follows future prereleases. Exact versions remain reproducible. No stable `latest` release is part of this beta. An organization's pnpm minimum release age or trust policy can delay a new release. Follow that policy rather than disabling it globally.
@@ -64,7 +64,7 @@ artifacts export ARTIFACT_ID --revision REVISION_ID --out artifact.zip
64
64
  artifacts logout
65
65
  ```
66
66
 
67
- Publication URLs require authenticated owner access. A retained request ID appears on stderr before each mutation. If the response is lost, run `artifacts retry REQUEST_ID` with the same server and state directory. Keep that state directory when upgrading, reinstalling, or switching between global and one-off execution.
67
+ A publication URL requires sign-in. It opens for the owner and for a user whose verified email equals an email share of the artifact. That user sees only the published revision and its attachments. A retained request ID appears on stderr before each mutation. If the response is lost, run `artifacts retry REQUEST_ID` with the same server and state directory. Keep that state directory when upgrading, reinstalling, or switching between global and one-off execution.
68
68
 
69
69
  ## Upgrade or uninstall
70
70
 
@@ -124,6 +124,9 @@ artifacts publish [ID] --command FILE
124
124
  artifacts export ID --revision REV --out FILE
125
125
  artifacts move ID --project NAME [--request-id ID]
126
126
  artifacts move ID --no-project [--request-id ID]
127
+ artifacts share ID --email EMAIL [--notify] [--request-id ID]
128
+ artifacts shares ID
129
+ artifacts unshare ID SHARE_ID [--request-id ID]
127
130
  artifacts retry REQUEST_ID
128
131
  artifacts skills [list]
129
132
  artifacts skills get NAME [--full]
@@ -197,6 +200,27 @@ Every artifact read and list item includes `project`, a name or null. `projects`
197
200
 
198
201
  Version `0.1.0-beta.4` and earlier parse artifact responses strictly and fail with `invalid_response` against a server that returns `project`. Upgrade the CLI together with the server.
199
202
 
203
+ ## Share publication access
204
+
205
+ These commands are available from `0.1.0-beta.6`. Check installed help for support.
206
+
207
+ ```sh
208
+ artifacts share ARTIFACT_ID --email ana@example.com --request-id share-ana-001
209
+ artifacts shares ARTIFACT_ID
210
+ artifacts unshare ARTIFACT_ID SHARE_ID --request-id unshare-ana-001
211
+ artifacts retry share-ana-001
212
+ ```
213
+
214
+ `share` trims and lowercases the email and prints one share as JSON with `shareId`, `artifactId`, `kind: "email"`, `value`, and `createdAt`. `shares` prints `{ "items": [...] }` with current grants and retains no operation. Take `SHARE_ID` from that output. `unshare` and a successful retry of an unshare exit zero with empty stdout. All three commands require the owner's `artifacts:publish` scope.
215
+
216
+ A signed-in user whose verified email matches a share can view the published revision and its attachments. History, export, edits, and project moves remain owner-only. Shares created before first publication remain stored, but the URL opens only after publication. Add `--notify` to request one invitation email. Invited people can sign in with an email code when the server enables email.
217
+
218
+ Share and unshare retain the complete normalized command before submission. Recover an uncertain result with `retry REQUEST_ID`. A replay confirms the original operation, so a historical share replay does not restore a revoked grant. An old unshare replay leaves a deliberate new share intact. Run `shares ARTIFACT_ID` to verify current grants.
219
+
220
+ Share with `--notify` to request one email attempt. Access succeeds when mail is disabled or the owner has used the daily allowance. The JSON result adds `notification.status` as `requested` or `skipped`, with `disabled` or `rate_limited` as the skip reason. `requested` does not confirm delivery. A crash after commit can lose the message. An exact retry preserves the captured flag and never sends a second email. Use a new deliberate command to request another attempt for existing access.
221
+
222
+ Owners can reserve 100 invitations per UTC day. Revoke, deletion, and failed delivery do not refund reservations. New access is limited to 100 people per artifact and 1000 grants per owner. Existing access remains listable and revocable.
223
+
200
224
  ## Complete commands and edits
201
225
 
202
226
  `--command FILE` accepts a complete create, update, or publish command. It cannot be combined with other mutation options. For update and publish, an optional positional artifact ID must equal the command's `artifactId`. Every field, including the omission of `response`, is retained.
@@ -228,7 +252,7 @@ Publish always selects an immutable revision. `--expected-version 0` expects no
228
252
 
229
253
  Each invocation resolves one access token for its reads, owner check, and submission. A concurrent login cannot change the command owner between these requests. Every mutation verifies the owner and retains an exclusive private operation file before submission. Missing request IDs default to random UUIDs. The operation stores its exact command, owner, and resource. It contains no credentials. Existing operation files require explicit `retry` and are never replaced by another preparation.
230
254
 
231
- `retry REQUEST_ID` verifies the current resource and owner, then sends the unchanged command once. Retry does not read a new head, change expected versions, add a response projection, or rebase a conflict. Its diagnostic identifies the result as a retained receipt. A fresh `read` returns current state.
255
+ `retry REQUEST_ID` verifies the current resource and owner, then sends the unchanged command once. Retry does not read a new head, change expected versions, add a response projection, or rebase a conflict. Its diagnostic identifies replay of the original operation and warns that replay does not establish current state. A fresh `read` or `shares` returns current state.
232
256
 
233
257
  If a response is lost after commit, retry recovers the original receipt. If a conflict occurs, the retained operation remains available for inspection. A new read and a deliberate new command with a new request ID are required to incorporate another writer's changes.
234
258
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yargote/artifacts-cli",
3
- "version": "0.1.0-beta.5",
3
+ "version": "0.1.0-beta.6",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Command-line client for the Artifacts REST API.",
@@ -27,8 +27,8 @@
27
27
  "skills"
28
28
  ],
29
29
  "dependencies": {
30
- "@yargote/artifacts-contracts": "0.1.0-beta.5",
31
- "@yargote/artifacts-client": "0.1.0-beta.5",
30
+ "@yargote/artifacts-contracts": "0.1.0-beta.6",
31
+ "@yargote/artifacts-client": "0.1.0-beta.6",
32
32
  "openid-client": "6.8.8",
33
33
  "zod": "4.6.5"
34
34
  },
@@ -41,7 +41,17 @@ Use `move ID --project NAME` to file an existing artifact under a project and `m
41
41
 
42
42
  Publish when the user requests publication, a shareable artifact URL, or a change to the live artifact. Use the exact saved revision selected by the user or returned by the edit. Read the current publication version before preparing publication. Use zero only when the artifact is unpublished.
43
43
 
44
- A published URL still requires its owner's Artifacts login. Publishing a historical revision leaves the draft head unchanged. Report the returned protected URL only after the publication succeeds and its current state is verified.
44
+ A published URL requires sign-in by the owner or a user whose verified email matches a current share. Publishing a historical revision leaves the draft head unchanged. Report the returned protected URL only after the publication succeeds and its current state is verified.
45
+
46
+ ## Share publication access
47
+
48
+ Check installed help for `share`, `shares`, and `unshare` support. These commands are available from `0.1.0-beta.6`. Share only when the user requests access for an exact email. Use `share ARTIFACT_ID --email EMAIL`, then verify the normalized email with `shares ARTIFACT_ID`. Use the observed `shareId` with `unshare ARTIFACT_ID SHARE_ID` to revoke it. Unshare and its successful retry exit zero with empty stdout.
49
+
50
+ A matching signed-in user must have a verified email. Access covers the published revision and its attachments. History, export, editing, and project moves stay owner-only. A share before first publication remains stored and waits for publication. Add `--notify` when the user asks for an invitation email. Invited people can sign in with an email code when the server enables email.
51
+
52
+ Recover uncertain share or unshare results with `retry REQUEST_ID`. Replay confirms the original operation and does not establish current grants. An old share replay does not restore revoked access. An old unshare replay does not remove a deliberate new share. Verify current grants with `shares ARTIFACT_ID`.
53
+
54
+ When the user asks to notify an invited person, add `--notify` to `artifacts share`. Preserve that flag in the retained command and use `artifacts retry` after an uncertain response. Report `requested` as an invitation request, never as confirmed delivery. A skipped notification still grants access. A new deliberate share command can request another attempt for existing access, subject to the owner's daily allowance.
45
55
 
46
56
  ## Recover an uncertain write
47
57
 
@@ -158,6 +158,27 @@ The publication example applies only to an unpublished artifact. For an existing
158
158
 
159
159
  History returns `nextBeforeSequence`; pass it as `--before-sequence N` to continue. List returns `nextCursor`; pass it as `--cursor CURSOR`. Do not substitute an offset or assume that one page contains everything.
160
160
 
161
+ ## Share publication access
162
+
163
+ Check installed help for these commands. They are available from `0.1.0-beta.6`.
164
+
165
+ ```sh
166
+ artifacts share ARTIFACT_ID --email ana@example.com --request-id share-ana-001
167
+ artifacts shares ARTIFACT_ID
168
+ artifacts unshare ARTIFACT_ID SHARE_ID --request-id unshare-ana-001
169
+ artifacts retry share-ana-001
170
+ ```
171
+
172
+ Share requires the owner's `artifacts:publish` scope, trims and lowercases the email, and prints a share with `shareId`, `artifactId`, `kind: "email"`, `value`, and `createdAt`. `shares` requires the same scope and prints `{ "items": [...] }` with current grants without retaining a mutation. Use an observed `shareId` to unshare. Unshare requires the same scope. Unshare and its successful retry exit zero with empty stdout.
173
+
174
+ The publication admits the owner and signed-in users whose verified email matches a current share. Guests see the published revision and attachments. History, export, editing, and project moves remain owner-only. Shares before first publication wait for publication. Add `--notify` to request one invitation email. Invited people can sign in with an email code when the server enables email.
175
+
176
+ Share and unshare retain complete normalized commands. `retry REQUEST_ID` replays the original operation. A historical share replay does not regrant revoked access, and an old unshare replay leaves a deliberate new share intact. Verify current grants with `shares ARTIFACT_ID` after a replay.
177
+
178
+ Share with `--notify` to request one email attempt. Access succeeds when mail is disabled or the owner has used the daily allowance. The JSON result adds `notification.status` as `requested` or `skipped`, with `disabled` or `rate_limited` as the skip reason. `requested` does not confirm delivery. A crash after commit can lose the message. An exact retry preserves the captured flag and never sends a second email. Use a new deliberate command to request another attempt for existing access.
179
+
180
+ Owners can reserve 100 invitations per UTC day. Revoke, deletion, and failed delivery do not refund reservations. New access is limited to 100 people per artifact and 1000 grants per owner. Existing access remains listable and revocable.
181
+
161
182
  ## Retry and export
162
183
 
163
184
  ```sh
package/src/index.js CHANGED
@@ -6,7 +6,7 @@ import { homedir } from 'node:os';
6
6
  import { join, resolve } from 'node:path';
7
7
  import { parseArgs } from 'node:util';
8
8
  import { z } from 'zod';
9
- import { createSchema, moveSchema, publishSchema, updateSchema } from '@yargote/artifacts-contracts';
9
+ import { createSchema, listSharesSchema, moveSchema, publishSchema, shareSchema, unshareSchema, updateSchema } from '@yargote/artifacts-contracts';
10
10
  import { canonicalResource } from '@yargote/artifacts-contracts/http';
11
11
  import { createArtifactHttpClient, HttpError, prepareArtifactEdit } from '@yargote/artifacts-client';
12
12
  import { accessToken, login, logout } from "./auth.js";
@@ -46,6 +46,9 @@ Commands:
46
46
  export ID --revision REV --out FILE
47
47
  move ID --project NAME [--request-id ID]
48
48
  move ID --no-project [--request-id ID]
49
+ share ID --email EMAIL [--notify] [--request-id ID]
50
+ shares ID
51
+ unshare ID SHARE_ID [--request-id ID]
49
52
  retry REQUEST_ID
50
53
 
51
54
  Mutation command files contain the complete create, update, or publish JSON.
@@ -75,6 +78,7 @@ const commandOptions = {
75
78
  update: ['snapshot', 'expected-revision', 'request-id', 'response', 'command'],
76
79
  edit: ['changes', 'request-id'], history: ['limit', 'before-sequence', 'summary'],
77
80
  publish: ['revision', 'expected-version', 'request-id', 'command'],
81
+ share: ['email', 'notify', 'request-id'], shares: [], unshare: ['request-id'],
78
82
  export: ['revision', 'out'], move: ['project', 'no-project', 'request-id'], retry: [],
79
83
  skills: ['full', 'all', 'agent', 'global', 'force'],
80
84
  };
@@ -112,7 +116,7 @@ async function main() {
112
116
  summary: { type: 'boolean' }, out: { type: 'string' }, quiet: { type: 'boolean' },
113
117
  full: { type: 'boolean' }, all: { type: 'boolean' }, agent: { type: 'string' },
114
118
  global: { type: 'boolean' }, force: { type: 'boolean' },
115
- project: { type: 'string' }, 'no-project': { type: 'boolean' },
119
+ project: { type: 'string' }, 'no-project': { type: 'boolean' }, email: { type: 'string' }, notify: { type: 'boolean' },
116
120
  },
117
121
  });
118
122
  const seen = new Set();
@@ -255,11 +259,18 @@ async function main() {
255
259
  throw new UsageError('Specify exactly one of --project NAME or --no-project');
256
260
  return mutate({ kind: 'move', command: moveSchema.parse({ artifactId, requestId: values['request-id'] ?? randomUUID(), project: values.project ?? null }) });
257
261
  }
262
+ case 'share': return mutate({ kind: 'share', command: shareSchema.parse({ artifactId: argument(), requestId: values['request-id'] ?? randomUUID(), email: required('email'), ...(values.notify === undefined ? {} : { notify: values.notify }) }) });
263
+ case 'shares': return client.listShares(listSharesSchema.parse({ artifactId: argument() }));
264
+ case 'unshare': {
265
+ if (arguments_.length !== 2)
266
+ throw new UsageError('unshare requires two positional arguments');
267
+ return mutate({ kind: 'unshare', command: unshareSchema.parse({ artifactId: arguments_[0], shareId: arguments_[1], requestId: values['request-id'] ?? randomUUID() }) });
268
+ }
258
269
  case 'retry': {
259
270
  const requestId = argument();
260
271
  const owner = await client.identity();
261
272
  const operation = await loadOperation(stateDir, requestId, resource, owner.userId);
262
- process.stderr.write(`Replaying retained ${operation.kind} operation ${requestId}. The result is its retained receipt and may differ from current state.\n`);
273
+ process.stderr.write(`Replaying retained ${operation.kind} operation ${requestId}. Replay confirms the original operation and does not establish current state.\n`);
263
274
  return submitOperation(client, operation);
264
275
  }
265
276
  }
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod';
2
- import { createSchema, moveSchema, publishSchema, updateSchema } from '@yargote/artifacts-contracts';
2
+ import { createSchema, moveSchema, publishSchema, shareSchema, unshareSchema, updateSchema } from '@yargote/artifacts-contracts';
3
3
  import type { ArtifactHttpClient } from '@yargote/artifacts-client';
4
4
  export declare const operationSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
5
5
  kind: z.ZodLiteral<"create">;
@@ -97,6 +97,28 @@ export declare const operationSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
97
97
  version: z.ZodLiteral<1>;
98
98
  resource: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
99
99
  ownerId: z.ZodString;
100
+ }, z.core.$strict>, z.ZodObject<{
101
+ kind: z.ZodLiteral<"share">;
102
+ command: z.ZodObject<{
103
+ requestId: z.ZodString;
104
+ artifactId: z.ZodString;
105
+ email: z.ZodPipe<z.ZodString, z.ZodEmail>;
106
+ notify: z.ZodOptional<z.ZodBoolean>;
107
+ requireNew: z.ZodOptional<z.ZodLiteral<true>>;
108
+ }, z.core.$strict>;
109
+ version: z.ZodLiteral<1>;
110
+ resource: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
111
+ ownerId: z.ZodString;
112
+ }, z.core.$strict>, z.ZodObject<{
113
+ kind: z.ZodLiteral<"unshare">;
114
+ command: z.ZodObject<{
115
+ requestId: z.ZodString;
116
+ artifactId: z.ZodString;
117
+ shareId: z.ZodString;
118
+ }, z.core.$strict>;
119
+ version: z.ZodLiteral<1>;
120
+ resource: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
121
+ ownerId: z.ZodString;
100
122
  }, z.core.$strict>], "kind">;
101
123
  export type PreparedOperation = z.infer<typeof operationSchema>;
102
124
  export type Mutation = {
@@ -111,11 +133,33 @@ export type Mutation = {
111
133
  } | {
112
134
  kind: 'move';
113
135
  command: z.input<typeof moveSchema>;
136
+ } | {
137
+ kind: 'share';
138
+ command: z.input<typeof shareSchema>;
139
+ } | {
140
+ kind: 'unshare';
141
+ command: z.input<typeof unshareSchema>;
114
142
  };
115
143
  export declare function operationPath(stateDir: string, resource: string, requestId: string): string;
116
144
  export declare function retainOperation(stateDir: string, resource: string, ownerId: string, mutation: Mutation): Promise<PreparedOperation>;
117
145
  export declare function loadOperation(stateDir: string, requestId: string, resource: string, ownerId: string): Promise<PreparedOperation>;
118
- export declare function submitOperation(client: ArtifactHttpClient, operation: PreparedOperation): Promise<{
146
+ export declare function submitOperation(client: ArtifactHttpClient, operation: PreparedOperation): Promise<void> | Promise<{
147
+ shareId: string;
148
+ artifactId: string;
149
+ kind: "email";
150
+ value: string;
151
+ createdAt: string;
152
+ notification?: {
153
+ status: "requested";
154
+ } | {
155
+ status: "skipped";
156
+ reason: "disabled";
157
+ } | {
158
+ status: "skipped";
159
+ reason: "rate_limited";
160
+ retryAt: string;
161
+ } | undefined;
162
+ }> | Promise<{
119
163
  artifactId: string;
120
164
  project: string | null;
121
165
  }> | Promise<{
package/src/operations.js CHANGED
@@ -3,7 +3,7 @@ import { lstat, mkdir, open } from 'node:fs/promises';
3
3
  import { createHash } from 'node:crypto';
4
4
  import { join } from 'node:path';
5
5
  import { z } from 'zod';
6
- import { createSchema, moveSchema, publishSchema, updateSchema } from '@yargote/artifacts-contracts';
6
+ import { createSchema, moveSchema, publishSchema, shareSchema, unshareSchema, updateSchema } from '@yargote/artifacts-contracts';
7
7
  import { canonicalResource } from '@yargote/artifacts-contracts/http';
8
8
  const binding = {
9
9
  version: z.literal(1),
@@ -15,6 +15,8 @@ export const operationSchema = z.discriminatedUnion('kind', [
15
15
  z.strictObject({ ...binding, kind: z.literal('update'), command: updateSchema }),
16
16
  z.strictObject({ ...binding, kind: z.literal('publish'), command: publishSchema }),
17
17
  z.strictObject({ ...binding, kind: z.literal('move'), command: moveSchema }),
18
+ z.strictObject({ ...binding, kind: z.literal('share'), command: shareSchema }),
19
+ z.strictObject({ ...binding, kind: z.literal('unshare'), command: unshareSchema }),
18
20
  ]);
19
21
  export function operationPath(stateDir, resource, requestId) {
20
22
  createSchema.shape.requestId.parse(requestId);
@@ -88,5 +90,9 @@ export function submitOperation(client, operation) {
88
90
  case 'update': return client.update(operation.command);
89
91
  case 'publish': return client.publish(operation.command);
90
92
  case 'move': return client.move(operation.command);
93
+ case 'share': return client.share(operation.command);
94
+ case 'unshare': return client.unshare(operation.command);
91
95
  }
96
+ const exhaustive = operation;
97
+ throw new Error(`Unsupported retained operation: ${exhaustive}`);
92
98
  }