@yargote/artifacts-cli 0.1.0-beta.3 → 0.1.0-beta.4

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.3
14
+ npm install --global @yargote/artifacts-cli@0.1.0-beta.4
15
15
  # Or, with pnpm and an existing global bin directory on PATH:
16
- pnpm add --global @yargote/artifacts-cli@0.1.0-beta.3
16
+ pnpm add --global @yargote/artifacts-cli@0.1.0-beta.4
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.3 --help
26
- pnpm dlx @yargote/artifacts-cli@0.1.0-beta.3 --help
25
+ npx --yes @yargote/artifacts-cli@0.1.0-beta.4 --help
26
+ pnpm dlx @yargote/artifacts-cli@0.1.0-beta.4 --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.
@@ -108,9 +108,11 @@ The private `credentials.sqlite` file stores credentials. The separate `credenti
108
108
  artifacts login [--no-browser]
109
109
  artifacts logout
110
110
  artifacts whoami
111
- artifacts create --snapshot FILE [--request-id ID] [--response full|compact]
111
+ artifacts create --snapshot FILE [--project NAME] [--request-id ID]
112
+ [--response full|compact]
112
113
  artifacts create --command FILE
113
- artifacts list [--limit N] [--cursor CURSOR]
114
+ artifacts list [--limit N] [--cursor CURSOR] [--project NAME]
115
+ artifacts projects
114
116
  artifacts read ID [--revision REV] [--response full|compact] [--out FILE [--quiet]]
115
117
  artifacts update ID --snapshot FILE --expected-revision REV [--request-id ID]
116
118
  [--response full|compact]
@@ -120,6 +122,8 @@ artifacts history ID [--limit N] [--before-sequence N] [--summary]
120
122
  artifacts publish ID --revision REV --expected-version N [--request-id ID]
121
123
  artifacts publish [ID] --command FILE
122
124
  artifacts export ID --revision REV --out FILE
125
+ artifacts move ID --project NAME [--request-id ID]
126
+ artifacts move ID --no-project [--request-id ID]
123
127
  artifacts retry REQUEST_ID
124
128
  artifacts skills [list]
125
129
  artifacts skills get NAME [--full]
@@ -175,6 +179,24 @@ A default full read contains the complete snapshot under `revision.snapshot`. `e
175
179
 
176
180
  History defaults to complete revisions. `--summary` returns revision metadata and attachment summaries. `--before-sequence` continues a page. `list --cursor` continues an artifact list.
177
181
 
182
+ ## Projects
183
+
184
+ A project groups artifacts by name. `create --snapshot FILE --project NAME` files the new artifact under that project. A name that matches an existing project regardless of letter case reuses its stored spelling, so `--project alpha` joins `Alpha`. Names are trimmed and NFC-normalized, contain 1 to 80 characters, and cannot contain control characters. A complete create command file can include the same optional `project` field.
185
+
186
+ ```sh
187
+ artifacts create --snapshot snapshot.json --project Alpha
188
+ artifacts list --project alpha
189
+ artifacts projects
190
+ artifacts move ARTIFACT_ID --project Beta
191
+ artifacts move ARTIFACT_ID --no-project
192
+ ```
193
+
194
+ Every artifact read and list item includes `project`, a name or null. `projects` prints `{ "items": [{ "name": "Alpha", "artifactCount": 1 }] }`, ordered by name without regard to letter case. A project exists only while it contains an artifact.
195
+
196
+ `move ID` requires exactly one of `--project NAME` or `--no-project`. It prints `{ "artifactId": "...", "project": "Beta" }` with the stored spelling, or `null` after `--no-project`. Move never creates a revision or changes the publication. Moving to the current project succeeds without change. Move is a retained mutation like the others, so `retry` recovers a lost response.
197
+
198
+ 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
+
178
200
  ## Complete commands and edits
179
201
 
180
202
  `--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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yargote/artifacts-cli",
3
- "version": "0.1.0-beta.3",
3
+ "version": "0.1.0-beta.4",
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.3",
31
- "@yargote/artifacts-client": "0.1.0-beta.3",
30
+ "@yargote/artifacts-contracts": "0.1.0-beta.4",
31
+ "@yargote/artifacts-client": "0.1.0-beta.4",
32
32
  "openid-client": "6.8.8",
33
33
  "zod": "4.6.5"
34
34
  },
@@ -31,6 +31,12 @@ Resolve an existing artifact by its supplied ID, an observed publication URL, or
31
31
 
32
32
  Use `edit` for partial changes that preserve omitted fields. Use `update` for a complete next snapshot with the observed `headRevisionId` as its expected revision. Changing the attachment list replaces that inventory; an empty list removes all attachments. An update creates a retained revision and does not change the published revision.
33
33
 
34
+ ## Organize projects
35
+
36
+ When the user names a project for new content, add `--project NAME` to `create`. Run `artifacts projects` first when an existing project may already fit. A name matching an existing project regardless of letter case joins it and keeps the stored spelling. Do not invent a project the user did not ask for. Use `list --project NAME` to find artifacts in a project.
37
+
38
+ Use `move ID --project NAME` to file an existing artifact under a project and `move ID --no-project` to remove it from one. A move changes only the project. It creates no revision and leaves the publication unchanged. It is a retained mutation, so recover an uncertain move with `retry`. Verify it with `read` or `list --project NAME`. A project disappears when its last artifact leaves it.
39
+
34
40
  ## Publish deliberately
35
41
 
36
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.
@@ -75,6 +75,22 @@ For `read`, `--out FILE --quiet` saves the returned response without printing su
75
75
 
76
76
  Check installed help for `--quiet` support before running these examples. Older versions, including `0.1.0-beta.1`, can suppress stdout with `artifacts read ARTIFACT_ID --out saved.json > /dev/null`. This redirection preserves stderr and the command's exit status. Use the same fallback for the revision reads below.
77
77
 
78
+ ## Organize projects
79
+
80
+ Check that installed help lists `projects` and `move` before using them.
81
+
82
+ ```sh
83
+ artifacts create --snapshot snapshot.json --project "Client onboarding"
84
+ artifacts projects
85
+ artifacts list --project "client onboarding"
86
+ artifacts move ARTIFACT_ID --project Archive
87
+ artifacts move ARTIFACT_ID --no-project
88
+ ```
89
+
90
+ A project name contains 1 to 80 characters after trimming and NFC normalization, without control characters. A name that matches an existing project regardless of letter case joins it and keeps its stored spelling. Every read and list item includes `project`, a name or null. `projects` prints `{ "items": [{ "name": "Client onboarding", "artifactCount": 1 }] }`, ordered by name without regard to letter case. A project exists only while it contains an artifact.
91
+
92
+ `move` takes exactly one of `--project NAME` or `--no-project` and prints `{ "artifactId": "...", "project": "Archive" }`, or `null` for the project after `--no-project`. It creates no revision and leaves the publication unchanged. Moving to the current project succeeds without change. A lost move response is recovered with `retry REQUEST_ID` like any other retained mutation.
93
+
78
94
  ## Edit without losing retained data
79
95
 
80
96
  For a title change, save `changes.json` with only the requested field.
@@ -104,7 +120,7 @@ Command files contain the command itself, without the CLI's retained-operation w
104
120
  | `update` | `requestId`, `artifactId`, `expectedRevisionId`, `snapshot` |
105
121
  | `publish` | `requestId`, `artifactId`, `revisionId`, `expectedPublicationVersion` |
106
122
 
107
- `requestId` is a caller-chosen string of 8 to 128 characters. Create and update also accept optional `response` with value `full` or `compact`. This complete update example replaces a snapshot. Substitute the observed IDs and preserve all required content and attachments in its `snapshot`.
123
+ `requestId` is a caller-chosen string of 8 to 128 characters. Create and update also accept optional `response` with value `full` or `compact`. Create also accepts an optional `project` name. This complete update example replaces a snapshot. Substitute the observed IDs and preserve all required content and attachments in its `snapshot`.
108
124
 
109
125
  ```json
110
126
  {
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, publishSchema, updateSchema } from '@yargote/artifacts-contracts';
9
+ import { createSchema, moveSchema, publishSchema, 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";
@@ -30,9 +30,11 @@ Commands:
30
30
  login [--no-browser]
31
31
  logout
32
32
  whoami
33
- create --snapshot FILE [--request-id ID] [--response full|compact]
33
+ create --snapshot FILE [--project NAME] [--request-id ID]
34
+ [--response full|compact]
34
35
  create --command FILE
35
- list [--limit N] [--cursor CURSOR]
36
+ list [--limit N] [--cursor CURSOR] [--project NAME]
37
+ projects
36
38
  read ID [--revision REV] [--response full|compact] [--out FILE [--quiet]]
37
39
  update ID --snapshot FILE --expected-revision REV [--request-id ID]
38
40
  [--response full|compact]
@@ -42,11 +44,16 @@ Commands:
42
44
  publish ID --revision REV --expected-version N [--request-id ID]
43
45
  publish [ID] --command FILE
44
46
  export ID --revision REV --out FILE
47
+ move ID --project NAME [--request-id ID]
48
+ move ID --no-project [--request-id ID]
45
49
  retry REQUEST_ID
46
50
 
47
51
  Mutation command files contain the complete create, update, or publish JSON.
48
52
  Mutation request IDs default to a random UUID. Commands are retained before
49
53
  submission. Retry sends the exact retained command for the same server and owner.
54
+ Projects group artifacts by name without regard to letter case. A project
55
+ exists while it contains an artifact. Move changes the project of an artifact
56
+ without creating a revision; --no-project removes it from its project.
50
57
  Read --out saves the returned artifact JSON. Export --out saves the ZIP.
51
58
  Read --quiet requires --out and suppresses successful JSON stdout.
52
59
  Output files are created exclusively and never overwrite an existing path.
@@ -63,12 +70,12 @@ class UsageError extends Error {
63
70
  }
64
71
  const commandOptions = {
65
72
  login: ['no-browser'], logout: [], whoami: [],
66
- create: ['snapshot', 'request-id', 'response', 'command'],
67
- list: ['limit', 'cursor'], read: ['revision', 'response', 'out', 'quiet'],
73
+ create: ['snapshot', 'project', 'request-id', 'response', 'command'],
74
+ list: ['limit', 'cursor', 'project'], projects: [], read: ['revision', 'response', 'out', 'quiet'],
68
75
  update: ['snapshot', 'expected-revision', 'request-id', 'response', 'command'],
69
76
  edit: ['changes', 'request-id'], history: ['limit', 'before-sequence', 'summary'],
70
77
  publish: ['revision', 'expected-version', 'request-id', 'command'],
71
- export: ['revision', 'out'], retry: [],
78
+ export: ['revision', 'out'], move: ['project', 'no-project', 'request-id'], retry: [],
72
79
  skills: ['full', 'all', 'agent', 'global', 'force'],
73
80
  };
74
81
  async function jsonFile(path) {
@@ -105,6 +112,7 @@ async function main() {
105
112
  summary: { type: 'boolean' }, out: { type: 'string' }, quiet: { type: 'boolean' },
106
113
  full: { type: 'boolean' }, all: { type: 'boolean' }, agent: { type: 'string' },
107
114
  global: { type: 'boolean' }, force: { type: 'boolean' },
115
+ project: { type: 'string' }, 'no-project': { type: 'boolean' },
108
116
  },
109
117
  });
110
118
  const seen = new Set();
@@ -203,11 +211,14 @@ async function main() {
203
211
  if (values.command)
204
212
  return mutate(await commandFile('create'));
205
213
  none();
206
- return mutate({ kind: 'create', command: createSchema.parse({ requestId: values['request-id'] ?? randomUUID(), snapshot: await jsonFile(required('snapshot')), ...(response === undefined ? {} : { response }) }) });
214
+ return mutate({ kind: 'create', command: createSchema.parse({ requestId: values['request-id'] ?? randomUUID(), snapshot: await jsonFile(required('snapshot')), ...(values.project === undefined ? {} : { project: values.project }), ...(response === undefined ? {} : { response }) }) });
207
215
  }
208
216
  case 'list':
209
217
  none();
210
- return client.list({ ...(values.limit === undefined ? {} : { limit: integer('limit') }), ...(values.cursor === undefined ? {} : { cursor: values.cursor }) });
218
+ return client.list({ ...(values.limit === undefined ? {} : { limit: integer('limit') }), ...(values.cursor === undefined ? {} : { cursor: values.cursor }), ...(values.project === undefined ? {} : { project: values.project }) });
219
+ case 'projects':
220
+ none();
221
+ return client.projects();
211
222
  case 'read': {
212
223
  const result = await client.read({ artifactId: argument(), ...(values.revision === undefined ? {} : { revisionId: values.revision }), ...(response === undefined ? {} : { response }) });
213
224
  if (values.out)
@@ -238,6 +249,12 @@ async function main() {
238
249
  await writeOutput(output, archive.bytes);
239
250
  return { filename: archive.filename, path: resolve(output), mediaType: archive.mediaType, sha256: archive.sha256, bytes: archive.bytes.byteLength };
240
251
  }
252
+ case 'move': {
253
+ const artifactId = argument();
254
+ if ((values.project !== undefined) === (values['no-project'] ?? false))
255
+ throw new UsageError('Specify exactly one of --project NAME or --no-project');
256
+ return mutate({ kind: 'move', command: moveSchema.parse({ artifactId, requestId: values['request-id'] ?? randomUUID(), project: values.project ?? null }) });
257
+ }
241
258
  case 'retry': {
242
259
  const requestId = argument();
243
260
  const owner = await client.identity();
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod';
2
- import { createSchema, publishSchema, updateSchema } from '@yargote/artifacts-contracts';
2
+ import { createSchema, moveSchema, publishSchema, 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">;
@@ -29,6 +29,7 @@ export declare const operationSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
29
29
  data: z.ZodString;
30
30
  }, z.core.$strict>>;
31
31
  }, z.core.$strict>;
32
+ project: z.ZodOptional<z.ZodString>;
32
33
  response: z.ZodOptional<z.ZodEnum<{
33
34
  full: "full";
34
35
  compact: "compact";
@@ -86,6 +87,16 @@ export declare const operationSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
86
87
  version: z.ZodLiteral<1>;
87
88
  resource: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
88
89
  ownerId: z.ZodString;
90
+ }, z.core.$strict>, z.ZodObject<{
91
+ kind: z.ZodLiteral<"move">;
92
+ command: z.ZodObject<{
93
+ requestId: z.ZodString;
94
+ artifactId: z.ZodString;
95
+ project: z.ZodNullable<z.ZodString>;
96
+ }, z.core.$strict>;
97
+ version: z.ZodLiteral<1>;
98
+ resource: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
99
+ ownerId: z.ZodString;
89
100
  }, z.core.$strict>], "kind">;
90
101
  export type PreparedOperation = z.infer<typeof operationSchema>;
91
102
  export type Mutation = {
@@ -97,48 +108,19 @@ export type Mutation = {
97
108
  } | {
98
109
  kind: 'publish';
99
110
  command: z.input<typeof publishSchema>;
111
+ } | {
112
+ kind: 'move';
113
+ command: z.input<typeof moveSchema>;
100
114
  };
101
115
  export declare function operationPath(stateDir: string, resource: string, requestId: string): string;
102
116
  export declare function retainOperation(stateDir: string, resource: string, ownerId: string, mutation: Mutation): Promise<PreparedOperation>;
103
117
  export declare function loadOperation(stateDir: string, requestId: string, resource: string, ownerId: string): Promise<PreparedOperation>;
104
118
  export declare function submitOperation(client: ArtifactHttpClient, operation: PreparedOperation): Promise<{
119
+ artifactId: string;
120
+ project: string | null;
121
+ }> | Promise<{
105
122
  revisionId: string;
106
123
  version: number;
107
124
  url: string;
108
125
  artifactId: string;
109
- }> | Promise<{
110
- artifactId: string;
111
- ownerId: string;
112
- headRevisionId: string;
113
- publication: {
114
- revisionId: string;
115
- version: number;
116
- url: string;
117
- } | null;
118
- revision: {
119
- id: string;
120
- parentRevisionId: string | null;
121
- sequence: number;
122
- authorUserId: string;
123
- authorClientId: string;
124
- createdAt: string;
125
- sha256: string;
126
- snapshot: {
127
- metadata: {
128
- title: string;
129
- description: string;
130
- custom?: z.core.util.JSONType | undefined;
131
- };
132
- content: {
133
- kind: "text" | "html" | "markdown";
134
- text: string;
135
- };
136
- files: {
137
- path: string;
138
- mediaType: string;
139
- encoding: "utf8" | "base64";
140
- data: string;
141
- }[];
142
- };
143
- };
144
- } | import("@yargote/artifacts-client").CompactArtifactRead>;
126
+ }>;
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, publishSchema, updateSchema } from '@yargote/artifacts-contracts';
6
+ import { createSchema, moveSchema, publishSchema, updateSchema } from '@yargote/artifacts-contracts';
7
7
  import { canonicalResource } from '@yargote/artifacts-contracts/http';
8
8
  const binding = {
9
9
  version: z.literal(1),
@@ -14,6 +14,7 @@ export const operationSchema = z.discriminatedUnion('kind', [
14
14
  z.strictObject({ ...binding, kind: z.literal('create'), command: createSchema }),
15
15
  z.strictObject({ ...binding, kind: z.literal('update'), command: updateSchema }),
16
16
  z.strictObject({ ...binding, kind: z.literal('publish'), command: publishSchema }),
17
+ z.strictObject({ ...binding, kind: z.literal('move'), command: moveSchema }),
17
18
  ]);
18
19
  export function operationPath(stateDir, resource, requestId) {
19
20
  createSchema.shape.requestId.parse(requestId);
@@ -86,5 +87,6 @@ export function submitOperation(client, operation) {
86
87
  case 'create': return client.create(operation.command);
87
88
  case 'update': return client.update(operation.command);
88
89
  case 'publish': return client.publish(operation.command);
90
+ case 'move': return client.move(operation.command);
89
91
  }
90
92
  }