Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
built for agents that author and review Foundation Plans with their users.

The package is not released yet. This repository contains the auditable command shell, local Foundation Plan
initialization, and conditional whole-document push; release behavior will arrive in reviewed increments.
initialization, subject identity generation, and conditional whole-document push; release behavior will arrive in
reviewed increments.

## Requirements

Expand All @@ -31,6 +32,19 @@ This creates an empty `sketch/0.19` Plan and client-generated Project ID under `
keeps that local scratch area out of Git without changing the project's own `.gitignore`. Initialization makes no
network request and refuses to replace an existing `.firstdraft` path.

## Add Foundation Plan subjects

Generate an identity before adding each new independently mutable authored subject:

```sh
firstdraft plan subject-id
```

The command prints one UUIDv7 for the subject's `subject_uuid`. It does not read or modify the Plan, reserve the
value, or make a network request. Preserve that UUID when renaming the subject or moving it to a different semantic
owner without changing its kind. Use a new UUID for a replacement concept. Readable keys and paths may change and
remain the document's links; the UUID preserves continuity between complete-document pushes.

## Push a Foundation Plan

From the initialized project:
Expand Down
11 changes: 11 additions & 0 deletions scripts/smoke-package.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,17 @@ try {

assert.equal(execution.stdout, `${packageMetadata.version}\n`);
assert.equal(execution.stderr, "");

const subjectId = runNpm(
["exec", "--offline", "--", "firstdraft", "plan", "subject-id"],
installationDirectory,
);

assert.match(
subjectId.stdout,
/^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\n$/,
);
assert.equal(subjectId.stderr, "");
} finally {
rmSync(temporaryDirectory, { recursive: true, force: true });
}
Expand Down
80 changes: 73 additions & 7 deletions src/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,9 @@ Usage:
firstdraft plan <command> [options]

Commands:
init Create a local empty Foundation Plan
push Send the local Foundation Plan to First Draft
init Create a local empty Foundation Plan
subject-id Generate a UUIDv7 for a new Plan subject
push Send the local Foundation Plan to First Draft

Options:
-h, --help Show help
Expand All @@ -55,6 +56,18 @@ The first successful push saves its API origin in .firstdraft/state.json.
Later pushes reject a different origin.
`;

const PLAN_SUBJECT_ID_HELP = `First Draft CLI

Usage:
firstdraft plan subject-id

Prints one UUIDv7 for a new independently mutable Plan subject.
The command reads no files and makes no network request.

Options:
-h, --help Show help
`;

const PLAN_INIT_HELP = `First Draft CLI

Usage:
Expand Down Expand Up @@ -89,6 +102,8 @@ const PLAN_PUSH_NETWORK_ERROR =
"Could not complete the First Draft request. The Plan may have been accepted; local state was not changed.\n";
const PLAN_PUSH_PROTOCOL_ERROR =
"First Draft returned an unexpected response. The Plan may have been accepted; local state was not changed.\n";
const PLAN_SUBJECT_ID_USAGE_ERROR =
"Invalid arguments.\nRun 'firstdraft plan subject-id --help' for usage.\n";

/**
* @typedef {object} Writer
Expand All @@ -101,7 +116,9 @@ const PLAN_PUSH_PROTOCOL_ERROR =
* @property {Writer} stdout
* @property {Writer} stderr
* @property {string} [cwd]
* @property {() => string} [getCwd]
* @property {() => string} [createProjectId]
* @property {() => string} [createSubjectId]
* @property {import("./commands/plan-init.js").FileSystem} [fileSystem]
* @property {typeof globalThis.fetch} [fetchFunction]
* @property {import("./commands/plan-push.js").PlanPushFileSystem} [planPushFileSystem]
Expand All @@ -117,6 +134,7 @@ const PLAN_PUSH_PROTOCOL_ERROR =
* @property {Writer} stderr
* @property {string} cwd
* @property {() => string} createProjectId
* @property {() => string} createSubjectId
* @property {import("./commands/plan-init.js").FileSystem} [fileSystem]
* @property {typeof globalThis.fetch} [fetchFunction]
* @property {import("./commands/plan-push.js").PlanPushFileSystem} [planPushFileSystem]
Expand All @@ -125,13 +143,19 @@ const PLAN_PUSH_PROTOCOL_ERROR =
* @property {string} [apiUrl]
*/

/**
* @typedef {Omit<CommandOptions, "cwd"> & {cwd?: string, getCwd: () => string}} PlanCommandOptions
*/

/** @param {RunOptions} options */
export async function run({
argv,
stdout,
stderr,
cwd = process.cwd(),
cwd,
getCwd = process.cwd,
createProjectId = generateUuidV7,
createSubjectId = generateUuidV7,
fileSystem,
fetchFunction,
planPushFileSystem,
Expand All @@ -145,7 +169,9 @@ export async function run({
stdout,
stderr,
cwd,
getCwd,
createProjectId,
createSubjectId,
fileSystem,
fetchFunction,
planPushFileSystem,
Expand Down Expand Up @@ -201,13 +227,15 @@ function runRoot({ argv, stdout, stderr }) {
return 0;
}

/** @param {CommandOptions} options */
/** @param {PlanCommandOptions} options */
async function runPlan({
argv,
stdout,
stderr,
cwd,
getCwd,
createProjectId,
createSubjectId,
fileSystem,
fetchFunction,
planPushFileSystem,
Expand All @@ -220,7 +248,7 @@ async function runPlan({
argv: argv.slice(1),
stdout,
stderr,
cwd,
cwd: cwd ?? getCwd(),
createProjectId,
fileSystem,
});
Expand All @@ -231,7 +259,7 @@ async function runPlan({
argv: argv.slice(1),
stdout,
stderr,
cwd,
cwd: cwd ?? getCwd(),
fetchFunction,
planPushFileSystem,
createTemporaryId,
Expand All @@ -240,6 +268,15 @@ async function runPlan({
});
}

if (argv[0] === "subject-id") {
return runPlanSubjectId({
argv: argv.slice(1),
stdout,
stderr,
createSubjectId,
});
}

const parsed = parseArguments(() =>
parseArgs({
args: [...argv],
Expand Down Expand Up @@ -268,6 +305,33 @@ async function runPlan({
return 0;
}

/**
* @param {Pick<CommandOptions, "argv" | "stdout" | "stderr" | "createSubjectId">} options
*/
function runPlanSubjectId({ argv, stdout, stderr, createSubjectId }) {
const parsed = parseArguments(() =>
parseArgs({
args: [...argv],
options: { help: { type: "boolean", short: "h" } },
allowPositionals: false,
strict: true,
}),
);

if (!parsed) {
stderr.write(PLAN_SUBJECT_ID_USAGE_ERROR);
return 2;
}

if (parsed.values.help) {
stdout.write(PLAN_SUBJECT_ID_HELP);
return 0;
}

stdout.write(`${createSubjectId()}\n`);
return 0;
}

/**
* @param {Pick<CommandOptions, "argv" | "stdout" | "stderr" | "cwd" | "fetchFunction" | "planPushFileSystem" | "createTemporaryId" | "createRequestSignal" | "apiUrl">} options
*/
Expand Down Expand Up @@ -365,7 +429,9 @@ async function runPlanPush({
return 0;
}

/** @param {CommandOptions} options */
/**
* @param {Pick<CommandOptions, "argv" | "stdout" | "stderr" | "cwd" | "createProjectId" | "fileSystem">} options
*/
function runPlanInit({
argv,
stdout,
Expand Down
7 changes: 4 additions & 3 deletions test/plan-init.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,9 @@ Usage:
firstdraft plan <command> [options]

Commands:
init Create a local empty Foundation Plan
push Send the local Foundation Plan to First Draft
init Create a local empty Foundation Plan
subject-id Generate a UUIDv7 for a new Plan subject
push Send the local Foundation Plan to First Draft

Options:
-h, --help Show help
Expand Down Expand Up @@ -72,7 +73,7 @@ const EXPECTED_STATE = `{
}
`;

test("plan help describes the available command", async () => {
test("plan help describes the available commands", async () => {
assert.deepEqual(await invoke(["plan"]), {
status: 0,
stdout: PLAN_HELP,
Expand Down
Loading